閱讀315 返回首頁    go 阿裏雲 go 技術社區[雲棲]


.NET Core的日誌[2]:將日誌輸出到控製台

對於一個控製台應用,比如采用控製台應用作為宿主的ASP.NET Core應用,我們可以將記錄的日誌直接輸出到控製台上。針對控製台的Logger是一個類型為ConsoleLogger的對象,ConsoleLogger對應的LoggerProvider類型為ConsoleLoggerProvider,這兩個類型都定義在 NuGet包“Microsoft.Extensions.Logging.Console”之中。 本文已經同步到《ASP.NET Core框架揭秘》之中]

目錄
一、ConsoleLogger
二、ConsoleLogScope
三、ConsoleLoggerProvider
四、擴展方法AddConsole

如下所示的代碼片段展示了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

在默認情況下針對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:           
   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:       => 
   3:       商品庫存不足(商品ID: 9787121237812, 當前庫存:20, 訂購數量:50)
   4: fail: Ordering[0]
   5:       => 
   6:       商品ID錄入錯誤(商品ID: 9787121235368)

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")));

針對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的日誌直接被忽略。

5


.NET Core的日誌[1]:采用統一的模式記錄日誌
.NET Core的日誌[2]:將日誌寫入控製台
.NET Core的日誌[3]:將日誌寫入Debug窗口
.NET Core的日誌[4]:利用EventLog寫日誌
.NET Core的日誌[5]:利用TraceSource寫日誌
作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-25 11:05:34

  上一篇:go  .NET Core采用的全新配置係統[10]: 配置的同步機製是如何實現的?
  下一篇:go  .NET Core的日誌[3]:將日誌寫入Debug窗口