簡體   English   中英

我應該服用 ILogger, ILogger<T> , ILoggerFactory 或 ILoggerProvider 用於庫?

[英]Should I take ILogger, ILogger<T>, ILoggerFactory or ILoggerProvider for a library?

這可能與將ILogger 或 ILoggerFactory 傳遞給 AspNet Core 中的構造函數有些相關? ,然而這只是關於庫設計,而不是關於使用這些庫的實際應用程序如何實現其日志記錄。

我正在編寫一個將通過 Nuget 安裝的 .net 標准 2.0 庫,並允許使用該庫的人獲取一些調試信息,我依賴Microsoft.Extensions.Logging.Abstractions來允許注入標准化的 Logger。

但是,我看到多個接口,並且網絡上的示例代碼有時使用ILoggerFactory並在類的 ctor 中創建一個記錄器。 還有ILoggerProvider看起來像 Factory 的只讀版本,但實現可能會或可能不會實現這兩個接口,所以我必須選擇。 (工廠似乎比提供者更常見)。

我見過的一些代碼使用非泛型ILogger接口,甚至可能共享同一個記錄器的一個實例,有些代碼在其 ctor 中采用ILogger<T>並期望 DI 容器支持開放泛型類型或每個顯式注冊以及我的庫使用的每個ILogger<T>變體。

現在,我確實認為ILogger<T>是正確的方法,也許是一個不接受該論點而只傳遞 Null Logger 的 ctor。 這樣,如果不需要記錄,則不使用任何記錄。 但是,一些 DI 容器會選擇最大的 ctor,因此無論如何都會失敗。

我很好奇我應該在這里做什么來給用戶帶來最少的頭痛,同時如果需要的話仍然允許適當的日志支持。

定義

我們有 3 個接口: ILoggerILoggerProviderILoggerFactory 讓我們看一下源代碼,找出它們的職責:

ILogger :負責寫入給定日志級別的日志消息。

ILoggerProvider :負責創建ILogger的實例(您不應該直接使用ILoggerProvider來創建記錄器)

ILoggerFactory :您可以向工廠注冊一個或多個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的集合,並將日志消息寫入所有這些。 查看 Logger 源代碼我們可以確認Logger有一個 ILogger 數組(即ILoggers LoggerInformation[] ),同時它正在實現ILogger接口。


依賴注入

MS 文檔提供了兩種注入記錄器的方法:

1、注廠:

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

使用 Category = TodoApi.Controllers.TodoController 創建一個 Logger

2. 注入一個通用的ILogger<T>

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

使用 Category = TodoController 的完全限定類型名稱創建一個記錄器


在我看來,使文檔令人困惑的是它沒有提到任何關於注入非泛型ILogger的內容。 在上面的同一個示例中,我們正在注入一個非泛型ITodoRepository ,但是它並沒有解釋為什么我們沒有對ILogger做同樣的事情。

根據馬克·西曼的說法:

注入構造函數應該只接收依賴項。

將工廠注入到Controller中並不是一個好方法,因為初始化Logger不是Controller的職責(違反SRP)。 同時注入一個通用的ILogger<T>會增加不必要的噪音。 有關詳細信息,請參閱Simple Injector 的博客: ASP.NET Core DI 抽象有什么問題?

應該注入(至少根據上面的文章)是一個非通用ILogger ,但是,這不是微軟的內置 DI 容器可以做的事情,您需要使用第 3 方 DI 庫。 兩個文檔解釋了如何將 3rd 方庫與 .NET Core 一起使用。


這是 Nikola Malovic 的另一篇文章,他在其中解釋了他的 5 條 IoC 定律。

尼古拉的國際奧委會第四定律

正在解析的類的每個構造函數除了接受一組自己的依賴項外,不應有任何實現。

除了ILoggerProvider之外,這些都是有效的。 ILoggerILogger<T>是您應該用於記錄的。 要獲得ILogger ,請使用ILoggerFactory ILogger<T>是獲取特定類別的記錄器的快捷方式(類型為類別的快捷方式)。

當您使用ILogger執行日志記錄時,每個注冊的ILoggerProvider都有機會處理該日志消息。 使用代碼直接調用ILoggerProvider並不真正有效。

