記錄各種級別的日志是所有應用不可或缺的功能。關于日志記錄的實現,我們有太多第三方框架可供選擇,比如Log4Net、NLog、Loggr和Serilog 等,當然我們還可以選擇微軟原生的診斷機制(相關API定義在命名空間“System.Diagnostics”中)實現對日志的記錄。.NET Core提供了獨立的日志模型使我們可以采用統一的API來完成針對日志記錄的編程,我們同時也可以利用其擴展點對這個模型進行定制,比如可以將上述這些成熟的日志框架整合到我們的應用中。本系列文章旨在從設計和實現的角度對.NET Core提供的日志模型進行深入剖析,不過在這之前我們必須對由它提供的日志記錄編程模式具有一個大體的認識,接下來我們會采用實例的形式來演示如何相應等級的日志并最終將其寫入到我們期望的目的地中。
目錄
一、日志模型三要素
二、將日志寫入不同的目的地
三、依賴注入
四、根據等級過濾日志消息
五、利用TraceSource記錄日志
直接利用TraceSource記錄追蹤日志
利用TraceSourceLoggerProvider記錄追蹤日志
一、日志模型三要素
日志記錄編程主要會涉及到三個核心對象,它們分別是Logger、LoggerFactory和LoggerProvider,這三個對象同時也是.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接口的擴展方法AddConsole和AddDebug分別注冊一個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接口的擴展方法AddLogging來完成。當我們調用這個方法的時候,它會創建一個LoggerFactory對象并以Singleton模式注冊到指定的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)
五、利用TraceSource記錄日志
從微軟推出第一個版本的.NET Framework的時候,就在“System.Diagnostics”命名空間中提供了Debug和Trace兩個類幫助我們完成針對調試和追蹤信息的日志記錄。在.NET Framework 2.0種,增強的追蹤日志功能實現在新引入的TraceSource類型中,并成為我們的首選。.NET Core的日志模型借助TraceSourceLoggerProvider實現對TraceSource的整合。
直接利用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)
利用TraceSourceLoggerProvider記錄追蹤日志
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");
文章列表