在 2 月份期刊中,我探討了新命名的 .NET Core 1.0 平臺中包含的新配置 API(請參閱bit.ly/1OoqmkJ)。(我認為大多數讀者都已聽說過最近重新命名的 .NET Core 1.0,以前稱為.NET Core 5 和 ASP.NET 5 平臺的一部分 [請參閱 bit.ly/1Ooq7WI]。) 在那篇文章中我使用了單元測試,以便探索 Microsoft.Extensions.Configuration API。在這篇文章中我使用了類似的方法(Microsoft.Extensions.Logging 除外)。我的方法中的主要區別是,我從 .NET 4.6 CSPROJ 檔案進行測試,而不是從 ASP.NET Core 專案。這強調了一個事實,.NET Core 可供您立刻使用,即使您沒有遷移到 ASP.NET Core 專案也立刻使用。
日誌記錄? 我們究竟為何需要一個新的記錄框架? 我們已經擁有了 NLog、Log4Net、Loggr、Serilog 和內建 Microsoft.Diagnostics.Trace/Debug/TraceSource。這只是其中一些例子而已。事實證明,存在如此之多的記錄框架實際上是使 Microsoft.Extensions.Logging 相關的驅動因素之一。作為一個面對無數選擇的開發人員,您很可能選擇一個熟悉的,稍後您可能不得不切換到另一個。因此,您很可能想要編寫您自己的記錄 API 包裝器,以呼叫您或您的公司本週選擇的任何特定記錄框架。類似地,您可能在您的應用程式中使用一個特定的記錄框架,卻發現您要利用的庫之一正在使用另一個記錄框架,致使您不得不編寫一個偵聽器,用於將訊息從一個記錄框架傳遞到另一個記錄框架。
Microsoft 使用 Microsoft.Extensions.Logging 提供的正是這種包裝器,因此並不是每個人都必須編寫其自己的包裝器。此包裝器提供了一組 API,隨後會轉發到您選擇的提供程式。而且,當 Microsoft 包括用於控制檯 (Microsoft.Extensions.Logging.Console)、除錯 (Microsoft.Extensions.Logging.Debug)、事件日誌 (Microsoft.Extensions.Logging.EventLog) 和 TraceSource (Microsoft.Estensions.Logging.TraceSource) 等功能的提供程式時,它還與各種記錄框架團隊(其中包括 NLog、Serilog、Loggr、Log4Net 等第三方)協作,從而也存在來自它們的與 Microsoft.Extensions.Logging 相容的提供程式。
開始使用
日誌記錄活動的根始於日誌工廠,如圖 1 中所示。
1 2 3 4 5 6 7 8 9 |
public static void Main(string[] args = null) { ILoggerFactory loggerFactory = new LoggerFactory() .AddConsole() .AddDebug(); ILogger logger = loggerFactory.CreateLogger<Program>(); logger.LogInformation( "This is a test of the emergency broadcast system."); } |
正如程式碼所示,您以例項化 Microsoft.Extensions.Logging.LoggerFactory 開始,這可在同一名稱空間內實施 ILoggerFactory。接下來,您通過利用 ILoggerFactory 的擴充套件方法指定您想使用的提供程式。在圖 1 中,我專門使用了Microsoft.Extensions.Logging.ConsoleLoggerExtensions.AddConsole 和 Microsoft.Extensions.Logging.DebugLoggerFactoryExtensions.AddDebug。(雖然這兩個類均在 Microsoft.Extensions.Logging 名稱空間中,但實際上可分別在 Microsoft.Extensions.Logging.Console 和 Microsoft.Extensions.Logging.Debug NuGet 包中找到它們。)
擴充套件方法僅僅是新增提供程式的更通用方式的方便快捷方式 — ILoggerFactory.AddProvider(ILoggerProvider provider)。快捷方式是 AddProvider 方法需要日誌提供程式的一個例項(很可能該例項的建構函式需要一個日誌級別篩選器表示式),而擴充套件方法為這類表示式提供預設值。例如,ConsoleLoggerProvider 的建構函式簽名是:
1 2 |
public ConsoleLoggerProvider(Func<string, LogLevel, bool> filter, bool includeScopes); |
第一個引數是一個謂詞表示式,允許您基於已記錄文字的值和日誌級別定義某個訊息是否將出現在輸出中。
例如,您可以使用從比 LogLevel.Information 級別更高(更重要)的所有訊息的篩選器構造的特定 ConsoleLoggerProvider 例項呼叫 AddProvider:
1 2 3 |
loggerFactory.AddProvider( new ConsoleLoggerProvider( (text, logLevel) => logLevel >= LogLevel.Verbose , true)); |
(有趣的是,與返回 ILoggerFactory 的擴充套件方法不同,AddProvider 返回 void,防止出現圖 1 中所示的流式語法。)
重要的是要認識到,很遺憾,關於日誌級別越高的值是更重要還是更不重要,日誌提供程式之間存在一些不一致性。日誌級別為 6 是否表示發生了嚴重錯誤,或者它只是冗長的診斷資訊? Microsoft.Extensions.Logging.LogLevel 使用的值越高表示下面的 LogLevel 列舉宣告的優先順序越高:
1 2 3 4 5 6 7 8 9 10 |
public enum LogLevel { Debug = 1, Verbose = 2, Information = 3, Warning = 4, Error = 5, Critical = 6, None = int.MaxValue } |
因此,通過例項化僅在 logLevel >= LogLevel.Verbose 時寫入訊息的 ConsoleLoggerProvider,您將只排除將除錯級別資訊寫入到輸出。
請注意,您可以將多個提供程式新增到日誌工廠,甚至是同一個型別的多個提供程式。因此,如果我將 ILoggerFactory.AddProvider 的呼叫新增到圖 1,ILogger.LogInformation 的呼叫將在控制檯上顯示兩次某條訊息。第一個控制檯提供程式(由 AddConsole 新增的那個提供程式)預設顯示 LogLevel.Information 或更高階別的任何訊息。然而,僅在第二個提供程式(通過 AddProvider 方法新增的)成功避免被篩選掉時,ILogger.LogVerbose 呼叫才會只出現一次。
日誌記錄模式
正如圖 1 所示,所有日誌記錄的根始於日誌工廠,從中您可以通過 ILoggerFactory.CreateLogger<T> 方法請求 ILogger。此方法中的泛型型別 T 用於確定執行程式碼的類,因此可能寫出記錄器將在其中寫入資訊的類名。換句話說,通過呼叫 loggerFactory.CreateLogger<Program>,您基本上可啟動一個特定於 Program 類的記錄器,以便每次寫入一條資訊,還可能寫入 Program 類中的執行上下文。因此,圖 1 的控制檯輸出是:
1 2 |
info: SampleWebConsoleApp.Program[0] This is a test of the emergency broadcast system. |
此輸出基於以下內容:
- “info”事實上是一個 LogInformation 方法呼叫。
- “SampleWebConsoleApp.Program”是由 T 確定的。
- “[0]”是事件 ID(一個值),我沒有指定,因此預設為 0。
- “This is a test of the emergency broadcast system.”是傳遞到 LogInformation 的訊息引數。
因為值 Program 表示類級別上下文,您將很可能想要為您要從中進行記錄的每個類例項化不同的記錄器例項。例如,如果 Program 建立並呼叫到 Controller 類例項中,您將想要擁有通過另一個方法呼叫建立的 Controller 類中的新記錄器例項,其中 T 現在是 Controller:
1 |
loggerFactory.CreateLogger<Controller>() |
您可能會注意到,這需要訪問先前在其上配置提供程式的相同記錄器工廠例項。而且可以想象,當您可以將該記錄器工廠例項傳遞到您想要從中執行日誌記錄的每個類中時,它會很快成為一個需要請求重構的麻煩。
解決方案是將單個靜態 ILoggerFactory 儲存為一個靜態屬性,當例項化類的物件的特定 ILoggger 例項時可用於所有這些類。例如,考慮新增一個包含靜態 ILoggerFactory 例項的 ApplicationLogging 靜態類:
1 2 3 4 5 6 |
public static class ApplicationLogging { public static ILoggerFactory LoggerFactory {get;} = new LoggerFactory(); public static ILogger CreateLogger<T>() => LoggerFactory.CreateLogger<T>(); } |
這樣的類中的明顯問題是 LoggerFactory 是否是執行緒安全型的。而且幸運的是,圖 2 中所示的 AddProvider 方法正好說明了這個問題。
1 2 3 4 5 6 7 8 9 10 11 |
public void AddProvider(ILoggerProvider provider) { lock (_sync) { _providers = _providers.Concat(new[] { provider }).ToArray(); foreach (var logger in _loggers) { logger.Value.AddProvider(provider); } } } |
因為 ILogger 例項中僅有的資料是由泛型型別 T 確定的,所以您可能會認為每個類都可以有一個每個類的物件可以利用的靜態 ILogger。然而,假設確保執行緒安全性的程式設計標準適合所有靜態成員,這種做法將需要 ILogger 實現內的併發控制(預設情況下不存在),而且佔用和釋放鎖很可能導致一個重大瓶頸。為此,事實上建議一個類的每個例項都擁有單獨的 ILogger 例項。因此,結果是您支援日誌記錄的每個類上的 ILogger 屬性(請參見圖 3)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Controller { ILogger Logger { get; } = ApplicationLogging.CreateLogger<Controller>(); // ... public void Initialize() { using (Logger.BeginScopeImpl( $"=>{ nameof(Initialize) }")) { Logger.LogInformation("Initialize the data"); //... Logger.LogInformation("Initialize the UI"); //... } } } |
瞭解作用域
通常,提供程式支援“作用域”的概念,以便您可以(例如)記錄您的程式碼如何遍歷呼叫鏈。繼續上述示例,如果 Program 呼叫 Controller 類上的方法,相應地該類使用它自己的 T 型別的上下文例項化它自己的記錄器例項。然而,不是簡單地顯示後跟 info: SampleWebConsoleApp.Controller[0] 的 info: SampleWebConsoleApp.Program[0] 的訊息上下文,您可能希望記錄 Program 呼叫的 Controller,可能甚至包括方法名本身。要實現這一點,您可啟用提供程式內作用域的概念。圖 3 提供了一個在 Initialize 方法內呼叫 Logger.BeginScopeImpl 的示例。
使用日誌記錄模式的同時利用作用域啟用將產生 Program 類,可能看起來有點像圖 4。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Program { static ILogger Logger { get; } = ApplicationLogging.CreateLogger<Program>(); public static void Main(string[] args = null) { ApplicationLogging.LoggerFactory.AddConsole(true); Logger.LogInformation( "This is a test of the emergency broadcast system."); using (Logger.BeginScopeImpl(nameof(Main))) { Logger.LogInformation("Begin using controller"); Controller controller = new Controller(); controller.Initialize(); Logger.LogInformation("End using controller"); } Logger.Log(LogLevel.Information, 0, "Shutting Down...", null, null); } } |
圖 3 與圖 4 結合的輸出如圖 5 中所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
info: SampleWebConsoleApp.Program[0] This is a test of the emergency broadcast system. info: SampleWebConsoleApp.Program[0] => Main Begin using controller info: SampleWebConsoleApp.Controller[0] => Main => Initialize Initialize the data info: SampleWebConsoleApp.Controller[0] => Main => Initialize Initialize the UI info: SampleWebConsoleApp.Program[0] => Main End using controller info: SampleWebConsoleApp.Program[0] Shutting down... |
注意作用域如何自動展開,不再包含 Initialize 或 Main。之所以提供此功能,是因為使用語句呼叫 Dispose 時 BeginScopeImpl 返回可自動展開作用域的 IDisposable 例項。
利用第三方提供程式
為了使一些最為突出的第三方記錄框架可用,Microsoft 與其開發人員協作並確保每個記錄框架都有提供程式。無需指明首選項,只需考慮如何接上 NLog 框架,如圖 6 所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[TestClass] public class NLogLoggingTests { ILogger Logger {get;} = ApplicationLogging.CreateLogger<NLogLoggingTests>(); [TestMethod] public void LogInformation_UsingMemoryTarget_LogMessageAppears() { // Add NLog provider ApplicationLogging.LoggerFactory.AddNLog( new global::NLog.LogFactory( global::NLog.LogManager.Configuration)); // Configure target MemoryTarget target = new MemoryTarget(); target.Layout = "${message}"; global::NLog.Config.SimpleConfigurator.ConfigureForTargetLogging( target, global::NLog.LogLevel.Info); Logger.LogInformation(Message); Assert.AreEqual<string>( Message, target.Logs.FirstOrDefault<string>()); } } |
熟悉 NLog 即可知道此程式碼的大部分。首先,我例項化和配置 NLog.Targets.MemoryTarget 型別的 NLog target。(有許多 NLog target,而且每個 target 均可在 NLog 配置檔案中確定和配置,還可以使用配置程式碼,如圖 6 中所示。) 注意,雖然外觀相似,但是為 Layout 分配的是引數值 ${message},而不是字串內插值。
一旦新增到 LoggerFactory 並進行了配置,該程式碼便與任何其他提供程式程式碼完全相同。
異常處理
當然,要記錄的最常見原因之一是要在引發異常時(更具體地說,是在處理異常時而不是重新引發異常時,或者在完全未處理異常時)記錄(請參閱 bit.ly/1LYGBVS)。正如您所期望的,Microsoft.Extensions.Logging 具有處理異常的特定方法。大多數這樣的方法是作為 ILogger 的擴充套件方法在Microsoft.Extensions.Logging.LoggerExtensions 中實施的。而且,它來自針對特定的日誌級別(ILogger.LogInformation、ILogger.LogDebug、ILogger.LogCritical 等等)實施每個方法的類。例如,如果您想記錄與異常相關的 LogLevel.Critical 訊息(或許在從容地關閉應用程式之前),您將呼叫:
日誌記錄和異常處理的另一個重要方面是日誌記錄(特別是在處理異常時)不應引發異常。如果您在記錄時引發異常,可能訊息或異常將永遠不會被寫入,而且不管多麼嚴重都可能會被完全忽視。遺憾的是,現成的 ILogger 實現 (Microsoft.Extensions.Logging.Logger) 沒有此類異常處理,因此,如果發生異常,將需要呼叫程式碼來處理,而且想要這樣做的話,每次都得呼叫 Logger.LogX。解決此問題的常規方法可能是包裝記錄器以捕獲異常。然而,您可能想要實施您自己的 ILogger 和 ILoggerFactory 版本(有關示例,請參閱 bit.ly/1LYHq0Q)。由於 .NET Core 是開源的,您甚至可以克隆類以及在您自己的 LoggerFactory 和 ILogger 實現中故意實施異常處理。
總結
我開始問到“我們究竟為何還想要 .NET 中的其他記錄框架?” 我希望現在已經清楚了。新框架將建立一個抽象層或包裝器,使您可以將任何您想要的記錄框架用作提供程式。這確保您作為開發人員在工作中有最大的靈活性。此外,即使它僅隨附 .NET Core 提供,引用 .NET Core NuGet 包(如 Microsoft.Extensions.Logging)對於標準的 Visual Studio .NET 4.6 專案是沒有問題的。