1. 程式人生 > >.NET Core的日誌[1]:採用統一的模式記錄日誌

.NET Core的日誌[1]:採用統一的模式記錄日誌

記錄各種級別的日誌是所有應用不可或缺的功能。關於日誌記錄的實現,我們有太多第三方框架可供選擇,比如Log4Net、NLog、Loggr和Serilog 等,當然我們還可以選擇微軟原生的診斷框架(相關API定義在名稱空間“System.Diagnostics”中)實現對日誌的記錄。.NET Core提供了獨立的日誌模型使我們可以採用統一的API來完成針對日誌記錄的程式設計,我們同時也可以利用其擴充套件點對這個模型進行定製,比如可以將上述這些成熟的日誌框架整合到我們的應用中。 [ 本文已經同步到《ASP.NET Core框架揭祕》之中]

目錄
一、日誌模型三要素
二、將日誌寫入不同的目的地
三、採用依賴注入程式設計模式建立Logger
四、根據等級過濾日誌訊息

一、日誌模型三要素

日誌記錄程式設計主要會涉及到三個核心物件,它們分別是Logger、LoggerFactory和LoggerProvider,這三個物件同時也是.NET Core日誌模型中的核心物件,並通過相應的介面(ILogger、ILoggerFactory和ILoggerProvider)來表示。對於日誌模型的這個三個核心物件之間具有如下圖所示的關係,我們不難看出,LoggerFactory和LoggerProvider都是Logger的建立者, 而Loggerrovider卻註冊到LoggerFactory。單單從這個簡單的描述來看,我想很多人會覺得這個三個物件之間的關係很“混亂”,混亂的關係主要體現在Logger具有兩個不同的建立者。

image

LoggerProvider和LoggerFactory建立的其實是不同的Logger。LoggerProvider建立的Logger提供真正的日誌寫入功能,即它的作用就是將提供的日誌訊息寫到對應的目的地(比如檔案、資料庫等)。LoggerFactory建立的實際上一個“組合式”的Logger,換句話說,這個Logger實際上是對一組Logger的封裝,它自身並不提供真正的日誌寫入功能,而是委託這組內部封裝的Logger來寫日誌。

一個LoggerFactory物件上可以註冊多個LoggerProvider物件。在進行日誌程式設計的時候,我們會利用LoggerFactory物件建立Logger來寫日誌,而這個Logger物件內部封裝的Logger則通過註冊到LoggerFactory上的這些LoggerProvider來提供。如果我們將上圖1所示的關係採用下圖的形式來表示,日日誌模型中這三個核心要素之間的關係就顯得很清楚了。

2

二、將日誌寫入不同的目的地

接下來我們通過一個簡單的例項來演示如何將具有不同等級的日誌寫入兩種不同的目的地,其中一種是直接將格式化的日誌訊息輸出到當前控制檯,另一種則是將日誌寫入Debug輸出視窗(相當於直接呼叫Debug.WriteLine方法),針對這兩種日誌目的地的Logger分別通過ConsoleLoggerProvider和DebugLoggerProvider這兩種不同的LoggerProvider來提供。

