.NET Core下的日誌(1):記錄日誌信息
記錄各種級別的日誌是所有應用不可或缺的功能。關於日誌記錄的實現,我們有太多第三方框架可供選擇,比如Log4Net、NLog、Loggr和Serilog 等,當然我們還可以選擇微軟原生的診斷機製(相關API定義在命名空間“System.Diagnostics”中)實現對日誌的記錄。.NET Core提供了獨立的日誌模型使我們可以采用統一的API來完成針對日誌記錄的編程,我們同時也可以利用其擴展點對這個模型進行定製,比如可以將上述這些成熟的日誌框架整合到我們的應用中。本係列文章旨在從設計和實現的角度對.NET Core提供的日誌模型進行深入剖析,不過在這之前我們必須對由它提供的日誌記錄編程模式具有一個大體的認識,接下來我們會采用實例的形式來演示如何相應等級的日誌並最終將其寫入到我們期望的目的地中。
目錄
一、日誌模型三要素
二、將日誌寫入不同的目的地
三、依賴注入
四、根據等級過濾日誌消息
五、利用TraceSource記錄日誌
直接利用TraceSource記錄追蹤日誌
利用TraceSourceLoggerProvider記錄追蹤日誌
日誌記錄編程主要會涉及到三個核心對象,它們分別是、和,這三個對象同時也是.NET Core日誌模型中的核心對象,並通過相應的接口(ILogger、ILoggerFactory和ILoggerProvider)來體現。右圖所示的UML揭示了日誌模型的這三個核心對象之間的關係。
在進行日誌記錄編程時,我們直接調用Logger對象相應的方法寫入日誌,LoggerFactory是創建Logger對象的工廠。由LoggerFactory創建的Logger並不真正實現對日誌的寫入操作,真正將日誌寫入相應目的地的Logger是通過相應的LoggerProvider提供的,前者是對後者的封裝,它將日誌記錄請求委托給後者來完成。
具體來說,在通過LoggerFactory創建Logger之前,我們會根據需求將一個或者多個LoggerProvider注冊到LoggerFactory之上。比如,如果我們需要將日誌記錄到EventLog中,我們會注冊一個EventLogLoggerProvider,後者會提供一個EventLogLogger對象來實現針對EventLog的日誌記錄。當我們利用LoggerFactory創建Logger對象時,它會利用注冊其上的所有LoggerProvider創建一組具有真正日誌寫入功能的Logger對象,並采用“組合(Composition)”模式利用這個Logger列表創建並返回一個Logger對象。
綜上所述,LoggerFactory創建的Logger僅僅是一個“殼”,在它內部封裝了一個或者多個具有真正日誌寫入功能的Logger對象。當我們調用前者實施日誌記錄操作時,它會遍曆被封裝的Logger對象列表,並委托它們將日誌寫入到相應的目的地。
接下來我們通過一個簡單的實例來演示如何將具有不同等級的日誌寫入兩種不同的目的地,其中一種是直接將格式化的日誌消息輸出到當前控製台,另一種則是將日誌寫入Debug輸出窗口(相當於直接調用Debug.WriteLine方法),針對這兩種日誌目的地的Logger分別通過ConsoleLoggerProvider和DebugLoggerProvider來提供。
我們創建一個空的.NET Core控製台應用,並在其project.json文件中添加如下三個NuGet包的依賴,其中默認使用的LoggerFactory和由它創建的Logger定義在“Microsoft.Extensions.Logging”之中,而上述的ConsoleLoggerProvider和DebugLoggerProvider則分別由其餘兩個NuGet包來提供。由於在默認情況下 ,.NET Core並不支持中文編碼,我們需要顯式注冊一個名為的針對相應的EncodingProvider,後者定義在NuGet包 “System.Text.Encoding.CodePages”之中,所以我們需要添加這個這NuGet包的依賴。
1: {
2:
3: "dependencies": {
4: ...
5: "Microsoft.Extensions.Logging" : "1.0.0-rc2-final",
6: "Microsoft.Extensions.Logging.Console" : "1.0.0-rc2-final",
7: "Microsoft.Extensions.Logging.Debug" : "1.0.0-rc2-final",
8:
9: "System.Text.Encoding.CodePages" : "4.0.1-rc2-24027"
10: },
11: ...
12: }
我們在入口的Main方法中編寫如下一段程序。我們首先創建一個LoggerFactory對象,並先後通過調用AddProvider方法在它上麵注冊一個ConsoleLoggerProvider對象和DebugLoggerProvider對象。創建它們調用的構造函數具有一個Func<string, LogLevel, bool>類型的參數旨在對日誌消息進行寫入前過濾(針對日子類型和等級),由於我們傳入的委托對象總是返回True,意味著提供的所有日誌均會被寫入。
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("App");
14:
15: int eventId = 3721;
16:
17: logger.LogInformation(eventId, "升級到最新版本({version})", "1.0.0.rc2");
18: logger.LogWarning(eventId, "並發量接近上限({maximum}) ", 200);
19: logger.LogError(eventId, "數據庫連接失敗(數據庫:{Database},用戶名:{User})", "TestDb", "sa");
20:
21: Console.Read();
22: }
23: }
我們通過指定日誌類型(“App”)調用LoggerFactory對象的CreateLogger方法創建一個Logger對象,並先後調用其LogInformation、LogWarning和LogError方法記錄三條日誌,這三個方法決定了寫入日誌的等級(Information、Warning和Error)。我們在調用這三個方法的時候指定了一個表示日誌記錄事件ID的整數(3721),以及具有占位符(“{version}”、“{maximum}”、“{Database}”和“{User}”)的消息模板和替換這些占位符的參數。
由於ConsoleLoggerProvider被事先注冊到創建Logger的LoggerFactory上,所以當我們執行這個實例程序之後,三條日誌消息會直接按照如下的形式打印到控製台上。我們可以看出格式化的日誌消息不僅僅包含我們指定的消息內容,日誌的等級、類型和事件ID同樣包含其中。
1: info: App[3721]
2: 升級到最新版本(1.0.0.rc2)
3: warn: App[3721]
4: 並發量接近上限(200)
5: fail: App[3721]
6: 數據庫連接失敗(數據庫:TestDb,用戶名:sa)
由於LoggerFactory上還注冊了另一個DebugLoggerProvider對象,由它創建的Logger會直接調用Debug.WriteLine方法寫入格式化的日誌消息。所以當我們以Debug模式編譯並執行該程序時,Visual
Studio的輸出窗口會以右圖所示的形式呈現出格式化的日誌消息。
上麵這個實例演示了日誌記錄采用的基本變成模式,即創建/獲取LoggerFactory並注冊相應的LoggerProvider,然後利用LoggerFactory創建Logger,並最終利用Logger記錄日誌。LoggerProvider的注冊除了可以直接調用LoggerFactory的AddProvider方法來完成之外,對於預定義的LoggerProvider,我們還可以調用相應的擴展方法來將它們注冊到指定的LoggerFactory上。比如在如下所示的代碼片斷中,我們直接調用針對ILoggerFactory接口的擴展方法和分別注冊一個ConsoleLoggerProvider和DebugLoggerProvider。
1: ILogger logger = new LoggerFactory()
2: .AddConsole()
3: .AddDebug()
4: .CreateLogger("App");
在我們演示的實例中,我們直接調用構造函數創建了一個LoggerFactory並利用它來創建用於記錄日誌的Logger,在一個.NET Core應用中,LoggerFactory會以依賴注入的方式注冊到ServiceProvider之中。如果我們需要采用依賴注入的方式來獲取注冊的LoggerFactory,我們需要在project.json文件中添加針對“Microsoft.Extensions.DependencyInjection”這個NuGet包的依賴。
1: {
2: "dependencies": {
3: ...
4: "Microsoft.Extensions.DependencyInjection" : "1.0.0-rc2-final",
5: "Microsoft.Extensions.Logging" : "1.0.0-rc2-final",
6: "Microsoft.Extensions.Logging.Console" : "1.0.0-rc2-final",
7: "Microsoft.Extensions.Logging.Debug" : "1.0.0-rc2-final",
8: },
9: ...
10: }
針對LoggerFactory的注冊可以通過調用針對IServiceCollection接口的擴展方法來完成。當我們調用這個方法的時候,它會創建一個對象並以模式注冊到指定的ServiceCollection之上。對於我們演示實例中使用的Logger對象,可以利用以依賴注入形式獲取的LoggerFactory來創建,如下所示的代碼片斷體現了這樣的編程方式。
1: ILogger logger = new ServiceCollection()
2: .AddLogging()
3: .BuildServiceProvider()
4: .GetService<ILoggerFactory>()
5: .AddConsole()
6: .AddDebug()
7: .CreateLogger("App");
對於通過某個LoggerProvider提供的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 =
2: (category, level) => level >= LogLevel.Warning;
3:
4: ILoggerFactory loggerFactory = new LoggerFactory();
5: loggerFactory.AddProvider(new ConsoleLoggerProvider(filter, false));
6: loggerFactory.AddProvider(new DebugLoggerProvider(filter));
7: ILogger logger = loggerFactory.CreateLogger("App");
針對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("App");
或者
1: ILogger logger = new ServiceCollection()
2: .AddLogging()
3: .BuildServiceProvider()
4: .GetService<ILoggerFactory>()
5: .AddConsole(LogLevel.Warning)
6: .AddDebug(LogLevel.Warning)
7: .CreateLogger("App");
由於注冊到LoggerFactory上的ConsoleLoggerProvider和DebugLoggerProvider都采用了上述的日誌過濾條件,所有由它們提供Logger都隻會寫入等級為Warning和Error的兩條日誌,至於等級為Information的那條則會自動忽略掉。所以我們的程序執行之後會在控製台上打印出如下所示的日誌消息。
1: warn: App[3721]
2: 並發量接近上限(200)
3: fail: App[3721]
4: 數據庫連接失敗(數據庫:TestDb,用戶名:sa)
從微軟推出第一個版本的.NET Framework的時候,就在“System.Diagnostics”命名空間中提供了Debug和Trace兩個類幫助我們完成針對調試和追蹤信息的日誌記錄。在.NET Framework 2.0種,增強的追蹤日誌功能實現在新引入的TraceSource類型中,並成為我們的首選。.NET Core的日誌模型借助TraceSourceLoggerProvider實現對TraceSource的整合。
.NET Core 中的TraceSource以及相關類型定義在NuGet包“System.Diagnostics.TraceSource”,如果我們需要直接使用TraceSource來記錄日誌,應用所在的Project.json文件中需要按照如下的方式添加針對這個NuGet包的依賴。
1: {
2: "dependencies": {
3: ...
4: "System.Diagnostics.TraceSource": "4.0.0-rc2-24027"
5: },
6: }
不論采用Debug和Trace還是TraceSource,追蹤日誌最終都是通過注冊的TraceListener被寫入相應的目的地。在“System.Diagnostics”命名空間中提供了若幹預定義的TraceListener,我們也可以自由地創建自定義的TraceListener。如下麵的代碼片斷所示,我們通過繼承抽象基類TraceListener自定義了一個ConsoleTranceListener類,它通過重寫的Write和WriteLine方法將格式化的追蹤消息輸出到當前控製台。
1: public class ConsoleTraceListener : TraceListener
2: {
3: public override void Write(string message)
4: {
5: Console.Write(message);
6: }
7:
8: public override void WriteLine(string message)
9: {
10: Console.WriteLine(message);
11: }
12: }
我們可以直接利用TraceSource記錄上麵實例演示的三條日誌。如下麵的代碼片斷所示,我們通過指定名稱(“App”)創建了一個TraceSource對象,然後在它的TraceListener列表中注冊了一個ConsoleTraceListener對象。我們為這個TraceSource指定了一個開關(一個SourceSwitch對象)讓它僅僅記錄等級高於Warning的追蹤日誌。我們調用TraceSource的TraceEvent方法實現針對不同等級(Information、Warning和Error)的三條追蹤日誌的記錄。
1: public class Program
2: {
3: public static void Main(string[] args)
4: {
5: TraceSource traceSource = new TraceSource("App");
6: traceSource.Listeners.Add(new ConsoleTraceListener());
7: traceSource.Switch = new SourceSwitch("LogWarningOrAbove", "Warning");
8:
9: int eventId = 3721;
10: traceSource.TraceEvent(TraceEventType.Information, eventId, "升級到最新版本({0})", "1.0.0.rc2");
11: traceSource.TraceEvent(TraceEventType.Warning, eventId, "並發量接近上限({0}) ", 200);
12: traceSource.TraceEvent(TraceEventType.Error, eventId, "數據庫連接失敗(數據庫:{0},用戶名:{1})", "TestDb", "sa");
13: }
14: }
當我們執行該程序之後,滿足TraceSource過濾條件的兩條追蹤日誌(即等級分別為Warning和Error的兩條追蹤日誌)將會通過注冊的ConsoleTraceListner寫入當前控製台,具體的內容如下所示。由於一個DefaultTraceListener對象會自動注冊到TraceSource之上,在它的Write或者WriteLine方法中會調用Win32函數OutputDebugString或者Debugger.Log方法,所以如果我們采用Debug模式編譯我們的程序,當程序運行後會在Visual Studio的輸出窗口中看到這兩條日誌消息。
1: App Warning: 3721 : 並發量接近上限(200)
2: App Error: 3721 : 數據庫連接失敗(數據庫:TestDb,用戶名:sa)
NET Core的日誌模型借助TraceSourceLoggerProvider實現對TraceSource的整合。具體來說,由於TraceSourceLoggerProvider提供的Logger對象實際上是對一個TraceSource的封裝,對於提供給Logger的日誌消息,後者會借助注冊到TraceSource上麵的TraceListener來完成對日誌消息的寫入工作。由於TraceSourceLoggerProvider定義在NuGet包“Microsoft.Extensions.Logging.TraceSource”,我們需要按照如下的方式將針對它的依賴定義在project.json中。
1: {
2: "dependencies": {
3: "Microsoft.Extensions.DependencyInjection" : "1.0.0-rc2-final",
4: "Microsoft.Extensions.Logging" : "1.0.0-rc2-final",
5: "Microsoft.Extensions.Logging.TraceSource" : "1.0.0-rc2-final"
6: },
7: ...
8: }
如果采用要利用日誌模型標準的編程方式來記錄日誌,我們可以按照如下的方式來創建對應的Logger對象。如下麵的代碼片斷所示,我們創建一個TraceSourceLoggerProvider對象並調用AddProvider方法將其注冊到LoggerFactory對象上。創建TraceSourceLoggerProvider的構造函數接受兩個參數,前者是一個SourceSwitch對象,用於過濾等級低於Warning的日誌消息,後者則是我們自定義的ConsoleTraceListener對象。
1: ILoggerFactory loggerFactory = new ServiceCollection()
2: .AddLogging()
3: .BuildServiceProvider()
4: .GetService<ILoggerFactory>();
5:
6: SourceSwitch sourceSwitcher = new SourceSwitch("LogWarningOrAbove", "Warning");
7: loggerFactory.AddProvider(new TraceSourceLoggerProvider(sourceSwitcher, new ConsoleTraceListener()));
8:
9: ILogger logger = loggerFactory.CreateLogger("App");
我們可以調用針對ILoggerFactory的擴展方法AddTraceSource來實現對TraceSourceLoggerProvider的注冊,該方法具有與TraceSourceLoggerProvider構造函數相同的參數列表。如下所示的代碼片斷通過調用這個擴展方法以更加精簡的方式創建了日誌記錄所需的Logger對象。
1: ILogger logger = new ServiceCollection()
2: .AddLogging()
3: .BuildServiceProvider()
4: .GetService<ILoggerFactory>()
5: .AddTraceSource(new SourceSwitch("LogWarningOrAbove", "Warning"), new ConsoleTraceListener())
6: .CreateLogger("App");
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-25 14:04:24