我是否应该将 ILogger、 ILogger < T > 、 ILoggerFactory 或 ILoggerProvider 作为库?

这可能在某种程度上与 在 AspNet 内核中将 ILogger 或 ILoggerFactory 传递给构造函数?有关,但是这是关于 图书馆设计的,而不是关于使用这些库的实际应用程序如何实现它的日志记录。

我正在写一个。Net 标准2.0库,将通过 Nuget 安装,并允许人们使用该库获得一些调试信息,我依赖于 微软。扩展。日志记录。抽象,以允许一个标准的日志器被注入。

但是,我看到了多个接口,而且 Web 上的示例代码有时使用 ILoggerFactory并在类的 ctor 中创建一个日志记录器。还有 ILoggerProvider,它看起来像是工厂的只读版本,但是实现可能实现也可能不实现这两个接口,所以我必须选择。(工厂似乎比提供者更常见)。

我看到的一些代码使用非通用的 ILogger接口,甚至可能共享同一个日志记录器的一个实例,还有一些代码在其 ctor 中使用 ILogger<T>,并期望 DI 容器支持开放的通用类型或者显式注册我的库使用的每个 ILogger<T>变体。

现在,我确实认为 ILogger<T>是正确的方法,也许一个 ctor 不接受这个参数,而只是传递一个 Null Logger。这样,如果不需要日志记录,就不需要使用日志记录。然而,一些 DI 容器选择了最大的 ctor,因此无论如何都会失败。

我很好奇我在 假设这里要做什么来为用户创建最少的麻烦,同时仍然允许适当的日志支持,如果需要的话。

67225 次浏览

这些都是有效的,除了 ILoggerProviderILoggerILogger<T>应该用于日志记录。要得到一个 ILogger,你使用一个 ILoggerFactoryILogger<T>是获取特定类别的日志记录器的快捷方式(类型作为类别的快捷方式)。

当使用 ILogger执行日志记录时,每个已注册的 ILoggerProvider都有机会处理该日志消息。直接调用 ILoggerProvider的代码实际上是无效的。

ILogger<T>实际上是为 DI 制作的。ILogger<T>的出现是为了帮助更容易地实现工厂模式,而不是您自己编写所有的 DI 和 Factory 逻辑,这是 ASP.NET Core 中最聪明的决策之一

你可以选择:

如果需要在代码 或者中使用工厂和 DI 模式,可以使用 ILogger实现简单的日志记录,而不需要 DI。

因此,ILoggerProvider只是处理每个注册日志消息的桥梁。没有必要使用它,因为它不会影响您应该干预代码的任何事情。它侦听注册的 ILoggerProvider并处理消息。就是这样。

定义

我们有3个接口: ILoggerILoggerProviderILoggerFactory。让我们看看 源代码,了解它们的职责:

ILogger : 负责编写给定 日志级别的日志消息。

ILoggerProvider : 负责创建 ILogger的实例(您不应该直接使用 ILoggerProvider来创建日志记录器)

ILoggerFactory : 您可以向工厂注册一个或多个 ILoggerProvider,然后工厂使用所有这些 ILoggerProvider来创建 ILogger的实例。ILoggerFactory包含 ILoggerProviders的集合。

在下面的示例中,我们向工厂注册了2个提供程序(控制台和文件)。当我们创建一个日志记录器时,工厂使用这两个提供程序来创建 Logger的一个实例:

ILoggerFactory factory = new LoggerFactory().AddConsole();    // add console provider
factory.AddProvider(new LoggerFileProvider("c:\\log.txt"));   // add file provider
Logger logger = factory.CreateLogger(); // creates a console logger and a file logger

因此,日志记录器本身,正在维护一个 ILogger集合,并将日志消息写入所有 ILogger集合。我们可以确认 Logger有一个 ILoggers数组(即 LoggerInformation[]) ,同时它正在实现 ILogger接口。


依赖注入

MS 文档 提供了两种注入日志记录器的方法:

1. 注射工厂:

public TodoController(ITodoRepository todoRepository, ILoggerFactory logger)
{
_todoRepository = todoRepository;
_logger = logger.CreateLogger("TodoApi.Controllers.TodoController");
}

创建一个 Logger,类别 = TodoApi. Controllers. TodoController

2. 注射一种普通的 ILogger<T>:

public TodoController(ITodoRepository todoRepository, ILogger<TodoController> logger)
{
_todoRepository = todoRepository;
_logger = logger;
}

创建一个类别为 Category = 完全限定类型名为 TodoController 的日志记录器


