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

Posted

技术标签:

【中文标题】我应该将 ILogger、ILogger<T>、ILoggerFactory 或 ILoggerProvider 作为库吗?【英文标题】:Should I take ILogger, ILogger<T>, ILoggerFactory or ILoggerProvider for a library? 【发布时间】:2018-12-23 00:25:29 【问题描述】:

这可能与Pass ILogger or ILoggerFactory to constructors in AspNet Core? 有一定的关系,但是这只是关于库设计,而不是关于使用这些库的实际应用程序如何实现其日志记录。

我正在编写一个 .net 标准 2.0 库,它将通过 Nuget 安装,并允许使用该库的人获得一些调试信息,我依赖 Microsoft.Extensions.Logging.Abstractions 来允许注入标准化的 Logger。

但是,我看到多个接口,并且网络上的示例代码有时使用ILoggerFactory 并在类的 ctor 中创建一个记录器。还有ILoggerProvider,它看起来像Factory 的只读版本,但实现可能会或可能不会实现这两个接口,所以我必须选择。 (Factory 似乎比 Provider 更常见)。

我见过的一些代码使用非泛型 ILogger 接口,甚至可能共享同一个记录器的一个实例,有些代码在其 ctor 中使用 ILogger&lt;T&gt; 并期望 DI 容器支持开放的泛型类型或显式注册我的库使用的每个 ILogger&lt;T&gt; 变体。

现在,我确实认为 ILogger&lt;T&gt; 是正确的方法,也许是一个不接受该论点而只是传递 Null Logger 的 ctor。这样,如果不需要记录,则不使用任何记录。但是,一些 DI 容器会选择最大的 ctor,因此无论如何都会失败。

我很好奇我应该在这里做什么,以便为用户带来最少的麻烦,同时在需要时仍然允许适当的日志记录支持。

【问题讨论】:

微软似乎为Library design注入了ILoggerFactory...但是正如我在下面解释的那样,我不认为注入工厂是一个好的解决方案。 【参考方案1】:

定义

我们有 3 个接口:ILoggerILoggerProviderILoggerFactory。让我们看看source code找出他们的职责:

ILogger:负责写入给定Log Level的日志消息。

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

ILoggerFactory:您可以向工厂注册一个或多个ILoggerProviders,然后工厂使用它们来创建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

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


依赖注入

MS documentation 提供了 2 种注入 logger 的方法:

1.注入工厂:

public TodoController(ITodoRepository todoRepository, ILoggerFactory logger)

    _todoRepository = todoRepository;
    _logger = logger.CreateLogger("TodoApi.Controllers.TodoController");

创建一个 Category = TodoApi.Controllers.TodoController 的 Logger

2。注入泛型ILogger&lt;T&gt;

public TodoController(ITodoRepository todoRepository, ILogger<TodoController> logger)

    _todoRepository = todoRepository;
    _logger = logger;

使用 Category = TodoController 的完全限定类型名称创建一个记录器


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

根据Mark Seemann:

注入构造函数应该只接收 依赖关系。

将工厂注入Controller不是一个好方法,因为初始化Logger不是Controller的责任(违反SRP)。同时注入一个通用的ILogger&lt;T&gt; 会增加不必要的噪音。查看Simple Injector 的博客了解更多详情:What’s wrong with the ASP.NET Core DI abstraction?

应该注入(至少根据上面的文章)是非泛型ILogger,但是,这不是微软的内置 DI 容器可以做的事情,你需要使用 3rd 方 DI图书馆。 Thesetwo 文档解释了如何将 3rd 方库与 .NET Core 一起使用。


这是 Nikola Malovic 的 another article,他在其中解释了他的 5 条 IoC 定律。

尼古拉的国际奥委会第四定律

正在解析的类的每个构造函数都不应该有任何 除了接受一组自己的依赖项之外的实现。

【讨论】:

你的回答和史蒂文的文章是最正确的,同时也是最令人沮丧的。 如果未找到 AddConsole,请确保安装了 NuGet "Microsoft.Extensions.Logging.Console",您的源文件包含 using Microsoft.Extensions.Logging;。那最后试试ILoggerFactory factory = LoggerFactory.Create(builder =&gt; builder.AddConsole()); 如果未找到LoggerFileProvider,请使用 NuGet "NReco.Logging.File" 中的 FileLoggerProvider 抱歉,您声称哪篇文章,特别是文本的哪一部分解释了为什么注入 ILogger 是一件坏事?我同意一般不注入工厂,但为什么 ILogger 比 ILogger 差? T 通常是使用依赖项的消费类 在this question 上引用 Steven 的 cmets:“ILogger&lt;T&gt; 的注入对消费者来说只是噪音,当指定错误的 T 时可能导致意外错误,并且会使测试复杂化。”... “注入ILoggerFactoryILogger&lt;T&gt; 是一个糟糕的主意,在我看来,微软这样做(并公开推广)的唯一原因是因为他们的内置容器无法映射非通用接口到一个通用的实现。”【参考方案2】:

ILoggerProvider 外,这些都是有效的。 ILoggerILogger&lt;T&gt; 是您应该用于记录的。要获得ILogger,请使用ILoggerFactoryILogger&lt;T&gt; 是获取特定类别的记录器的快捷方式(类型为类别的快捷方式)。

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

【讨论】:

谢谢。这让我认为ILoggerFactory 是一种非常简单的方法,可以通过只有 1 个依赖项为消费者连接 DI(“只需给我一个工厂,我将创建自己的记录器” ),但会阻止使用现有的记录器(除非消费者使用一些包装器)。采用ILogger - 通用与否 - 允许消费者给我一个他们专门准备的记录器,但会使 DI 设置可能更加复杂(除非使用支持 Open Generics 的 DI 容器)。这听起来正确吗?那样的话,我想我会和工厂一起去。 @MichaelStum - 我不确定我是否遵循你的逻辑。您希望您的消费者使用 DI 系统,但又想接管并手动管理库中的依赖项?为什么这看起来是正确的方法? @Damien_The_Unbeliever 这是一个很好的观点。拿一家工厂似乎很奇怪。我认为与其采用ILogger&lt;T&gt;,不如采用ILogger&lt;MyClass&gt;,甚至只是ILogger——这样,用户只需一次注册即可将其连接起来,而无需在他们的DI中打开泛型容器。倾向于非通用的ILogger,但会在周末进行大量实验。【参考方案3】:

ILogger&lt;T&gt; 是真正为 DI 制作的。 ILogger&lt;T&gt; 的出现是为了帮助更轻松地实现工厂模式,而不是您自己编写所有 DI 和工厂逻辑,这是 ASP.NET Core 中最明智的决定之一

您可以选择:

ILogger&lt;T&gt; 如果您需要在代码中使用工厂模式和 DI 模式您可以使用 ILogger 来实现不需要 DI 的简单日志记录。

鉴于此,ILoggerProvider 只是处理每个已注册日志消息的桥梁。无需使用它,因为它不会影响您应该干预代码的任何内容。它监听注册的ILoggerProvider 并处理消息。就是这样。

【讨论】:

ILogger vs ILogger 与 DI 有什么关系?要么被注射,不是吗? 它实际上是 MS Docs 中的 ILogger。它派生自 ILogger,除了要记录的类名之外没有添加任何新功能。这也提供了一个独特的类型,因此 DI 注入正确命名的记录器。 Microsoft 扩展扩展了非通用 this ILogger【参考方案4】:

坚持这个问题,我认为ILogger&lt;T&gt; 是正确的选择,考虑到其他选项的缺点:

    注入ILoggerFactory 会强制您的用户将可变全局记录器工厂的控制权交给您的类库。此外,通过接受ILoggerFactory,您的班级现在可以使用CreateLogger 方法使用任意类别名称写入日志。虽然ILoggerFactory 通常在 DI 容器中作为单例提供,但作为用户,我会怀疑为什么任何库都需要使用它。 虽然ILoggerProvider.CreateLogger 方法看起来很像,但它并非用于注入。它与ILoggerFactory.AddProvider 一起使用,因此工厂可以创建聚合ILogger,写入从每个注册的提供者创建的多个ILogger。当您检查 the implementation of LoggerFactory.CreateLogger 时,这一点很清楚 接受ILogger 看起来也是可行的方法,但使用 .NET Core DI 是不可能的。这实际上听起来像是他们首先需要提供ILogger&lt;T&gt; 的原因。

所以毕竟,如果我们要从这些类中进行选择,我们没有比ILogger&lt;T&gt; 更好的选择。

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

【讨论】:

【参考方案5】:

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

    不要强迫消费者将记录器注入你的类。只需创建另一个传递 NullLoggerFactory 的构造函数。

    class MyClass
    
        private readonly ILoggerFactory _loggerFactory;
    
        public MyClass():this(NullLoggerFactory.Instance)
        
    
        
        public MyClass(ILoggerFactory loggerFactory)
        
          this._loggerFactory = loggerFactory ?? NullLoggerFactory.Instance;
        
    
    

    限制您在创建记录器时使用的类别数量,以便消费者轻松配置日志过滤。

    this._loggerFactory.CreateLogger(Consts.CategoryName)
    

【讨论】:

【参考方案6】:

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

MyLibrary.MyClass:Information: 我的信息日志

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

loggerFactory.CreateLogger("MyLibrary");

