當我們利用LoggerFactory創建一個Logger對象并利用它來實現日志記錄,這個過程會產生一個日志消息,日志消息的流向取決于注冊到LoggerFactory之上的LoggerProvider。說的更加具體一點,日志消息的歸宿取決于注冊到LoggerFactory的LoggerProvider究竟會提供怎樣的Logger。微軟提供了一系列原生的LoggerProvider,我們先來認識一下將控制臺作為日志輸出目的地的ConsoleLoggerProvider。ConsoleLoggerProvider會提供一個名為ConsoleLogger的Logger對象,讓后者在進行日志寫入的時候會將格式化的日志消息輸出到當前控制臺上,這兩個類型(ConsoleLoggerProvider和ConsoleLogger)均定義在NuGet包“Microsoft.Extensions.Logging.Console”之中。
目錄
一、ConsoleLogger
二、ConsoleLogScope
三、ConsoleLoggerProvider
四、擴展方法AddConsole
一、ConsoleLogger
如下所示的代碼片段展示了由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所示的映射關系,可以看出日志等級在控制臺上均會顯示為僅包含四個字母的簡寫形式。日志等級也同時決定了改部分內容在控制臺上顯示的前景色。
二、ConsoleLogScope
在默認情況下針對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: =>{State}
3: {Message}
比如在一個處理訂購訂單的應用場景中,需要將針對同一筆訂單的多條日志消息關聯在一起,我們就可以針對訂單的ID創建一個日志上下文范圍,并在此上下文范圍內調用Logger對象的Log方法進行日志記錄,那么訂單ID將會包含在每條寫入的日志消息中。
1: ILogger logger = new ServiceCollection()
2: .AddLogging()
3: .BuildServiceProvider()
4: .GetService<ILoggerFactory>()
5: .AddConsole(true)
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創建的。這段程序執行之后會在控制臺上輸出如下所示的兩條日志消息。
三、ConsoleLoggerProvider
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的日志直接被忽略。
文章列表