定義在NuGet包“Microsoft.Extensions.Logging.Debug”中的DebugLogger會直接調用Debug的WriteLine方法來寫入分發給它的日志消息。如果需要使用DebugLogger來寫日志,我們需要將它的提供者DebugLoggerProvider注冊到LoggerFactory上。由于定義在Debug類型中的所有方法都是針對Debug編譯模式的,所以在只有針對Debug模式編譯的應用中使用DebugLogger才有意義。這里將的“Debug編譯模式”涉及到一個叫做“條件編譯”的話題。 本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、Debug類型與條件編譯
二、DebugLogger
三、DebugLoggerProvider
一、Debug類型與條件編譯
DebugLogger適用于.NET Framework和.NET Core應用,我們說DebugLogger最終是通過調用Debug類型的靜態方法WriteLine來寫入分發給它的日志消息,但是使用的這個Debug類型在.NET Framework和.NET Core應用下其實是兩個完全不同的類型。針對.NET Framework的Debug類型定義在程序集“System.dll”下,而針對.NET Core的Debug類型則承載于“System.Diagnostics.Debug”這個NuGet包中,這兩個Debug方法具有不同的API定義。
這兩個Debug類型針對日志的寫入機制也不盡相同,針對.NET Framework的Debug類型定會利用注冊到Debug.Listeners屬性TraceListener來寫日志,默認注冊的DefaultTraceListener會通過調用Win32函數OutputDebugString將格式化的日志消息輸出給Debug監視器(Debug Monitor)。對于針對針對.NET Core的Debug類型來說,它針對不同的平臺具有不同的實現,針對Windows平臺下日志消息依然是通過調用OutputDebugString這Win32函數來寫入的。
雖然兩個Debug類型在API定義和寫入日志的實現都不同,但是對于被DebugLogger用來寫日志的WriteLine方法來說,它們都具有如下所示的定義方式。該方法具有兩個參數,分別代表寫入日志的文本消息和類型。我們可以看到這個方法上標注了一個類型為ConditionalAttribute的特性,它具有一個值為“DEBUG”的參數。這個ConditionalAttribute特性就與我們所說的“條件編譯”有關。
1: public static class Debug
2: {
3: [Conditional("DEBUG")]
4: public static void WriteLine(string message,string category);
5: }
所謂的“條件編譯”,就是說編譯器在進行編譯的時候會根據指定的條件來過濾參與編譯的源代碼,這個源代碼過濾條件是在編譯時指定的符號化的字符串,我們稱它為“條件編譯符(Conditional Compilation Symbol)”,上面代碼片段中作為ConditionalAttribute特性參數的“DEBUG”就是條件編譯符。如果我們使用Visual Studio作為IDE,我們可以利用它以可視化的方式來為某個的項目設置一個或者多個就是條件編譯符。我們只需要右擊某個項目并在彈出的上下文菜單中選擇“屬性(Properties)”,然后按照如下圖所示方式在顯示的項目屬性窗口中選擇“生成(Build)”選項卡。
如圖8所示,我們可以定義任意字符串作為條件編譯符(比如“UAT”,“SIT”)。除此之外,Visual Studio還為我們預設了“DEBUG”和“TRACE”這兩個常用的條件編譯符,如果需要我們只需要選擇相應的復選框(“Define DEBUG/TRACE constant”)即可。我們通過這種方法設置的條件編譯符最終會作為編譯選項以如下的方式寫入到project.json文件中,具體的配置項目為“buildOptions/define”,換句話說,我們完全可以直接編輯project.json文件的方式來定義條件編譯符。
1: {
2: ...
3: "buildOptions": {
4: "define": [ "DEBUG", "TRACE", "UAT, SIT" ]
5: }
6: }
從某種意義來說,條件編譯符實際上是為應用定義相應的“部署場景”,比如我們在上邊定義的條件編譯符“UAT”和“SIT”就是針對兩種不同類型(用戶接收測試和系統集成測試)的測試部署場景。如果我們需要編寫針對具有某種部署場景的程序,可以采用預編譯指令“#if/#endif”來實現。如果編譯器在編譯如下一段代碼的時候,只有指定的條件編譯符包含“DEBUG”的情況下,調用WriteDebug方法的這段代碼才會參與編譯,否則這段代碼將直接被忽略。
1: #if DEBUG
2: WriteDebug(message);
3: #endif
完全采用預編譯指令“#if/#endif”來編寫針對具體某個條件編譯符的代碼其實是很繁瑣的。如果某個方法總是針對具體某個條件編譯符,我們可以直接在這樣的方法上標注一個ConditionalAttribute特性,并將對應的條件編譯符作為其參數即可。比如上面這個WriteDebug方法就可以采用如下的方式來定義,它可以作為一個普通的方法來調用,而無需再使用任何預編譯指令。
1: [Conditional(“DEBUG”)]
2: public static void WriteBug(string message);
編譯器在編譯我們的程序的時候,如果程序中調用了某個標注了ConditionalAttribute特性的方法并且指定的條件編譯符與當前不一致,針對該方法調用的代碼將被自動忽略。定義在Debug類型上的WriteLine方法上就標注了這么一個ConditionalAttribute特性,指定的編譯符為“DEBUG”,大家應該知道為什么DebugLogger為什么只有針對Debug模式編譯生成的應用才后意義了吧。
二、DebugLogger
在了解了Debug類型和條件編譯的背景知識后,我們來正式認識一下DebugLogger類型。我們采用如下一段現對簡介的代碼模擬了DebugLogger的定義。當我們調用構造函數創建一個DebugLogger對象的時候需要指定Logger的名稱和進行日志過濾的Func<string, LogLevel, bool>對象,后者是可選的。DebugLogger調用Debug的WriteLine方法來進行日志寫入體現在它的Log方法中,寫入的日志消息將DebugLogger的名稱作為日志類型。
1: public class DebugLogger : ILogger
2: {
3: private readonly Func<string, LogLevel, bool> _filter;
4: private readonly string _name;
5:
6: public DebugLogger(string name, Func<string, LogLevel, bool> filter)
7: {
8: _name = string.IsNullOrEmpty(name) ? "DebugLogger" : name;
9: _filter = filter?? ((cate, level) => true);
10: }
11:
12: public DebugLogger(string name) : this(name, null)
13: {}
14:
15:
16: public IDisposable BeginScope<TState>(TState state)
17: {
18: return NoopDisposable.Instance;
19: }
20:
21: public bool IsEnabled(LogLevel logLevel)
22: {
23: return Debugger.IsAttached && _filter(_name, logLevel);
24: }
25:
26: public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
27: {
28: if (this.IsEnabled(logLevel))
29: {
30: string message = formatter(state, exception);
31: message = $"{logLevel}: {message}";
32: if (exception != null)
33: {
34: message = $"{message}{Environment.NewLine}{Environment.NewLine}{exception}";
35: }
36: Debug.WriteLine(message, _name);
37: }
38: }
39:
40: private class NoopDisposable : IDisposable
41: {
42: public static DebugLogger.NoopDisposable Instance = new DebugLogger.NoopDisposable();
43: public void Dispose()
44: {}
45: }
46: }
上面這段代碼和體現了DebugLogger進行日志記錄的一些細節特性:
- 如果調用構造函數指定的名稱為Null或者是一個空字符串,創建的DebugLogger對象將使用它的類型名(“DebugLogger”)來命名。如果作為日志過濾器的Func<string, LogLevel, bool>對象沒有顯式指定,意味著不需要對日志進行過濾。
- DebugLogger并不支持日志上下文,所以它的BeginScope<TState>方法返回的NoopDisposable對象并承載任何上下文信息,這也是DebugLogger的構造函數不像ConsoleLogger一樣具有一個includeScope參數的原因。
- DebugLogger的IsEanbled方法不僅僅利用構造時指定的作為日志過濾器的Func<string, LogLevel, bool>對象來決定是否真正寫入日志,還需要考慮調試器是否附加到當前進程(Debugger.IsAttached),只有這個兩個條件都滿足的情況下,這個方法才會返回True。
- DebugLogger的Log方法在真正寫入日志的過程中,它會利用指定的作為格式化器的Func<TState, Exception, string>對象將承載原始日志信息的對象和異常(對應參數state和exception)格式成一個完整的字符串作為最終寫入的日志消息。但是在指定的Exception對象不為Null的情況下,它又會在這個格式好的日志消息上附加上異常信息,這其實是不太合理的。
三、DebugLoggerProvider
DebugLogger對應的LoggerProvider是一個DebugLoggerProvider對象。如下面的代碼片段所示,DebugLoggerProvider提供DebugLogger的邏輯非常簡單,它只需要在實現的CreateLogger方法中調用構造函數創建并返回一個DebugLogger對象即可,提供的作為日志過濾器的Func<string, LogLevel, bool>對象在自身的構造函數中由對應的參數指定。
1: public class DebugLoggerProvider : ILoggerProvider, IDisposable
2: {
3: private readonly Func<string, LogLevel, bool> _filter;
4: public DebugLoggerProvider(Func<string, LogLevel, bool> filter)
5: {
6: _filter = filter;
7: }
8:
9: public ILogger CreateLogger(string name)
10: {
11: return new DebugLogger(name, _filter);
12: }
13:
14: public void Dispose()
15: {}
16: }
針對DebugLoggerProvider的注冊可以通過如下三個針對ILoggerFactory接口的擴展方法AddDebug來完成。我們調用這些方法時可以為注冊的DebugLoggerProvider指定作為日志過濾器的Func<string, LogLevel, bool>對象,也可以指定一個最低的日志等級。如果這兩者都沒有指定,從給出的代碼片段可以看出該方法會默認將Information作為最低日志等級。也就是說,當我們調用AddDebug方法時如果沒有指定任何日志過濾條件,等級為Debug的日志消息并不會被記錄下來,這一點也是我們個人覺得不太合理的地方。
1: public static class DebugLoggerFactoryExtensions
2: {
3: public static ILoggerFactory AddDebug(this ILoggerFactory factory)
4: {
5: return factory.AddDebug(LogLevel.Information);
6: }
7:
8: public static ILoggerFactory AddDebug(this ILoggerFactory factory, LogLevel minLevel)
9: {
10: return factory.AddDebug((cate, level) => level >= minLevel);
11: }
12:
13: public static ILoggerFactory AddDebug(this ILoggerFactory factory, Func<string, LogLevel, bool> filter)
14: {
15: factory.AddProvider(new DebugLoggerProvider(filter));
16: return factory;
17: }
18: }
接下來我們通過一個簡單的實例來演示針對DebugLogger的日志記錄。我們創建一個空的控制臺應用,在添加必要的依賴之后,我們在Main方法中編寫了如下一段程序。如下面的代碼片段所示,我們采用依賴注入的方式創建了一個LoggerFactory,并調用AddDebug方法完成了針對DebugLoggerProvider的注冊。在利用LoggerFactory創建出Logger對象之后,我們利用后者記錄了三條日志消息。
1: public class Program
2: {
3: public static void Main(string[] args)
4: {
5: ILogger logger = new ServiceCollection()
6: .AddLogging()
7: .BuildServiceProvider()
8: .GetService<ILoggerFactory>()
9: .AddDebug()
10: .CreateLogger<Program>();
11:
12: logger.LogDebug("這是一個等級為Debug的日志");
13: logger.LogInformation("這是一個等級為Information的日志");
14: logger.Log(LogLevel.Error, 3721, "這是一個等級為Error的日志",new FileNotFoundException("目標文件不存在"),
15: (state, exception) => $"{state}{Environment.NewLine}{exception}");
16: }
17: }
記錄的三條日志具有不同的等級(分別為Debug、Information和Error)。第三條日志的記錄是調用Logger對象的Log方法實現的,我們在調用該方法時指定了所有的承載日志消息所有的信息(日志等級、事件ID、日志原始消息和異常)和作為格式化器的Func<TState, Exception, string>對象。值得一提是作為格式化器的這個委托對象已經考慮到了針對異常消息的格式化。
現在直接利用Visual Studio在Debug模式下編譯并運行這個程序,我們會在輸出窗口中看到寫入的日志。如下圖所示,Visual Studio的輸出窗口只顯示了兩條等級分別為Information和Error的日志,等級為Debug的日志并沒有被記錄下來。對于記錄的第二條日志,我們發現異常的信息被重復記錄,前者是的內容是源于我們指定的格式化器,后者則是DebugConsoleLogger的Log方法自行附加上去的。
.NET Core的日志[1]:采用統一的模式記錄日志
.NET Core的日志[2]:將日志寫入控制臺
.NET Core的日志[3]:將日志寫入Debug窗口
.NET Core的日志[4]:利用EventLog寫日志
.NET Core的日志[5]:利用TraceSource寫日志
文章列表