.NET Core下的日誌(3):如何將日誌消息輸出到控製台上
當我們利用LoggerFactory創建一個Logger對象並利用它來實現日誌記錄,這個過程會產生一個日誌消息,日誌消息的流向取決於注冊到LoggerFactory之上的LoggerProvider。說的更加具體一點,日誌消息的歸宿取決於注冊到LoggerFactory的LoggerProvider究竟會提供怎樣的Logger。微軟提供了一係列原生的LoggerProvider,我們先來認識一下將控製台作為日誌輸出目的地的ConsoleLoggerProvider。ConsoleLoggerProvider會提供一個名為ConsoleLogger的Logger對象,讓後者在進行日誌寫入的時候會將格式化的日誌消息輸出到當前控製台上,這兩個類型(ConsoleLoggerProvider和ConsoleLogger)均定義在NuGet包“Microsoft.Extensions.Logging.Console”之中。
目錄
一、ConsoleLogger
二、ConsoleLogScope
三、ConsoleLoggerProvider
四、擴展方法AddConsole
如下所示的代碼片段展示了由ConsoleLoggerProvider提供的這個ConsoleLogger類型的定義。ConsoleLogger具有四個屬性,代表Logger名稱的Name屬性最初由ConsoleLoggerProvider提供,實際上就是LoggerFactory在創建Logger時指定的日誌類型。出於對跨平台的支持,ConsoleLogger對不同平台下控製台進行了抽象並使用接口IConsole來表示,所示代碼當前控製台的Console屬性的類型為IConsole。Func<string, LogLevel, bool>類型的Filter屬性提供了一個針對日誌類型與等級的過濾條件,是否真正需要將提供的日誌消息輸出到控製台就由這個過濾條件來決定。最後一個屬性IncludeScopes與上麵提到的關聯多次日誌記錄的上下文範圍有關,我們後續內容中對此進行單獨介紹。
1: public class ConsoleLogger : ILogger
2: {
3: public string Name { get; }
4: public IConsole Console { get; set; }
5: public Func<string, LogLevel, bool> Filter { get; set; }
6: public bool IncludeScopes { get; set; }
7:
8: public ConsoleLogger(string name, Func<string, LogLevel, bool> filter, bool includeScopes);public IDisposable BeginScope<TState>(TState state);
9:
10: public bool IsEnabled(LogLevel logLevel);
11: public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);
12: public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string message);
13: }
對於ConsoleLogger的這四個屬性,除了表示當前控製台的Console屬性,其餘三個均可以在創建它的時候通過構造函數的相應參數來指定。接下來我們來了解一下用於抽象不同平台控製台的IConsole接口,如下麵的代碼片段所示,IConsole接口具有如下三個方法。在調用Write和WriteLine方法寫入日誌的時候,我們除了指定寫入的消息文本之外,還可以控製消息在控製台上的背景和前景顏色。Flush方法與數據輸出緩衝機製有關,如果采用緩衝機製,通過Write或者WriteLine方法寫入的消息並不會立即輸出到控製台,而是先被保存到緩衝區,Flush方法被執行的時候會將緩衝區的所有日誌消息批量輸出到控製台上。
1: public interface IConsole
2: {
3: void Write(string message, ConsoleColor? background, ConsoleColor? foreground);
4: void WriteLine(string message, ConsoleColor? background, ConsoleColor? foreground);
5: void Flush();
6: }
微軟默認提供了兩種類型的Console類型,一種是基於Windows平台的WindowsLogConsole,非Windows平台的控製台則通過AnsiLogConsole來表示。它們之間的不同之處在於對日誌消息在控製台上顯示顏色(前景色和背景色)的控製。對於Windows平台來說,消息顯示在控製台顏色是通過顯式設置System.Console的靜態屬性ForegroundColor和BackgroundColor來實現的,但是對於非Windows平台來說,顏色信息會直接以基於ASNI標準的轉意字符序列(ANSI Esacpe Sequences)的形式內嵌在消息文本之中)。
ConsoleLogger的IsEnabled方法最終決定了是否需要真正完成對提供日誌的寫入操作,這方法是由Filter屬性返回的委托對象的執行結果。當Log方法執行的時候,它會先調用IsEnabled方法,如果這個方法返回True,它調用另一個WriteMessage方法將提供的日誌消息輸出到由Console屬性表示的控製台上。WriteMessage方法是一個虛方法,如果它輸出的消息格式和樣式不滿足我們的要求,我們可以定義ConsoleLogger的子類,並通過重寫這個方法按照我們希望的方式輸出日誌消息。
1: {LogLevel} : {Category}[{EventId}]
2: {Message}
在默認情況下,被ConsoleLogger輸出到控製台上的日誌消息會采用上麵的格式,這也可以通過我們在上麵演示的實例來印證。對於輸出到控製台表示日誌等級的部分,輸出的文字與對應的日誌等級具有如表1所示的映射關係,可以看出日誌等級在控製台上均會顯示為僅包含四個字母的簡寫形式。日誌等級也同時決定了改部分內容在控製台上顯示的前景色。
在默認情況下針對Log方法的每次調用都是一次獨立的日誌記錄行為,但是在很多情況下多次相關的日誌記錄需要在同一個上下文範圍中進行,我們可以通過調用Logger的BeginScope方法來創建這個上下文範圍。對於ConsoleLogger來說,它的BeginScope方法創建的上下文範圍與一個具有如下定義的ConsoleLogScope類有關。
1: public class ConsoleLogScope
2: {
3: internal ConsoleLogScope(string name, object state);
4: public static IDisposable Push(string name, object state);
5: public override string ToString();
6:
7: public static ConsoleLogScope Current { get; set; }
8: public ConsoleLogScope Parent { get; set; }
9: }
我們說ConsoleLogger的BeginScope方法返回的日誌上下文範圍與ConsoleLogScope有關,但並沒有說該方法返回的是一個ConsoleLogScope對象,關於這一點從上麵給出的ConsoleLogScope類型定義也可以看出來,BeginScope方法返回類型為IDisposable接口,但是ConsoleLogScope並未實現該接口。如上麵的代碼片段所示,ConsoleLogScope隻定義了一個內部構造函數,所以我們不可以直接調用構造函數創建一個ConsoleLogScope對象,ConsoleLogScope的創建實現在它的靜態方法Push中,ConsoleLogger的BeginScope方法的返回值其實就是針對這方法的調用結果。
要了解實現在Push方法中針對ConsoleLogScope的創建邏輯,需要先來了解一下ConsoleLogScope的嵌套層次結構。一個ConsoleLogScope可以內嵌於另一個ConsoleLogScope之中,後者被稱為前者的“父親”,它的Parent屬性返回的就是這麼一個對象。ConsoleLogScope的靜態屬性Current表示當前的ConsoleLogScope,當我們通過指定name和state這兩個參數調用靜態方法Push時,該方法實際上會調用靜態構造函數創建一個新的ConsoleLogScope對象並將其作為當前ConsoleLogScope的“兒子”。於此同時,當前ConsoleLogScope被切換成這個新創建的ConsoleLogScope。
ConsoleLogScope的Push方法最終返回的是一個DisposableScope對象。如下麵的代碼片段所示,DisposableScope僅僅是內嵌於ConsoleLogScope的一個私有類型。當它的Dispose方法執行的時候,它僅僅是獲取當前ConsoleLogScope的“父親”,並將後者作為當前ConsoleLogScope。
1: public class ConsoleLogScope
2: {
3: public static IDisposable Push(string name, object state)
4: {
5: ConsoleLogScope current = Current;
6: Current = new ConsoleLogScope(name, state);
7: Current.Parent = current;
8: return new DisposableScope();
9: }
10:
11: private class DisposableScope : IDisposable
12: {
13: public void Dispose()
14: {
15: ConsoleLogScope.Current = ConsoleLogScope.Current.Parent;
16: }
17: }
18: }
簡單地說,我們調用ConsoleLogScope的Push方法創建當前日誌上下文範圍並返回一個DisposableScope對象,後者的Dispose方法的調用意味著這個上下文範圍的終結。與此同時,原來的ConsoleLogScope從新成為當前的上下文範圍。下麵的代碼片段體現了基於ConsoleLogScope的作用域控製方法,這段代碼來體現另一個細節,那就是ConsoleLogScope的ToString方法被重寫,它返回的是ConsoleLogScope對象被創建時指定的State對象(state參數)的字符串形式(調用ToString方法的返回值)。
1: using (ConsoleLogScope.Push("App", "Scope1"))
2: {
3: Debug.Assert("Scope1" == ConsoleLogScope.Current.ToString());
4: using (ConsoleLogScope.Push("App", "Scope1"))
5: {
6: Debug.Assert("Scope2" == ConsoleLogScope.Current.ToString());
7: }
8: Debug.Assert("Scope1" == ConsoleLogScope.Current.ToString());
9: }
當ConsoleLogger的BeginScope方法被執行的時候,它會將自己的名稱(Name屬性)和指定的State對象作為參數調用ConsoleLogScope的靜態方法Push。隻要我們沒有調用返回對象的Dispose方法,就可以表示當前日誌上下文範圍的ConsoleLogScope對象,這個對象和我們指定的State對象的ToString方法返回相同的字符串。
1: public class ConsoleLogger : ILogger
2: {
3: public IDisposable BeginScope<TState>(TState state)
4: {
5: return ConsoleLogScope.Push(this.Name, state);
6: }
7: }
如果一個ConsoleLogger對象的IncludeScopes屬性返回True,意味著我們希望針對它的日誌記錄會在一個預先創建的日誌上下文範圍中執行執行,輸出到控製台的日誌消息會包含當前上下文範圍的信息。在次情況下,ConsoleLogger會采用如下的格式呈現輸出在控製台上的日誌消息,其中{State}表示調用BeginScope方法傳入的State對象。
1: {LogLevel} : {Category}[{EventId}]
2:
3: {Message}
比如在一個處理訂購訂單的應用場景中,需要將針對同一筆訂單的多條日誌消息關聯在一起,我們就可以針對訂單的ID創建一個日誌上下文範圍,並在此上下文範圍內調用Logger對象的Log方法進行日誌記錄,那麼訂單ID將會包含在每條寫入的日誌消息中。
1: ILogger logger = new ServiceCollection()
2: .AddLogging()
3: .BuildServiceProvider()
4: .GetService<ILoggerFactory>()
5: .AddConsole()
6: .CreateLogger("App");
7:
8: using (logger.BeginScope("訂單: {ID}", "20160520001"))
9: {
10: logger.LogWarning("商品庫存不足(商品ID: {0}, 當前庫存:{1}, 訂購數量:{2})", "9787121237812",20, 50);
11: logger.LogError("商品ID錄入錯誤(商品ID: {0})","9787121235368");
12: }
如上麵的代碼片段所示,我們按照依賴注入的方式創建了一個注冊有ConsoleLoggerProvider的LoggerFactory,並利用創建了一個Logger對象。在調用注冊ConsoleLoggerProvider的AddConsole方法時,我們傳入True作為參數,意味著提供的ConsoleLogger會在當前的日誌上下文範圍中進行日誌記錄(它 的IncludeScope屬性被設置為True)。我們通過Logger對象記錄了兩條針對同一筆訂單的日誌,兩次日誌記錄所在的上下文範圍是調用BeginScope方法根據指定 的訂單ID創建的。這段程序執行之後會在控製台上輸出如下所示的兩條日誌消息。
ConsoleLogger最終通過注冊到LoggerFactory上的ConsoleLoggerProvider來提供。當我們在創建一個ConsoleLogger的時候,除了需要指定它的名稱之外,還需要指定一個進行日誌過濾的Func<string, LogLevel, bool>類型的委托對象和確定是否將日誌寫入操作納入當前上下文範圍的布爾值。由於這兩個對象最終都需要通過ConsoleLoggerProvider來提供,所以它具有對應的構造函數。
1: public class ConsoleLoggerProvider : ILoggerProvider, IDisposable
2: {
3: public ConsoleLoggerProvider(Func<string, LogLevel, bool> filter,bool includeScopes);
4: public ConsoleLoggerProvider(IConsoleLoggerSettings settings);
5:
6: public ILogger CreateLogger(string name);
7: public void Dispose();
8: }
ConsoleLoggerProvider還具有另一個構造函數重載,它接受一個IConsoleLoggerSettings接口的參數,該接口表示為創建的ConsoleLogger而指定的配置。配置的目的是為了指導ConsoleLoggerProvider創建正確的ConsoleLogger,所以它最終還是為了提供日誌寫入過濾條件和是否將日誌寫入操作納入當前上下文範圍的布爾值,前者體現為IConsoleLoggerSettings接口的TryGetSwitch方法,後者自然對應其IncludeScopes屬性。
1: public interface IConsoleLoggerSettings
2: {
3: bool IncludeScopes { get; }
4: IChangeToken ChangeToken { get; }
5:
6: IConsoleLoggerSettings Reload();
7: bool TryGetSwitch(string name, out LogLevel level);
8: }
由於配置數據具有不同的載體,或者具有不同來源,比如文件、數據庫和環境變量等,所以需要考慮應用於配置源的同步問題。IConsoleLoggerSettings的ChangeToken提供了一個向應用通知配置源發生改變的令牌,另一個Reload則在配置源發生改變時從新加載配置。
在NuGet包“Microsoft.Extensions.Logging.Console”中提供了兩個實現了IConsoleLoggerSettings接口的類型,其中一個是具有如下定義的ConsoleLoggerSettings。ConsoleLoggerSettings的實現方式非常簡單,它通過一個字典對象來保存日誌類型與最低等級(低於該等級的日誌將被ConsoleLogger忽略)之間的映射,並利用它來實現TryGetSwitch方法。由於配置原數據體現為一個內存變量,所以無需考慮配置的同步問題,所以ConsoleLoggerSettings的Reload方法的返回值是它自己,ChangeToken被定義成簡單的可讀寫的屬性。
1: public class ConsoleLoggerSettings : IConsoleLoggerSettings
2: {
3: public bool IncludeScopes { get; set; }
4: public IChangeToken ChangeToken { get; set; }
5: public IDictionary<string, LogLevel> Switches { get; set; } = new Dictionary<string, LogLevel>();
6:
7: public IConsoleLoggerSettings Reload() => this;
8: public bool TryGetSwitch(string name, out LogLevel level)=> Switches.TryGetValue(name, out level);
9: }
IConsoleLoggerSettings接口的另一個實現者ConfigurationConsoleLoggerSettings則直接采用真正的配置來提供創建ConsoleLogger使用的設置。如下麵的代碼片段所示,ConfigurationConsoleLoggerSettings的構造函數的唯一參數類型為IConfiguration接口,它的IncludeScopes屬性和TryGetSwitch方法的返回值都是利用這個Configuration對象承載的配置計算出來的。至於數據的同步,則直接借助配置模型自身的同步機製來實現。
1: public class ConfigurationConsoleLoggerSettings : IConsoleLoggerSettings
2: {
3: public bool IncludeScopes { get; }
4: public IChangeToken ChangeToken { get; }
5:
6: public ConfigurationConsoleLoggerSettings(IConfiguration configuration);
7:
8: public IConsoleLoggerSettings Reload();
9: public bool TryGetSwitch(string name, out LogLevel level);
10: }
如下所示的代碼片段以JSON格式定義了ConfigurationConsoleLoggerSettings期望的配置結構。我們可以看到這個配置和ConsoleLoggerSettings一樣,除了直接提供與日誌上下文範圍的IncludeScopes屬性之外,還定義一組日誌類型與最低等級直接的映射關係。對於這組映射關係中指定的某種類型的日誌,隻有在不低於設定的等級才會被ConsoleLogger輸出到控製台。
1: {
2: "includeScopes": true|false,
3: "logLevel":{
4: "Category1": "Debug",
5: "Category2": "Error",
6: …
7: }
8:
關於ConsoleLoggerProvider針對ConsoleLogger的創建,有一個細節值得一提。當我們調用它的CreateLogger方法的時候,ConsoleLoggerProvider並不總是直接創建一個新的ConsoleLogger對象。實際上它會對創建的ConsoleLogger根據其名稱進行緩存,如果後續調用CreateLogger方法時指定相同的名稱,緩存的ConsoleLogger對象會直接作為返回值。ConsoleLoggerProvider針對ConsoleLogger的緩存體現在如下所示的代碼片段中。
1: ConsoleLoggerProvider loggerProvider = new ConsoleLoggerProvider(new ConsoleLoggerSettings());
2: Debug.Assert(ReferenceEquals(loggerProvider.CreateLogger("App"), loggerProvider.CreateLogger("App")));
四、擴展方法AddConsole
針對ILoggerFactory接口的擴展方法AddConsole幫助我們根據提供的參數創建一個ConsoleLoggerProvider對象並將其注冊到指定的LoggerFactory之上。我們在前麵的使用了少數幾個AddConsole方法重載之外,實際上AddConsole方法還存在很多其他的重載。對於如下所示的這些AddConsole方法,它提供了不同類型的參數幫助我們創建ConsoleLoggerProvider對象。經過了上麵對ConsoleLoggerProvider的詳細介紹,相信大家對每個參數所代表的含義會有正確的理解。
1: public static class ConsoleLoggerExtensions
2: {
3: public static ILoggerFactory AddConsole(this ILoggerFactory factory);
4: public static ILoggerFactory AddConsole(this ILoggerFactory factory, IConfiguration configuration);
5: public static ILoggerFactory AddConsole(this ILoggerFactory factory, IConsoleLoggerSettings settings);
6: public static ILoggerFactory AddConsole(this ILoggerFactory factory, LogLevel minLevel);
7: public static ILoggerFactory AddConsole(this ILoggerFactory factory, bool includeScopes);
8: public static ILoggerFactory AddConsole(this ILoggerFactory factory, Func<string, LogLevel, bool> filter);
9: public static ILoggerFactory AddConsole(this ILoggerFactory factory, LogLevel minLevel, bool includeScopes);
10: public static ILoggerFactory AddConsole(this ILoggerFactory factory, Func<string, LogLevel, bool> filter, bool includeScopes);
11: }
接下來通過一個實例來演示通過指定一個Configuration對象來調用擴展方法AddConsole來創建並注冊ConsoleLoggerProvider。我們在一個.NET Core控製台應用的project.json文件中添加了針對如下幾個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.Configuration.Json" : "1.0.0-rc2-final",
8: "System.Text.Encoding.CodePages" : "4.0.1-rc2-24027"
9: }
10: }
我們將ConsoleLogger的相關配置按照如下的形式定義在一個JSON文件中,並將其命名為log.json。通過這個配置,我們要求創建的ConsoleLogger忽略當前的日誌上下文範圍,並為類型“App”的日誌設置的最低的等級“Warning”。
1: {
2: "IncludeScopes": false,
3: "LogLevel": {
4: "App": "Warning"
5: }
6: }
我們在作為入口的Main方法中編寫了下麵一段程序。我們通過加載上麵這個log.json文件創建了一個Configuration對象,並將其作為參數調用擴展方法AddConsole將創建的ConsoleLoggerProvider注冊到LoggerFactory上麵。我們利用LoggerFactory針對日誌類型“App”創建了一個Logger對象,並利用後者記錄三條日誌。
1: public class Program
2: {
3: public static void Main(string[] args)
4: {
5: Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
6:
7: IConfiguration settings = new ConfigurationBuilder()
8: .AddJsonFile("log.json")
9: .Build();
10:
11: ILogger logger = new ServiceCollection()
12: .AddLogging()
13: .BuildServiceProvider()
14: .GetService<ILoggerFactory>()
15: .AddConsole(settings)
16: .CreateLogger("App");
17:
18: int eventId = 3721;
19: logger.LogInformation(eventId, "升級到最新版本({version})", "1.0.0.rc2");
20: logger.LogWarning(eventId, "並發量接近上限({maximum}) ", 200);
21: logger.LogError(eventId, "數據庫連接失敗(數據庫:{Database},用戶名:{User})", "TestDb", "sa");
22: }
23: }
根據定義在配置文件中的日誌開關,隻有等級不低於Warning的日誌才會真正被ConsoleLogger輸出到控製台上,所以對於上麵程序中記錄的三條日誌,控製台上隻會按照如下的形式呈現出等級分別為Warning和Error的兩條,等級為Information的日誌直接被忽略。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-25 13:33:39