我們建立一個空的控制檯應用,並在其project.json檔案中新增如下四個NuGet包的依賴。其中預設使用的LoggerFactory和由它建立的Logger定義在“Microsoft.Extensions.Logging”這個NuGet包中。而上述的這兩個LoggerProvider型別(ConsoleLoggerProvider和DebugLoggerProvider)分別定義在其餘兩個NuGet包(“Microsoft.Extensions.Logging.Console”和“Microsoft.Extensions.Logging.Debug”)中。除此之外,由於.NET Core在預設情況下並不支援中文編碼,我們不得不程式啟動的時候顯式註冊一個支援中文編碼的EncodingProvider,後者定義在NuGet包 “System.Text.Encoding.CodePages”之中,所以我們需要新增這個這NuGet包的依賴。

   1: {
   2:   ...
   3:   "dependencies": {
   4:     ...
   5:     "Microsoft.Extensions.Logging"             : "1.0.0",
   6:     "Microsoft.Extensions.Logging.Console"     : "1.0.0",
   7:     "Microsoft.Extensions.Logging.Debug"       : "1.0.0",
   8:     "System.Text.Encoding.CodePages"           : "4.0.1"
   9:   },
  10:   

日誌記錄通過如下一段程式來完成。如下面的程式碼片段所示,我們首先建立一個LoggerFactory物件,並先後通過呼叫AddProvider方法將一個ConsoleLoggerProvider物件和一個DebugLoggerProvider物件註冊到它之上。建立這兩個LoggerProvider所呼叫的建構函式具有一個Func<string, LogLevel, bool>型別的引數,該委託物件的兩個輸入引數分別代表日誌訊息的型別和等級,布林型別的返回值決定了建立的Logger是否真的會寫入給定的日誌訊息。由於我們傳入的委託物件總是返回True,意味著提供的所有日誌均會被這兩個LoggerProvider建立的Logger物件寫入對應的目的地。

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         //註冊EncodingProvider實現對中文編碼的支援
   6:         Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
   7:  
   8:         Func<string, LogLevel, bool> filter = (category, level) => true;
   9:  
  10:         ILoggerFactory loggerFactory = new LoggerFactory();
  11:         loggerFactory.AddProvider(new ConsoleLoggerProvider(filter, false));
  12:         loggerFactory.AddProvider(new DebugLoggerProvider(filter));
  13:         ILogger logger = loggerFactory.CreateLogger(nameof(Program));
  14:  
  15:         int eventId = 3721;
  16:  
  17:         logger.LogInformation(eventId, "升級到最新.NET Core版本({version})", "1.0.0");
  18:         logger.LogWarning(eventId, "併發量接近上限({maximum}) ", 200);
  19:         logger.LogError(eventId, "資料庫連線失敗(資料庫:{Database},使用者名稱:{User})", "TestDb", "sa");
  20:  
  21:     }
  22: }

在完成針對LoggerProvider的註冊之後,我們通過指定日誌型別(“Program”)呼叫LoggerFactory物件的CreateLogger方法建立一個Logger物件,然後先後呼叫LogInformation、LogWarning和LogError這三個擴充套件方法記錄三條日誌訊息,這三個方法的命名決定了日誌的採用的等級(Information、Warning和Error)。我們在呼叫這三個方法的時候指定了一個表示日誌記錄事件ID的整數(3721),以及具有佔位符(“{version}”、“{maximum}”、“{Database}”和“{User}”)的訊息模板和替換這些佔位符的引數列表。

由於ConsoleLoggerProvider被註冊到建立Logger的LoggerFactory上,所以當我們執行這個例項程式之後,三條日誌訊息會直接按照如下的形式列印到控制檯上。我們可以看出格式化的日誌訊息不僅僅包含我們指定的訊息內容,日誌的等級、型別和事件ID同樣包含其中。不僅如此,表示日誌等級的文字還會採用不同的前景色和背景色來顯示。

3

由於LoggerFactory上還註冊了另一個DebugLoggerProvider物件,它建立的Logger會直接呼叫Debug.WriteLine方法寫入格式化的日誌訊息。所以當我們以Debug模式編譯並執行該程式時,Visual Studio的輸出視窗會以如下圖所示的形式呈現出格式化的日誌訊息。

4

上面這個例項演示了日誌記錄採用的基本程式設計模式:首先建立或者獲取一個LoggerFactory並根據需要註冊相應的LoggerProvider,然後利用LoggerFactory建立的Logger來記錄日誌。我們可以直接呼叫AddProvider方法將指定的LoggerProvider註冊到某個LoggerFactory物件上,除此之外,絕大部分LoggerFactory都具有相應的擴充套件方法使我們可以採用更加簡潔的程式碼來完成針對它們的註冊。比如在如下所示的程式碼片斷中,我們可以直接呼叫針對ILoggerFactory介面的擴充套件方法AddConsole和AddDebug分別完成針對ConsoleLoggerProvider和DebugLoggerProvider的註冊。

   1: ILogger logger = new LoggerFactory()
   2:     .AddConsole()
   3:     .AddDebug()
   4:     .CreateLogger(nameof(Program));

三、採用依賴注入程式設計模式建立Logger

在我們演示的例項中,我們直接呼叫建構函式建立了一個LoggerFactory並利用它來建立用於記錄日誌的Logger,但是在一個ASP.NET Core應用中,我們總是依賴注入的方式來獲取這個LoggerFactory物件。為了演示針對依賴注入的LoggerFactory獲取方式,我們首先需要作的是在project.json檔案中按照如下的方式新增針對“Microsoft.Extensions.DependencyInjection”這個NuGet包的依賴。

   1: {
   2:   "dependencies": {
   3:     ...
   4:     "Microsoft.Extensions.DependencyInjection"    : "1.0.0",
   5:     "Microsoft.Extensions.Logging"                : "1.0.0",
   6:     "Microsoft.Extensions.Logging.Console"        : "1.0.0",
   7:     "Microsoft.Extensions.Logging.Debug"          : "1.0.0",
   8:   },
   9:   ...
  10: }

