1001
人物
.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)來表示。對於日誌模型的這個三個核心對象之間具有如下圖所示的關係,我們不難看出,, 而Loggerrovider卻注冊到LoggerFactory。單單從這個簡單的描述來看,我想很多人會覺得這個三個對象之間的關係很“混亂”,混亂的關係主要體現在Logger具有兩個不同的創建者。
LoggerProvider和LoggerFactory創建的其實是不同的Logger。,即它的作用就是將提供的日誌消息寫到對應的目的地(比如文件、數據庫等)。,換句話說,這個Logger實際上是對一組Logger的封裝,它自身並不提供真正的日誌寫入功能,而是委托這組內部封裝的Logger來寫日誌。
一個LoggerFactory對象上可以注冊多個LoggerProvider對象。在進行日誌編程的時候,我們會利用LoggerFactory對象創建Logger來寫日誌,而這個Logger對象內部封裝的Logger則通過注冊到LoggerFactory上的這些LoggerProvider來提供。如果我們將上圖1所示的關係采用下圖的形式來表示,日日誌模型中這三個核心要素之間的關係就顯得很清楚了。
接下來我們通過一個簡單的實例來演示如何將具有不同等級的日誌寫入兩種不同的目的地,其中一種是直接將格式化的日誌消息輸出到當前控製台,另一種則是將日誌寫入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同樣包含其中。不僅如此,表示日誌等級的文字還會采用不同的前景色和背景色來顯示。
由於LoggerFactory上還注冊了另一個DebugLoggerProvider對象,它創建的Logger會直接調用Debug.WriteLine方法寫入格式化的日誌消息。所以當我們以Debug模式編譯並執行該程序時,Visual Studio的輸出窗口會以如下圖所示的形式呈現出格式化的日誌消息。
上麵這個實例演示了日誌記錄采用的基本編程模式:首先創建或者獲取一個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));
在我們演示的實例中,我們直接調用構造函數創建了一個LoggerFactory並利用它來創建用於記錄日誌的Logger,但是在一個ASP.NET Core應用中,我們總是依賴注入的方式來獲取這個LoggerFactory對象。為了演示針對依賴注入的LoggerFactory獲取方式,我們首先需要作的是在project.json文件中按照如下的方式添加針對“Microsoft.Extensions.DependencyInjection”這個NuGet包的依賴。
1: {
2: "dependencies": {
3: ...
4: "" : "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的那條則會自動忽略掉。所以我們的程序執行之後會在控製台上打印出如下圖所示的日誌消息。
.NET Core的日誌[1]:采用統一的模式記錄日誌
.NET Core的日誌[2]:將日誌寫入控製台
.NET Core的日誌[3]:將日誌寫入Debug窗口
.NET Core的日誌[4]:利用EventLog寫日誌
.NET Core的日誌[5]:利用TraceSource寫日誌
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-25 11:05:48