《診斷跟蹤的幾種基本編程方式》介紹了四種常用的診斷日志框架。其實除了微軟提供的這些日志框架,還有很多第三方日志框架可供我們選擇,比如Log4Net、NLog和Serilog 等。雖然這些框架大都采用類似的設計,但是它們采用的編程模式具有很大的差異。為了對這些日志框架進行整合,微軟創建了一個用來提供統一的日志編程模式的日志框架。(本篇提供的實例已經匯總到《ASP.NET Core 6框架揭秘-實例演示版》)
[S801]將日志輸出到控制台和調試窗口(源代碼)
[S802]利用ILoggerFactory工廠創建Ilogger<T>對象(源代碼)
[S803]注入Ilogger<T>對象(源代碼)
[S804]TraceSource和EventSource的日志輸出(源代碼)
[S805]針對等級的日志過濾(源代碼)
[S806]針對等級和類別的日志過濾(源代碼)
[S807]針對等級、類別和ILoggerProvider類型的日志過濾(源代碼)
[S801]將日志輸出到控制台和調試窗口
我們通過一個簡單的實例來演示如何將具有不同等級的日志消息輸出到當前控制台和Visual Studio的調試窗口。如下所示的兩個NuGet包提供了針對這兩種日志輸出渠道的支持,所以演示程序需要添加針對它們的引用。
- Microsoft.Extensions.Logging.Console
- Microsoft.Extensions.Logging.Debug
應用程序一般使用ILoggerFacotry工廠創建的ILogger對象來記錄日志,下面的演示實例利用依賴注入容器來提供ILoggerFactory對象。如代碼片段所示,我們創建了一個ServiceCollection對象,並調用AddLogging擴展方法注冊了與日志相關的核心服務,作為依賴注入容器的IServiceProvider對象被構建出來后,我們從中提取出ILoggerFactory對象。
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var logger = new ServiceCollection() .AddLogging(builder => builder .AddConsole() .AddDebug()) .BuildServiceProvider() .GetRequiredService<ILoggerFactory>() .CreateLogger("Program"); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, level => logger.Log(level, eventId++, "This is a/an {0} log message.", level)); Console.Read();
在調用AddLogging擴展方法時,我們利用提供的Action<ILoggingBuilder>委托完成了針對ConsoleLoggerProvider和DebugLoggerProvider的注冊。具體來說,前者由ILoggingBuilder接口的AddConsole擴展方法注冊,后者則由AddDebug擴展方法進行注冊。我們通過指定日志類別(“Program”)調用ILoggerFactory接口的CreateLogger方法將對應的ILogger對象創建出來。每個ILogger對象都對應一個確定的類別,我們傾向於將當前寫入日志的組件、服務或者類型名稱作為日志類別,所以需要指定的是當前類型的名稱“Program”。
我們通過調用ILogger的Log方法針對每個有效的日志等級分發了六個日志事件,事件的ID分別被設置成1~6的整數。我們在調用Log方法時通過指定一個包含占位符({0})的消息模板和對應參數的方式來格式化最終輸出的消息內容。程序啟動后,相應的日志會以圖1示的形式同時輸出到控制台和Visual Studio的調試窗口。
[S802]利用ILoggerFactory工廠創建Ilogger<T>對象
在前面演示的實例中,我們將字符串形式表示的日志類別“Program”作為參數調用ILoggerFactory工廠的CreateLogger方法來創建對應的ILogger對象,實際上我們還可以調用泛型的CreateLogger<T>方法創建一個ILogger<T>對象來完成相同的工作。如果調用這個方法,我們就不需要額外提供日志類別,因為日志類別會根據泛型參數類型T自動解析出來。在如下的代碼片段中,我們調用了ILoggerFactory工廠的CreateLogger<Program>方法將對應的 ILogger<Program>對象創建出來。作為日志負載內容的消息模板除了可以采用{0},{1},...,{n}這樣的占位符,還可以使用任意字符串(“{level}”)來表示。啟動改寫的程序之后,輸出到控制台和調試輸出窗口的內容與圖1完全一致的。
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var logger = new ServiceCollection() .AddLogging(builder => builder .AddConsole() .AddDebug()) .BuildServiceProvider() .GetRequiredService<ILoggerFactory>() .CreateLogger<Program>(); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, level => logger.Log(level, eventId++, "This is a/an {level} log message.", level)); Console.Read();
[S803]注入Ilogger<T>對象
除了利用ILoggerFactory工廠來創建泛型的ILogger<Program>對象之外,我們還具有更簡潔的方式,那就是按照如下的方式直接利用IServiceProvider對象來提供這個ILogger<Program>對象。換句話說,ILogger<T>實際上是可以作為依賴服務注入到消費它的類型中。
...
var logger = new ServiceCollection()
.AddLogging(builder => builder
.AddConsole()
.AddDebug())
.BuildServiceProvider()
.GetRequiredService<ILogger<Program>>();
...
[S804]TraceSource和EventSource的日志輸出
除了控制台和調試器這兩種輸出渠道,日志框架還提供針對其他輸出渠道的支持。第7章重點介紹了針對TraceSource和EventSource的日志框架也是默認支持的兩種輸出渠道。針對這兩種輸出渠道的整合式由如下兩個NuGet包提供的。
- Microsoft.Extensions.Logging.TraceSource
- Microsoft.Extensions.Logging.EventSource
在添加了上述兩個NuGet包的引用之后,我們對演示實例作了如下的修改。為了捕捉由EventSource分發的日志事件,我們自定義了一個FoobarEventListener類型。我們在應用啟動的時候創建了這個FoobarEventListener對象並分別注冊了它的EventSourceCreated和EventWritten事件。一個名為“Microsoft-Extensions-Logging”的EventSource會幫助我們完成日志的輸出,所以EventSourceCreated事件的處理程序專門訂閱了這個EventSource。
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System.Diagnostics; using System.Diagnostics.Tracing; var listener = new FoobarEventListener(); listener.EventSourceCreated += (sender, args) => { if (args.EventSource?.Name == "Microsoft-Extensions-Logging") { listener.EnableEvents(args.EventSource, EventLevel.LogAlways); } }; listener.EventWritten += (sender, args) => { var payload = args.Payload; var payloadNames = args.PayloadNames; if (args.EventName == "FormattedMessage" && payload != null && payloadNames !=null) { var indexOfLevel = payloadNames.IndexOf("Level"); var indexOfCategory = payloadNames.IndexOf("LoggerName"); var indexOfEventId = payloadNames.IndexOf("EventId"); var indexOfMessage = payloadNames.IndexOf("FormattedMessage"); Console.WriteLine(@$"{(LogLevel)payload[indexOfLevel],-11}: { payload[indexOfCategory]}[{ payload[indexOfEventId]}]"); Console.WriteLine($"{"",-13}{payload[indexOfMessage]}"); } }; var logger = new ServiceCollection() .AddLogging(builder => builder .AddTraceSource(new SourceSwitch("default", "All"), new DefaultTraceListener { LogFileName = "trace.log" }) .AddEventSourceLogger()) .BuildServiceProvider() .GetRequiredService<ILogger<Program>(); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, level => logger.Log(level, eventId++, "This is a/an {level} log message.", level)); internal class FoobarEventListener : EventListener { }
上述的EventSource對象在進行日志分發的時候,它會采用不同的方式對將日志消息進行格式化,最終將格式化后的內容作為荷載內容的一部分通過多個事件分發出去,EventWritten事件處理程序選擇的是一個名為FormattedMessage的事件,它會將包括格式化日志消息在內的內容荷載信息輸出到控制台上。
基於TraceSource和EventSource日志框架的輸出渠道是調用ILoggingBuilder的AddTraceSource和AddEventSourceLogger擴展方法進行注冊的。針對AddTraceSource擴展方法的調用提供了兩個參數,前者是作為全局過濾器的SourceSwitch對象,后者則是注冊的DefaultTraceListener對象。由於我們為注冊的DefaultTraceListener指定了日志文件的路徑,所以輸出的日志消息最終會被寫入指定的文件中。程序運行后,日志消息會以如圖2示的形式同時輸出到控制台和指定的日志文件中(trace.log)。
圖2 對TraceSource和EventSource的日志輸出
[S805]針對等級的日志過濾
對於使用ILogger或者ILogger<T>對象分發的日志事件,並不能保證都會進入最終的輸出渠道,因為注冊的ILoggerProvider對象會對日志進行過濾,只有符合過濾條件的日志消息才會被真正地輸出到對應的渠道。每一個分發的日志事件都具有一個確定的等級。一般來說,日志消息的等級越高,表明對應的日志事件越重要或者反映的問題越嚴重,自然就越應該被記錄下來,所以在很多情況下我們指定的過濾條件只需要一個最低等級,所有不低於(等於或者高於)該等級的日志都會被記錄下來。最低日志等級在默認情況下被設置為Information,這就是前面演示實例中等級為Trace和Debug的兩條日志沒有被真正輸出的原因。如果需要將這個作為輸出“門檻”的日志等級設置得更高或者更低,我們只需要將指定的等級作為參數調用ILoggingBuilder接口的SetMinimumLevel方法即可。
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var logger = new ServiceCollection().AddLogging(builder => builder .SetMinimumLevel(LogLevel.Trace) .AddConsole()) .BuildServiceProvider() .GetRequiredService<ILogger<Program>>(); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, level => logger.Log(level, eventId++, "This is a/an {level} log message.", level)); Console.Read();
如上面的代碼片段所示,在調用AddLogging擴展方法時,我們調用ILoggingBuilder接口的SetMinimumLevel方法將最低日志等級設置為Trace。由於設置的是最低等級,所以所有的日志消息都會以圖3示的形式輸出到控制台上。
[S806]針對等級和類別的日志過濾
雖然“過濾不低於指定等級的日志消息”是常用的日志過濾規則,但過濾規則的靈活度並不限於此,很多時候還會同時考慮日志的類別。在創建對應ILogger時,由於一般將當前組件、服務或者類型的名稱作為日志類別,所以日志類別基本上體現了日志消息來源。如果我們只希望輸出由某個組件或者服務發出的日志事件,就需要針對類別對日志事件實施過濾。綜上可知,日志過濾條件其實可以通過一個類型為Func<string, LogLevel, bool>的委托對象來表示,它的兩個輸入參數分別代表日志事件的類別和等級。下面通過提供這樣一個委托對象對日志消息做更細粒度的過濾,所以需要對演示程序做如下修改。
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; var loggerFactory = new ServiceCollection() .AddLogging(builder => builder .AddFilter(Filter) .AddConsole()) .BuildServiceProvider() .GetRequiredService<ILoggerFactory>(); Log(loggerFactory, "Foo"); Log(loggerFactory, "Bar"); Log(loggerFactory, "Baz"); Console.Read(); static void Log(ILoggerFactory loggerFactory, string category) { var logger = loggerFactory.CreateLogger(category); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, level => logger.Log(level, eventId++, "This is a/an {0} log message.", level)); } static bool Filter(string category, LogLevel level) { return category switch { "Foo" => level >= LogLevel.Debug, "Bar" => level >= LogLevel.Warning, "Baz" => level >= LogLevel.None, _ => level >= LogLevel.Information, }; }
如上面的代碼片段所示,作為日志過濾器的Func<string, LogLevel, bool>對象定義的過濾規則如下:對於日志類別Foo和Bar,我們只會選擇輸出等級不低於Debug和Warning的日志;對於日志類別Baz,任何等級的日志事件都不會被選擇;至於其他日志類別,我們采用默認的最低等級Information。在執行AddLogging擴展方法時,我們調用ILoggerBuilder接口的AddFilter方法將Func<string, LogLevel, bool>對象注冊為全局過濾器。我們利用依賴注入容器提供的ILoggerFactory工廠創建了三個ILogger對象,它們采用的類別分別為“Foo”、“Bar”和“Baz”。我們最后利用這三個ILogger對象分發針對不同等級的六次日志事件,滿足過濾條件的日志消息會以圖4所示的形式輸出到控制台上。
[S807]針對等級、類別和ILoggerProvider類型的日志過濾
不論是通過調用ILoggerBuilder接口的SetMinimumLevel方法設置的最低日志等級,還是通過調用AddFilter擴展方法提供的過濾器,設置的日志過濾規則針對的都是所有注冊的ILoggerProvider對象,但是有時需要將過濾規則應用到某個具體的ILoggerProvider對象上。如果將ILoggerProvider對象引入日志過濾規則中,那么日志過濾器就應該表示成一個類型為Func<string, string, LogLevel, bool>的委托對象,該委托的三個輸入參數分別表示ILoggerProvider類型的全名、日志類別和等級。為了演示針對LoggerProvider的日志過濾,可以將演示程序做如下改動。
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Logging.Debug; var logger = new ServiceCollection() .AddLogging(builder => builder .AddFilter(Filter) .AddConsole() .AddDebug()) .BuildServiceProvider() .GetRequiredService<ILoggerFactory>() .CreateLogger("App.Program"); var levels = (LogLevel[])Enum.GetValues(typeof(LogLevel)); levels = levels.Where(it => it != LogLevel.None).ToArray(); var eventId = 1; Array.ForEach(levels, level => logger.Log(level, eventId++,"This is a/an {0} log message.", level)); Console.Read(); static bool Filter(string provider, string category, LogLevel level) => provider switch { var p when p == typeof(ConsoleLoggerProvider).FullName => level >= LogLevel.Debug, var p when p == typeof(DebugLoggerProvider).FullName => level >= LogLevel.Warning, _ => true, };
如上面的代碼片段所示,我們注冊的過濾器體現的過濾規則如下:ConsoleLoggerProvider,和DebugLoggerProvider的最低日志等級分別設置為Debug和Warning,至於其他的ILoggerProvider類型則不做任何的過濾。我們演示程序同時注冊了ConsoleLoggerProvider和DebugLoggerProvider,對於分發的12條日志消息,5條會在控制台上輸出,3條會出現在Visual Studio的調試輸出窗口中。