對于一個控制臺應用,比如采用控制臺應用作為宿主的ASP.NET Core應用,我們可以將記錄的日志直接輸出到控制臺上。針對控制臺的Logger是一個類型為ConsoleLogger的對象,ConsoleLogger對應的LoggerProvider類型為ConsoleLoggerProvider,這兩個類型都定義在 NuGet包“Microsoft.Extensions.Logging.Console”之中。 本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、ConsoleLogger
二、ConsoleLogScope
三、ConsoleLoggerProvider
四、擴展方法AddConsole
一、ConsoleLogger
如下所示的代碼片段展示了ConsoleLogger類型的定義。它具有四個屬性,代表Logger名稱的Name屬性最初由ConsoleLoggerProvider提供,實際上就是LoggerFactory在創建Logger時指定的日志類型(Category)。ConsoleLogger的Console屬性代表當前控制臺,它的類型為IConsole接口。之所以沒有直接采用System.Console向控制臺輸出格式化的日志消息,是因為需要提供跨平臺的支持,IConsole接口表示的就是這么一個與具體平臺無關的抽象化的控制臺。
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);
9: public IDisposable BeginScope<TState>(TState state);
10:
11: public bool IsEnabled(LogLevel logLevel);
12: public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);
13: public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string message);
14: }
ConsoleLogger的Filter屬性通過一個委托對象來過濾真正需要寫到控制臺的日志消息,該屬性的返回類型為Func<string, LogLevel, bool>,兩個輸入參數分別表示分發給它的日志消息的類型和等級,如果執行該委托對象返回False,日志消息將會被直接忽略。ConsoleLogger的IsEnabled方法會直接將指定日志等級作為參數(ConsoleLogger的Name屬性作為另一個參數)調用這個委托對象得到最終的返回結果。ConsoleLogger的IncludeScopes與上面介紹的日志上下文范圍有關,我們會在后續的部分對它進行單獨介紹。
對于ConsoleLogger的這四個屬性,除了表示當前控制臺的Console屬性,其余三個均可以在創建它的時候通過構造函數的相應參數來指定。接下來我們來簡單了解一下表示抽象化控制臺的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的Log方法被調用的時候,它會先將指定的日志等級作為參數調用IsEnabled方法。如果這個方法返回True,ConsoleLogger會調用WriteMessage方法將提供的日志消息輸出到由Console屬性表示的控制臺上。WriteMessage方法是一個虛方法,如果它輸出的消息格式和樣式不滿足我們的要求,我們可以定義ConsoleLogger的子類,并通過重寫這個方法按照我們希望的方式輸出日志消息。
1: {LogLevel} : {Category}[{EventId}]
2: {Message}
在默認情況下,被ConsoleLogger輸出到控制臺上的日志消息會采用上面的格式,這也可以通過我們在上面演示的實例來印證。對于輸出到控制臺表示日志等級的部分,輸出的文字與對應的日志等級具有如下表所示的映射關系,可以看出日志等級在控制臺上均會顯示為僅包含四個字母的簡寫形式。日志等級也同時決定了改部分內容在控制臺上顯示的前景色。
日志等級 |
顯示文字 |
前景顏色 |
背景顏色 |
Trace |
trce |
Gray |
Black |
Debug |
dbug |
Gray |
Black |
Information |
info |
DarkGreen |
Black |
Warning |
warn |
Yellow |
Black |
Error |
fail |
Red |
Black |
Critical |
crit |
White |
Red |
二、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對象,一旦我們調用這個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: }
再次將我們目光從ConsoleLogScope轉移到ConsoleLogger上面,當ConsoleLogger的BeginScope方法被調用的時候,它會將自己的名稱(Name屬性)和指定的State對象作為參數調用ConsoleLogScope的靜態方法Push并返回一個DisposableScope對象。只要我們沒有調用DisposableScope的Dispose方法,就可以通過調用ConsoleLogScope的靜態屬性Current得到當前日志上下文,它的ToString方法和指定的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屬性開啟這個特性。如果ConsoleLogger的Log方法是在某個日志上下文范圍中被調用,它會采用如下的格式輸出日志消息,其中{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("Ordering");
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對象。我們在調用擴展方法AddConsole方法注冊ConsoleLoggerProvider 的時候傳入True作為參數,意味著提供的ConsoleLogger會在當前的日志上下文范圍中進行日志記錄(它 的IncludeScope屬性被設置為True)。我們通過Logger對象記錄了兩條針對同一筆訂單的日志,兩次日志記錄所在的上下文范圍是調用BeginScope方法根據指定 的訂單ID創建的。這段程序執行之后會在控制臺上輸出如下所示的兩條日志消息。
1: warn: Ordering[0]
2: => Order ID: 20160520001
3: 商品庫存不足(商品ID: 9787121237812, 當前庫存:20, 訂購數量:50)
4: fail: Ordering[0]
5: => Order ID: 20160520001
6: 商品ID錄入錯誤(商品ID: 9787121235368)
三、ConsoleLoggerProvider
ConsoleLogger最終通過注冊到LoggerFactory上的ConsoleLoggerProvider來提供。當我們在創建一個ConsoleLogger的時候,除了需要指定它的名稱之外,還需要指定一個用于過濾日志的Func<string, LogLevel, bool>對象,以及用于確定是否將日志寫入操作納入當前上下文范圍的布爾值。這兩者最終都需要通過ConsoleLoggerProvider來提供,我們在調用構造函數創建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,所以它最終還是為了提供日志過濾條件和是否將日志寫入操作納入當前上下文范圍的布爾值,前者體現為TryGetSwitch方法,后者對應其IncludeScopes屬性。由于配置數據具有不同的載體,或者具有不同來源,比如文件、數據庫和環境變量等,所以需要考慮應用于配置源的同步問題。IConsoleLoggerSettings接口的ChangeToken屬性提供了一個向應用通知配置源發生改變的令牌,Reload則在配置源發生改變時從新加載配置。
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: }
在NuGet包“Microsoft.Extensions.Logging.Console”中提供了兩個實現了IConsoleLoggerSettings接口的類型,其中一個是具有如下定義的ConsoleLoggerSettings。ConsoleLoggerSettings的實現方式非常簡單,它通過一個字典對象來保存日志類型與最低等級之間的映射,并利用它來實現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則直接采用真正的配置。如下面的代碼片段所示,一個ConfigurationConsoleLoggerSettings對象實際上是對一個Configuration對象的封裝。它的IncludeScopes屬性和TryGetSwitch方法的返回值都來源于Configuration對象承載的配置。ConfigurationConsoleLoggerSettings的同步直接利用配置模型的同步機制來實現,具體來說它的ChangeToken屬性也是直接由這個Configuration對象提供(GetChangeToken方法返回的ChangeToken)。
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對象會直接作為返回值。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。我們將ConsoleLogger的相關配置按照如下的形式定義在一個JSON文件中,并將其命名為logging.json。通過這個配置,我們要求創建的ConsoleLogger忽略當前的日志上下文范圍,并為日志類型“App”設置的最低的等級“Warning”。
1: {
2: "IncludeScopes": false,
3: "LogLevel": {
4: "App": "Warning"
5: }
6: }
我們在project.json文件中添加了針對如下幾個NuGet包的依賴。為了在項目編譯時自動將配置文件logging.json拷貝到輸出目錄下,我們將這個配置文件名設置為配置項“buildOptions/copyToOutput”的值。
1: {
2: ...
3: "buildOptions": {
4: ...
5: "copyToOutput": "logging.json"
6: },
7:
8: "dependencies": {
9: "Microsoft.Extensions.DependencyInjection" : "1.0.0",
10: "Microsoft.Extensions.Logging" : "1.0.0",
11: "Microsoft.Extensions.Logging.Console" : "1.0.0",
12: "Microsoft.Extensions.Configuration.Json" : "1.0.0",
13: "System.Text.Encoding.CodePages" : "4.0.1",
14: ...
15: }
16: }
我們在作為入口的Main方法中編寫了下面一段程序。如下面的代碼片段所示,我們通過加載這個logging.json文件創建了一個Configuration對象。在成功創建LoggerFactory后,我們將Configuration對象作為參數調用擴展方法AddConsole創建一個ConsoleLoggerProvider并注冊到LoggerFactory之上。我們最終利用LoggerFactory創建了一個Logger對象,并利用后者記錄三條日志。Logger采用的類型為“App”,這與配置文件設置的類型一致。
1: Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
2:
3: IConfiguration settings = new ConfigurationBuilder()
4: .AddJsonFile("logging.json")
5: .Build();
6:
7: ILogger logger = new ServiceCollection()
8: .AddLogging()
9: .BuildServiceProvider()
10: .GetService<ILoggerFactory>()
11: .AddConsole(settings)
12: .CreateLogger("App");
13:
14: int eventId = 3721;
15: logger.LogInformation(eventId, "升級到最新.NET Core版本({version})", "1.0.0 ");
16: logger.LogWarning(eventId, "并發量接近上限({maximum}) ", 200);
17: logger.LogError(eventId,"數據庫連接失敗(數據庫:{Database},用戶名:{User})", "TestDb", "sa");
根據定義在logging.json文件中的日志配置,只有等級不低于Warning的日志才會真正被輸出到控制臺上,所以對于上面程序中記錄的三條日志,控制臺上只會按照如下的形式呈現出等級分別為Warning和Error的兩條,等級為Information的日志直接被忽略。
.NET Core的日志[1]:采用統一的模式記錄日志
.NET Core的日志[2]:將日志寫入控制臺
.NET Core的日志[3]:將日志寫入Debug窗口
.NET Core的日志[4]:利用EventLog寫日志
.NET Core的日志[5]:利用TraceSource寫日志
文章列表