在我看来,使文档令人困惑的是它没有提到任何关于注入非泛型的 ILogger的内容。在上面的同一个例子中,我们注入了一个非通用的 ITodoRepository,但是,它没有解释为什么我们没有对 ILogger做同样的事情。

根据 Mark Seemann:

注入构造函数所做的不应该超过接收 依赖关系。

将工厂注入 Controller 不是一个好方法,因为 Controller 没有责任初始化 Logger (违反 SRP)。同时注入一个通用的 ILogger<T>增加了不必要的噪音。更多细节见 简单注射器 blog: ASP.NET 核心 DI 抽象有什么问题?

应该注入的(至少根据上面的文章)是一个非通用的 ILogger,但是,这不是微软的内置 DI 容器可以做的事情,你需要使用第三方 DI 库。这些 文档解释了如何使用第三方库。NET 核心。


这是 Nikola Malovic 的 另一篇文章,他在其中解释了 IoC 的5条定律。

尼古拉的物联网第四定律

正在解析的类的每个构造函数都不应该有任何 实现,而不是接受它自己的一组依赖项。

对于图书馆设计来说,最好的方法是:

  1. 不要强制使用者将日志记录器注入到类中。只需创建另一个传递 NullLoggerFactory 的构造函数。

    class MyClass
    {
    private readonly ILoggerFactory _loggerFactory;
    
    
    public MyClass():this(NullLoggerFactory.Instance)
    {
    
    
    }
    public MyClass(ILoggerFactory loggerFactory)
    {
    this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
    }
    }
    
  2. 限制创建日志记录器时使用的类别数,以便使用者能够轻松地配置日志筛选。

    this._loggerFactory.CreateLogger(Consts.CategoryName)
    

默认的方法是 ILogger<T>。这意味着在日志中,来自特定类的日志将清晰可见,因为它们将包含完整的类名作为上下文。例如,如果您的类的全名是 MyLibrary.MyClass,那么您将在该类创建的日志条目中得到它。例如:

信息: 我的信息日志

如果要指定自己的上下文,应该使用 ILoggerFactory。例如,库中的所有日志都具有相同的日志上下文,而不是每个类。例如:

loggerFactory.CreateLogger("MyLibrary");

然后日志会是这样的:

我的图书馆: 信息: 我的信息日志

如果你在所有的类中都这样做,那么对于所有的类,上下文就只是 MyLibrary。我想如果您不想在日志中公开内部类结构,那么您可能希望对库这样做。

关于可选的日志记录。我认为你应该总是在构造函数中需要 ILogger 或者 iloggerFactory,让库的使用者来关闭它,或者提供一个 Logger,如果他们不想要日志记录的话,在依赖注入中什么都不做。对于配置中的特定上下文,很容易转换日志记录。例如:

{
"Logging": {
"LogLevel": {
"Default": "Warning",
"MyLibrary": "None"
}
}
}

回到这个问题上来,我认为 ILogger<T>是正确的选择,考虑到其他选择的负面影响:

  1. 注入 ILoggerFactory会迫使用户将可变全局日志记录器工厂的控制权交给类库。此外,通过接受 ILoggerFactory,您的类现在可以使用 CreateLogger方法写入任意类别名称的日志。虽然 ILoggerFactory通常作为 DI 容器中的单例可用,但我作为一个用户会怀疑为什么任何库都需要使用它。
  2. 虽然方法 ILoggerProvider.CreateLogger看起来像它,但它不是用于注入的。它与 ILoggerFactory.AddProvider一起使用,这样工厂就可以创建聚合的 ILogger,从而写入从每个已注册的提供程序创建的多个 ILogger。当你检查 LoggerFactory.CreateLogger的实施时,这一点很清楚
  3. 接受 ILogger看起来也是一种方式,但这是不可能的。NET 核心 DI。这实际上听起来像是他们需要首先提供 ILogger<T>的原因。

毕竟,我们没有比 ILogger<T>更好的选择,如果我们要从这些类中选择的话。

另一种方法是注入一些其他的东西来包装非通用的 ILogger,在这种情况下应该是非通用的。这个想法是,通过用您自己的类包装它,您可以完全控制用户如何配置它。

我希望保持简单,并注入非通用的 ILogger

这似乎是非默认行为,但很容易与以下内容联系起来:

services.AddTransient(s => s.GetRequiredService<ILoggerFactory>().CreateLogger(""));

这(向构造函数中注入 ILogger 并调用需要 ILogger 的基函数)之所以可行,是因为 ILogger<T>是协变的,而且只是一个对 LoggerFactory有依赖关系的包装器类。如果它不是协变的,你肯定会使用 ILoggerFactoryILogger。但是 ILogger应该被丢弃,因为您可以记录到任何类别,并且会失去所有关于记录的上下文。我认为 ILoggerFactory是最好的方法,然后使用 CreateLogger<T>()在你的类中创建一个 ILogger<T>。这样你就真的有了一个很好的解决方案,因为作为一个开发人员,你真的希望将类别与你的类对齐,直接跳转到代码,而不是一些不相关的派生类。(你可以加上行号。)您还可以让派生类使用由基类定义的日志记录器,然后还可以从哪里开始查找源代码?除此之外,我可以想象在同一个类中可能还有任何其他具有特殊用途类别(子类别)名称的 ILogger。没有什么阻止你有多个 ILogger 的在这种情况下 ILoggerFactory只是看起来更干净。

我的首选解决方案是注入 ILoggerFactory调用 CreatLogger<T>,其中 T是当前类,并将其分配给 private readonly ILogger<T> logger

或者,如果您已经注入了 IServiceProvider,您可以调用 serviceProvider.GetService<ILogger<T>>();

注意,注入 IServiceProvider是服务定位器模式,被认为是反模式。注入 ILoggerFactory也是服务定位器模式的一种变体。

注意: 服务提供者在日志记录器工厂不缓存的地方缓存日志记录器。如果不希望使用服务提供商,可以注入一个日志记录器管理器:

public interface ILoggerManager
{
public ILogger<T> CreateLogger<T>();
}


public class LoggerManager : ILoggerManager
{
private readonly IServiceProvider _serviceProvider;


public LoggerManager(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}


public ILogger<T> CreateLogger<T>()
{
return _serviceProvider.GetRequiredService<ILogger<T>>();
}
}

这里添加了,因为这里有很多其他的搜索结果链接。 如果你有一个 ILoggerFactory并且你需要提供一个 ILogger<Whatever>,这是创建它的方法: new Logger<Whatever>(myLoggerFactory)

当编写一个库时,ILoggerFactoryILoggerFactory<T>是最好的选择。

为什么?

作为图书馆的作者,你可能会关心:

  • 消息的内容
  • 消息的严重性
  • 消息的类别/类别/分组

你可能不在乎:

  • 使用者使用的日志库
  • 是否提供了日志库

当我写图书馆的时候:

我编写类的方式是,我控制消息的内容和严重性(有时是类别) ,同时允许使用者选择他们想要的任何日志实现,或者根本不选择。

例子

非泛型类

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;


public class MyClass
{
private readonly ILogger _logger;


public MyClass(
..., /* required deps */
..., /* other optional deps */
ILoggerFactory? loggerFactory)
{
_logger = loggerFactory?.CreateLogger<MyClass>()
?? NullLoggerFactory.Instance.CreateLogger<MyClass>();
}
}

通用类

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;


public class MyClass<T>
{
private readonly ILogger<T> _logger;


public MyClass<T>(
..., /* required deps */
..., /* other optional deps */
ILoggerFactory? loggerFactory)
{
_logger = loggerFactory?.CreateLogger<T>()
?? NullLoggerFactory.Instance.CreateLogger<T>();
}
}

现在你可以:

  • 使用完整的 MSILogger 界面来完成所有的日志记录工作,根本不关心是否真的有一个日志记录器
  • 如果需要控制类别,则用通用 CreateLogger<T>()替换非通用 CreateLogger("")

对于抱怨:

  • 是的,你可以在构造函数中使用 ILogger,或者 ILogger<T>,如果你不关心类别的话,但是我建议这是最通用的/通用的方法,它可以给你最多的选择而不会影响到消费者。
  • 使用者仍然可以使用日志工厂或其日志记录器实现的配置覆盖类别。
  • 接受日志工厂并不一定要初始化任何东西,这取决于 DI 容器配置/使用者
  • 在我的书中,空日志记录器不算作开销,因为我们使用的是单个实例
  • 如果需要,使用者可以传入 NullLoggerFactory
  • 如果您真的有些过头了,那么您可以设置一个库配置设置(通过对构造函数的修改)来启用/禁用库的日志记录(有条件地强制使用 NullLogger)

我使用这个简单的技术将 ILogger 注入到需要基本 ILogger 的遗留类中

services.AddSingleton<ILogger>(provider => provider.GetService<ILogger<Startup>>());