記錄各種級別的日誌是所有應用不可或缺的功能。關於日誌記錄的實現,我們有太多第三方框架可供選擇,比如Log4Net、NLog、Loggr和Serilog 等,當然我們還可以選擇微軟原生的診斷框架(相關API定義在命名空間“System.Diagnostics”中)實現對日誌的記錄。.NET Core提... ...
記錄各種級別的日誌是所有應用不可或缺的功能。關於日誌記錄的實現,我們有太多第三方框架可供選擇,比如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具有兩個不同的創建者。
LoggerProvider和LoggerFactory創建的其實是不同的Logger。LoggerProvider創建的Logger提供真正的日誌寫入功能,即它的作用就是將提供的日誌消息寫到對應的目的地(比如文件、資料庫等)。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));
三、採用依賴註入編程模式創建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的那條則會自動忽略掉。所以我們的程式執行之後會在控制臺上列印出如下圖所示的日誌消息。