所謂採用依賴注入的方式得到用於註冊LoggerProvider和建立Logger的LoggerFactory,本質上就是採用呼叫ServiceProvider的GetService方法得到這個物件。如果希望ServiceProvider能夠指定的型別(ILoggerFactory介面)得到我們所需的LoggerFactory,在這之前必須在建立ServiceProvider的ServiceCollection上作相應的服務註冊。針對LoggerFactory的註冊可以通過呼叫針對IServiceCollection介面的擴充套件方法AddLogging來完成。對於我們演示例項中使用的Logger物件,可以利用以依賴注入形式獲取的LoggerFactory來建立,如下所示的程式碼片斷體現了這樣的程式設計方式。

   1: ILogger logger = new ServiceCollection()
   2:     .AddLogging()
   3:     .BuildServiceProvider()
   4:     .GetService<ILoggerFactory>()
   5:     .AddConsole()
   6:     .AddDebug()
   7:     .CreateLogger(nameof(Program));

四、根據等級過濾日誌訊息

由於同一個LoggerFactory上可以註冊多個LoggerProvider,所以當我們利用LoggerFactory創建出相應的Logger用它來寫入某條日誌訊息的時候,這條訊息實際上會分發給由LoggerProvider提供的所有Logger。其實在很多情況下,我們並不希望每個Logger都去寫入分發給它的每條日誌訊息,而是希望Logger能夠“智慧”地忽略不應該由它寫入的日誌訊息。 每條日誌訊息都具有一個等級,針對日誌等級是我們普遍採用的日誌過濾策略。日誌等級通過具有如下定義的列舉LogLevel來表示,列舉項的值決定了等級的高低,值越大,等級越高;等級越高,越需要記錄。

   1: public enum LogLevel
   2: {
   3:     Trace           = 0,
   4:     Debug           = 1,
   5:     Information     = 2,
   6:     Warning         = 3,
   7:     Error           = 4,
   8:     Critical        = 5,
   9:     None            = 6
  10: }

在前面介紹ConsoleLoggerProvider和DebugLoggerProvider的時候,我們提到可以在呼叫建構函式時可以傳入一個Func<string, LogLevel, bool>型別的引數來指定日誌過濾條件。對於我們例項中寫入的三條日誌,它們的等級由低到高分別是Information、Warning和Error,如果我們選擇只寫入等級高於或等於Warning的日誌,可以採用如下的方式來建立對應的Logger。

   1: Func<string, LogLevel, bool> filter = (category, level) => level >= LogLevel.Warning;
   2:  
   3: ILoggerFactory loggerFactory = new LoggerFactory();
   4: loggerFactory.AddProvider(new ConsoleLoggerProvider(filter, false));
   5: loggerFactory.AddProvider(new DebugLoggerProvider(filter));
   6: ILogger logger = loggerFactory.CreateLogger(nameof(Program));

針對ILoggerFactory介面的擴充套件方法AddConsole和AddDebug同樣提供的相應的過載使我們可以通過傳入的Func<string, LogLevel, bool>型別的引數來提供日誌過濾條件。除此之外,我們還可以直接指定一個型別為LogLevel的引數來指定過濾日誌採用的最低等級。我們演示例項中的使用的Logger也可以按照如下兩種方式來建立。

   1: ILogger logger = new ServiceCollection()
   2:     .AddLogging()
   3:     .BuildServiceProvider()
   4:     .GetService<ILoggerFactory>()
   5:  
   6:     .AddConsole((c,l)=>l>= LogLevel.Warning)
   7:     .AddDebug((c, l) => l >= LogLevel.Warning)
   8:     .CreateLogger(nameof(Program));

或者

   1: ILogger logger = new ServiceCollection()
   2:     .AddLogging()
   3:     .BuildServiceProvider()
   4:     .GetService<ILoggerFactory>()
   5:     .AddConsole(LogLevel.Warning)
   6:     .AddDebug(LogLevel.Warning)
   7:     .CreateLogger(nameof(Program));

由於註冊到LoggerFactory上的ConsoleLoggerProvider和DebugLoggerProvider都採用了上述的日誌過濾條件,所有由它們提供Logger都只會寫入等級為Warning和Error的兩條日誌,等級為Information的那條則會自動忽略掉。所以我們的程式執行之後會在控制檯上打印出如下圖所示的日誌訊息。

5