然后日志将如下所示:

MyLibrary:Information: 我的信息日志

如果您在所有类中都这样做,那么上下文将只是所有类的 MyLibrary。如果您不想在日志中公开内部类结构,我想您会希望为库执行此操作。

关于可选的日志记录。我认为您应该始终在构造函数中要求 ILogger 或 ILoggerFactory 并将其留给库的使用者将其关闭或提供一个 Logger,如果他们不想记录,则在依赖注入中不执行任何操作。在配置中为特定上下文关闭日志记录非常容易。例如:


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

【讨论】:

【参考方案7】:

编写库时,ILoggerFactoryILoggerFactory&lt;T&gt; 是要走的路。

为什么?

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

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

你可能不在乎:

消费者使用哪个日志库 是否提供了日志库

当我写库时:

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

示例

非泛型类

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>();
  

现在您可以:

使用完整的 MS ILogger 界面来执行所有日志记录,而不关心是否真的有记录器 如果您需要控制类别,请将通用 CreateLogger&lt;T&gt;() 替换为非通用 CreateLogger("")

对于抱怨:

是的,如果您不关心类别,您可以在构造函数中使用ILoggerILogger&lt;T&gt;,但我建议这是最通用/通用的方式,它为您提供了最多的选择,无需踩在消费者身上。 消费者仍然可以使用日志工厂的配置或其记录器实现来覆盖类别。 您不必通过接受日志工厂来初始化任何东西,这取决于 DI 容器配置/消费者 在我的书中,空记录器不算作开销,因为我们使用的是单个实例 如果消费者愿意,他们可以传入 NullLoggerFactory 如果你真的太过分了,你可以有一个库配置设置(通过修改构造函数)将启用/禁用库的日志记录(有条件地强制 NullLogger)

【讨论】:

关于那些注射的事情我完全不懂,但是有你的解释它有效!非常感谢。【参考方案8】:

我宁愿保持简单并注入非通用 ILogger

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

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

【讨论】:

为什么要给出这个建议?记录到“默认”记录器只会禁用您可以对类别执行的所有配置和过滤。 因为也许有人不关心这样的功能。花了 13 年没有过滤类别。此外,更高级的 DI 容器能够以上下文感知的方式解析ILogger,从而在运行时获得ILogger&lt;[Class-Where-I'm-Injected]&gt;,在大多数情况下,我更喜欢这种不太好的“自通用”样板。当然这只是我的看法。 @ZoltánTamási 但这正是 ILogger&lt;Class-Where-I'm-Injected&gt; 所得到的。【参考方案9】:

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

我首选的解决方案是注入ILoggerFactory call CreatLogger&lt;T&gt; 其中T 是当前类并将其分配给private readonly ILogger&lt;T&gt; logger

或者如果你已经注入了IServiceProvider,你可以调用serviceProvider.GetService&lt;ILogger&lt;T&gt;&gt;();

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

【讨论】:

【参考方案10】:

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

【讨论】:

是的,但是Logger 来自哪里?您必须选择一个日志库(即使它是 MS)才能访问该类。如果您已经有一个 ILoggerFactory,然后使用一些随机的 Logger 类,那么该 logger 类必须有一个接受 ILoggerFactory 的构造函数。这仅在您知道或控制正在使用的日志库(或者只是幸运)时才有效。 ILoggerFactory 有 .CreateLogger() 它实际上使用 ILoggerFactory 的 Logger 实现类,而您不必/关心知道它实际上是什么。 它来自Microsoft.Extensions.Logging,如原问题所述。 或者使用loggerFactory.CreateLogger();正是这样做的。 谢谢。但是,ILoggerFactory.CreateLogger 创建一个 ILogger 实例,而不是 ILogger&lt;T&gt;。如果您使用的是新代码,那可能没关系,但我想我是在尝试填充一些旧代码。如果有办法直接从工厂获得ILogger&lt;T&gt;,那就太好了。【参考方案11】:

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

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

【讨论】:

您的答案可以通过额外的支持信息得到改进。请edit 添加更多详细信息,例如引用或文档,以便其他人可以确认您的答案是正确的。你可以找到更多关于如何写好答案的信息in the help center。

以上是关于我应该将 ILogger、ILogger<T>、ILoggerFactory 或 ILoggerProvider 作为库吗?的主要内容,如果未能解决你的问题,请参考以下文章

ILogger 到 Application Insights

如何在没有 DI 的类库中使用 ILogger<Class>

Serilog 不会覆盖 ILogger 的最低级别

如何将 NLog 目标与 Microsoft ILogger 过滤器结合使用?

在 Azure 函数 V2 中将 TraceWriter 替换为 ILogger

如何在 ASP.NET Core 中使用 ILogger 进行单元测试