ILogger<T>是為 DI 制作的實際版本。 ILogger<T>的出現是為了幫助更輕松地實現工廠模式,而不是您自己編寫所有 DI 和工廠邏輯,這是 ASP.NET Core 中最明智的決定之一

您可以選擇:

ILogger<T>如果您需要在代碼中使用工廠模式和 DI 模式,或者您可以使用ILogger來實現不需要 DI 的簡單日志記錄。

鑒於此, ILoggerProvider只是處理每個已注冊日志消息的橋梁。 無需使用它,因為它不會影響您應該干預代碼的任何內容。 它偵聽注冊的ILoggerProvider並處理消息。 就是這樣。

編寫庫時, 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>();
  }
}

現在你可以:

  • 使用完整的 MS ILogger 界面進行所有日志記錄,而不關心是否真的有記錄器
  • 如果您需要控制類別,請用通用CreateLogger<T>()替換非通用CreateLogger("")

對於抱怨:

  • 是的,如果您不關心類別,您可以在構造函數中使用ILoggerILogger<T> ,但我建議這是最通用/通用的方式,它為您提供了最多的選擇,而不會影響消費者.
  • 消費者仍然可以使用日志工廠的配置或其記錄器實現來覆蓋類別。
  • 您不必通過接受日志工廠來初始化任何東西,這取決於 DI 容器配置/消費者
  • 在我的書中,空記錄器不算作開銷,因為我們使用的是單個實例
  • 消費者可以傳入 NullLoggerFactory,如果他們願意
  • 如果你真的太過分了,你可以有一個庫配置設置(通過修改構造函數)將啟用/禁用庫的日志記錄(有條件地強制 NullLogger)

對於圖書館設計,好的方法是:

  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>是正確的選擇,考慮到其他選項的缺點:

  1. 注入ILoggerFactory強制您的用戶將可變全局記錄器工廠的控制權交給您的類庫。 此外,通過接受ILoggerFactory ,您的類現在可以使用CreateLogger方法使用任意類別名稱寫入日志。 雖然ILoggerFactory通常可用作 DI 容器中的單例,但作為用戶,我會懷疑為什么任何庫都需要使用它。
  2. 雖然ILoggerProvider.CreateLogger方法看起來很像,但它並非用於注入。 它與ILoggerFactory.AddProvider使用,因此工廠可以創建聚合ILogger ,以寫入從每個注冊的提供程序創建的多個ILogger 當您檢查LoggerFactory.CreateLogger的實現時,這一點很清楚
  3. 接受ILogger看起來也是可行的方法,但使用 .NET Core DI 是不可能的。 這實際上聽起來像是他們首先需要提供ILogger<T>的原因。

所以畢竟,如果我們要從這些類中進行選擇,我們沒有比ILogger<T>更好的選擇。

另一種方法是注入其他包裝非泛型ILogger的東西,在這種情況下應該是非泛型的。 這個想法是,通過用您自己的類包裝它,您可以完全控制用戶如何配置它。

默認方法是ILogger<T> 這意味着在日志中來自特定類的日志將清晰可見,因為它們將包含完整的類名作為上下文。 例如,如果您的類的全名是MyLibrary.MyClass ,您將在此類創建的日志條目中得到它。 例如:

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

如果要指定自己的上下文,則應使用ILoggerFactory 例如,您庫中的所有日志都具有相同的日志上下文,而不是每個類。 例如:

loggerFactory.CreateLogger("MyLibrary");

然后日志將如下所示:

MyLibrary:Information: 我的信息日志

如果您在所有類中都這樣做,那么上下文將只是所有類的 MyLibrary。 如果您不想在日志中公開內部類結構,我想您會希望為庫執行此操作。

關於可選的日志記錄。 我認為您應該始終在構造函數中要求 ILogger 或 ILoggerFactory 並將其留給庫的使用者將其關閉或提供一個在依賴注入中不執行任何操作的 Logger,如果他們不想記錄的話。 在配置中為特定上下文關閉日志記錄非常容易。 例如:

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

我寧願保持簡單並注入非通用 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)

我已經使用這種簡單的技術將 Ilogger 注入到需要基本 ILogger 的遺留類中

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM