547
技術社區[雲棲]
.NET Core下的日誌(2):日誌模型詳解
NET Core的日誌模型主要由三個核心對象構成,它們分別是Logger、LoggerProvider和LoggerFactory。總的來說,LoggerProvider提供一個具體的Logger對象將格式化的日誌消息寫入相應的目的地,但是我們在編程過程中使用的Logger對象則由LoggerFactory創建,這個Logger利用注冊到LoggerFactory的LoggerProvider來提供真正具有日誌寫入功能的Logger,並委托後者來記錄日誌。
目錄
一、Logger
擴展方法LogXxx與BeginScope
Logger<TCategoryName>
二、LoggerProvider
三、LoggerFactory
Logger提供的同一性
Logger類型
LoggerFactory類型
依賴注入
日誌模型的Logger泛指所有實現了ILogger接口的所有類型以及對應對象,該接口定義在NuGet包“Microsoft.Extensions.Logging.Abstractions”中,這個NuGet包同時定義了分別代表LoggerProvider和LoggerFactory的接口ILoggerProvider和ILoggerFactory。ILogger接口中定義了如下三個方法Log、IsEnabled和BeginScope。
1: public interface ILogger
2: {
3: void Log(LogLevel logLevel, EventId eventId, object state, Exception exception, Func<object, Exception, string> formatter);
4: bool IsEnabled(LogLevel logLevel);
5: IDisposable BeginScope<TState>(TState state);
6: }
Logger對日誌消息的寫入實現在Log方法中。Log方法的logLevel代表寫入日誌消息的等級,而日誌消息的原始內容通過參數state和exception這兩個參數來承載承載,前者代表一個原始的日誌條目(Log Entry),後者代表與之關聯的異常。日誌在被寫入之前必須格式成一個字符串,由於日誌原始信息分別由一個Object和Exception對象對象來表示,所以日誌的“格式化器”自然體現為一個Func<object, Exception, string>類型的委托對象。
一條寫入的日誌消息會關聯著一個日誌記錄事件,後者則通過一個EventId對象來標識,Log方法的eventId參數類型就是EventId。如下麵的代碼片段所示,EventId被定義成一個結構,它具有兩個基本的屬性Id和Name,前者代表必需的唯一標識,後者則是一個可選的名稱。除此之外,整形到EventId類型之間還存在一個隱式類型轉換,所以在需要使用EventId對象的地方,我們可以使用一個整數來代替。
1: public struct EventId
2: {
3: public int Id { get; }
4: public string Name{ get; }
5: public EventId(int id, string name = null);
6:
7: public static implicit operator EventId(int i);
8: }
對於任意一次日誌消息寫入請求,Logger並不會直接調用Log方法將日誌消息寫入對應的目的地,它會根據提供日誌消息的等級判斷是否應該執行寫入操作,判斷的邏輯實現在IsEnabled方法中,隻有當這個方法返回True的時候它的Log方法才會被執行。
在默認的情況下,每次調用Logger的Log方法所進行的日誌記錄操作都是相互獨立的,但是有時候我們需要將相關的多次日誌記錄做一個邏輯關聯,或者說我們需要為多次日誌記錄操作創建一個共同的上下文範圍。這樣一個關聯上下文範圍可以通過BeginScope<TState>方法來創建,該方法將該上下文範圍與參數state表示的對象進行關聯。被創建的這個關聯上下文體現為一個IDisposable對象,我們需要調用其Dispose方法將其釋放回收,也就是說被創建的關聯上下文的生命周期終止於Dispose方法的調用。
當我們調用Logger的Log方法記錄日誌時必須指定日誌消息采用的等級,出於調用便利性考慮,日誌模型還為ILogger接口定義了一係列針對不同日誌等級的擴展方法,比如LogDebug、LogTrace、LogInformation、LogWarning、LogError和LogCritical等。下麵的代碼片段列出了整個日誌等級Debug三個LogDebug方法重載的定義,針對其他日誌等級的擴展方法的定義與之類似。對於這些擴展方法來說,如果它們沒有定義表示日誌事件ID的參數eventId,默認使用的事件ID為0。
1: public static class LoggerExtensions
2: {
3: public static void LogDebug(this ILogger logger, EventId eventId, Exception exception, string message, params object[] args);
4: public static void LogDebug(this ILogger logger, EventId eventId, string message, params object[] args);
5: public static void LogDebug(this ILogger logger, string message, params object[] args);
6: }
對於定義在ILogger接口中的Log方法來說,原始日誌消息的內容通過Object類型的參數state和Exception類型的參數exception來承載,並通過一個Func<object, Exception, string>類型的委托對象來將它們格式化成可以寫入的字符串。上述這些擴展方法對此作了簡化,它利用一個包含占位符的字符串模板(對應參數message)和用於替換占位符的參數列表(對應參數args)來承載原始的日誌消息,日誌消息的格式化體現在如何使用提供的參數替換模板中相應的占位符進而生成一個完整的消息。值得一提的是,定義在模板中的占位符通過花括號括起來,可以使用零基連續整數(比如“{0}”、“{1}”和“{2}”等),也可以使用(比如“{Minimum}”和“Maximum”等)。
定義在ILogger接口的泛型方法BeginScope<TState>為多次相關的日誌記錄操作創建一個相同的執行上下文範圍,並將其上下文範圍與一個TState對象進行關聯。ILogger接口還具有如下一個同名的擴展方法,它采用與上麵類似的方式將創建的上下文範圍與一個字符串進行關聯,該字符串是指定的模板與參數列表格式化後的結果。
1: public static class LoggerExtensions
2: {
3: public static IDisposable BeginScope(this ILogger logger, string messageFormat, params object[] args);
4: }
每條日誌消息都關聯著一個具體的類型(Category),這個類型實際上創建這條日誌消息的“源”,我們一般將日誌記錄所在的應用或者組件名稱作為類型。除了ILogger這個基本的接口,日誌模型中還定義了如下一個泛型的ILogger <TCategoryName>接口,它派生與ILogger接口並將泛型參數的類型名稱作為由它寫入的日誌消息的類型。
1: public interface ILogger<out TCategoryName> : ILogger
2: {}
Logger<TCategoryName>實現了ILogger <TCategoryName>接口。一個Logger<TCategoryName>對象可以視為是對另一個Logger對象的封裝,它使用泛型參數類型來確定寫入日誌的類型,而采用這個內部封裝的Logger對象完成具體的日誌寫入操作。如下麵的代碼片段所示,Logger<TCategoryName>的構造函數接受一個LoggerFactory作為輸入參數,上述的這個內部封裝的Logger對象就是由它創建的。
1: public class Logger<TCategoryName> : ILogger<TCategoryName>
2: {
3: public Logger(ILoggerFactory factory) ;
4:
5: IDisposable ILogger.BeginScope<TState>(TState state;
6: void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state,
7: Exception exception, Func<TState, Exception, string> formatter) ;
8: }
在利用指定的LoggerFactory創建Logger對象時,泛型參數TCategoryName的類型被用來計算日誌類型。對於具有簡寫形式的基元類型(比如Int32、Boolean和Decimal等)來說,類型的簡寫形式(比如int、bool和decimal等)直接作為日誌類型名稱。對於一般的類型來說,日誌類型名稱就是該類型的全名(命名空間+類型名)。如果該類型內嵌於另一個類型之中(比如“Foo.Bar+Baz”),表示內嵌的“+”需要替換成“.”(比如“Foo.Bar.Baz”)。如果該類型是一個泛型類型(比如Foobar<T1,T2>),泛型參數部分將不包含在日誌類型名稱中(日誌類型為“Foobar”)。
除了調用構造函數創建一個Logger<TCategoryName>對象之外,我們還可以調用針對ILoggerFactory接口的擴展方法CreateLogger<T>來創建它。如下麵的代碼片段所示,除了這個CreateLogger<T>方法之外,另一個CreateLogger方法直接指定一個Type類型的參數,雖然返回類型不同,但是由此兩個方法創建的Logger在日誌記錄行為上是等效的。
1: public static class LoggerFactoryExtensions
2: {
3: public static ILogger<T> CreateLogger<T>(this ILoggerFactory factory)
4: public static ILogger CreateLogger(this ILoggerFactory factory, Type type);
5: }
二、LoggerProvider
日誌模型的LoggerProvider泛指所有實現了接口ILoggerProvider的類型和對應的對象,從其命名我們不難看出LoggerProvider的目的在於“提供”真正具有日誌寫入功能的Logger。如下麵的代碼片段所示,ILoggerProvider繼承了IDisposable,如果某個具體的LoggerProvider需要釋放某種資源,可以將相關的操作實現在Dispose方法中。
1: public interface ILoggerProvider : IDisposable
2: {
3: ILogger CreateLogger(string categoryName);
4: }
LoggerProvider針對Logger的提供實現在唯一的方法CreateLogger中,該方法的參數categoryName自然代表上麵我們所說的日誌消息的類型。這個CreateLogger方法返回類型為ILogger,代表根據指定日誌類型創建的Logger對象。
從命名的角度來講,LoggerProvider和LoggerFactory最終都是為了提供一個Logger對象,但是兩者提供的Logger對象在本質上是不同的。一個LoggerProvider一般針對某種具體的日誌目的地類型(比如控製台、文件或者Event Log等)提供對應的Logger,而LoggerFactory僅僅為我們創建日誌編程所用的那個Logger對象。
日誌模型中的LoggerFactory泛指所有實現了ILoggerFactory接口的所有類型及其對應的對象。如下麵的代碼片段所示,ILoggerFactory具有兩個簡單的方法,針對Logger的創建實現在CreateLogger方法中。我們通過調用AddProvider方法將某個LoggerProvider對象注冊到LoggerFactory之上,CreateLogger方法創建的Logger需要利用這些注冊的LoggerProvider來提供真正具有日誌寫入功能的Logger對象,並借助後者來完成對日誌的寫入操作。
1: public interface ILoggerFactory : IDisposable
2: {
3: ILogger CreateLogger(string categoryName);
4: void AddProvider(ILoggerProvider provider);
5: }
日誌模型中定義了一個實現了ILoggerFactory接口的類型,這就是我們在上麵演示實例中使用的LoggerFactory類,由它創建的是一個類型為Logger的對象,這兩個類型均定義在NuGet包“Microsoft.Extensions.Logging”之中。到目前為止,我們認識了日誌模型中的三個接口(ILogger、ILoggerProvider和ILoggerFactory)和其中兩個的實現者(Logger和LoggerFactory),右圖所示的UML體現了它們之間的關係。
上圖所示的UML基本上體現了Logger和LoggerFactory這兩個類型的實現邏輯,這個邏輯我們在上麵已經提到過多次,現在我們通過代碼實現的方式來對它做進一步地說明。在這之前,我們有必要了解LoggerFactory類型創建Logger過程中所體現出的一個重要特性,即對於CreateLogger方法的多次調用,如果我們指定的日誌類型(categoryName參數)相同(不區分大小寫),該方法返回的實際是同一個對象。
1: LoggerFactory loggerFactory = new LoggerFactory();
2: ILogger logger1 = loggerFactory.CreateLogger("App");
3:
4: loggerFactory.AddConsole();
5: ILogger logger2 = loggerFactory.CreateLogger("App");
6:
7: loggerFactory.AddDebug();
8: ILogger logger3 = loggerFactory.CreateLogger("App");
9:
10: Debug.Assert(ReferenceEquals(logger1, logger2) && ReferenceEquals(logger2, logger3));
如上麵的代碼片段所示,我們利用同一個LoggerFactory對象針對相同的日誌類型(“App”)先後得到三個Logger對象,雖然這三個Logger被創建的時候LoggerFactory具有不同的狀態(注冊到它上麵的LoggerProvider逐次增多),但是它們其實是同一個對象。換句話說,LoggerFactory和由它創建的Logger對象並不是兩個孤立的對象,它們之間存在著一種動態的關聯,當LoggerFactory自身的狀態發生改變時(注冊新的LoggerProvider),它會主動改變Logger的狀態使之與自身同步。
我們定義了一個精簡版本的同名類型來模擬真實Logger類的實現邏輯。如下麵的代碼片段所示,我們創建一個Logger對象的時候需要指定創建它的LoggerFactory對象和日誌類型。它的字段loggers代表由它封裝的一組具有真正日誌寫入功能的Logger對象,它們由注冊到LoggerFactory的LoggerProvider(體現為LoggerFactory的LoggerProviders屬性)來提供。
1: public class Logger : ILogger
2: {
3: private LoggerFactory loggerFactory;
4: private IList<ILogger> loggers;
5: private string categoryName;
6:
7: public Logger(LoggerFactory loggerFactory, string categoryName)
8: {
9: this.loggerFactory = loggerFactory;
10: this.categoryName = categoryName;
11: loggers = loggerFactory.LoggerProviders.Select(provider => provider.CreateLogger(categoryName)).ToList();
12: }
13:
14: public bool IsEnabled(LogLevel logLevel) => loggers.Any(logger => logger.IsEnabled(logLevel));
15:
16: public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
17: => loggers.ForEach(logger => logger.Log(logLevel, eventId, state, exception, formatter));
18:
19: internal void AddProvider(ILoggerProvider provider) => loggers.Add(provider.CreateLogger(categoryName));
20: //其他成員
21: }
IsEnabled方法實現了針對等級的日誌過濾,如果指定的日誌等級能夠通過任一Logger的過濾條件,該方法就返回True。至於真正用於實現日誌消息記錄的Log方法,它隻需要調用每個Logger對象的同名方法即可。除此之外,Logger類還定義了一個AddProvider方法,它利用指定的LoggerProvider來創建對應的Logger,並將後者添加到封裝的Logger列表中。一旦新的LoggerProvider注冊到LoggerFactory之上,LoggerFactory正是調用這個方法將新注冊的LoggerProvider應用到由它創建的Logger對象之上。
一個Logger對象是對一組具有真正日誌寫入功能的Logger對象的封裝,由它的BeginScope方法創建的日誌上下文範圍則是對這組Logger創建的上下文範圍的封裝。當這個日誌上下文範圍因調用Dispose方法被釋放的時候,這些內部封裝的上下文範圍同時被釋放。如下所示的代碼基本體現了定義在BeginScope方法中創建日誌上下文範圍的邏輯。
1: public class ConsoleLogger : ILogger
2: {
3: private IList<ILogger> loggers;
4: public IDisposable BeginScope<TState>(TState state)
5: {
6: return new Scope(loggers.Select(logger => logger.BeginScope(state)));
7: }
8:
9: private class Scope : IDisposable
10: {
11: private readonly IDisposable[] scopes;
12: public Scope(IEnumerable<IDisposable> scopes)
13: {
14: this.scopes = scopes.ToArray();
15: }
16: public void Dispose() => scopes.ForEach(scope => scope.Dispose());
17: }
18: }
我們同樣采用最精簡的代碼來模擬實現在LoggerFactory類型中的Logger創建邏輯。如下麵的代碼片段所示,處於線程安全方麵的考慮,我們定義了一個ConcurrentBag<ILoggerProvider>類型的屬性LoggerProviders來保存注冊到LogggerFactory上的LoggerProvider。另一個ConcurrentDictionary<string, Logger>類型的字段loggers則用來保存自身創建的Logger對象,該對象的Key表示日誌消息類型。
1: public class LoggerFactory : ILoggerFactory
2: {
3: internal ConcurrentBag<ILoggerProvider> LoggerProviders { get; private set; }
4: private readonly ConcurrentDictionary<string, Logger> loggers = new ConcurrentDictionary<string, Logger>(StringComparer.OrdinalIgnoreCase);
5:
6: public void AddProvider(ILoggerProvider provider)
7: {
8: this.LoggerProviders = new ConcurrentBag<ILoggerProvider>();
9: this.LoggerProviders.Add(provider);
10: loggers.ForEach(it => it.Value.AddProvider(provider));
11: }
12:
13: public ILogger CreateLogger(string categoryName)
14: {
15: Logger logger;
16: return loggers.TryGetValue(categoryName, out logger)
17: ? logger
18: : loggers[categoryName] = new Logger(this, categoryName);
19: }
20:
21: public void Dispose() => LoggerProviders.ForEach(provider => provider.Dispose());
22: }
當LoggerFactory的CreateLogger方法的時候,如果根據指定的日誌類型能夠在loggers字段表示的字典中找到一個Logger對象,則直接將它作為返回值。隻有在根據指定的日誌類型找不到 對應的Logger的情況下,LoggerFactory才會真正去創建一個新的Logger對象,並在返回之前將它添加到該字典之中。針對相同的日誌類型,LoggerFactory之所以總是返回同一個Logger,根源就在於此。
對於用於注冊LoggerProvider的AddProvider方法來說,LoggerFactory除了將指定的LoggerProvider添加到LoggerProviders屬性表示的列表之中,它還會調用每個已經創建的Logger對象的AddProvider方法。正是源於對這個方法的調用,我們新注冊到LoggerFactory上的LoggerProvider才會自動應用到所有已經創建的Logger對象中。
LoggerProvider類型都實現了IDisposable接口,針對它們的Dispose方法的調用被放在LoggerFactory的同名方法中。換句話說,當LoggerFactory被釋放的時候,注冊到它之上的所有LoggerProvider會自動被釋放。
在一個真正的.NET Core應用中,框架內部會借助ServiceProvider以依賴注入的形式向我們提供用於創建Logger對象的LoggerFactory。這樣一個ServiceProvider在根據一個ServiceCollection對象構建之前,我們必然需要在後者之上實施針對LoggerFactory的服務注冊,這樣的服務注冊可以通過針對接口IServiceCollection的擴展方法AddLogging來完成。
1: public static class LoggingServiceCollectionExtensions
2: {
3: public static IServiceCollection AddLogging(this IServiceCollection services)
4: {
5: services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());
6: services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));
7: return services;
8: }
9: }
如上麵的代碼片段所示,擴展方法AddLogging除了以Singleton模式注冊了ILoggerFactory接口與實現它的LoggerFactory類型之間的映射之外,還以同樣的模式注冊了ILogger<>接口和Logger<>類型的映射。如果創建ServiceProvider的ServiceCollection具有這兩個服務注冊,我們可以利用ServiceProvider直接提供一個Logger<T>,而不需要間接地利用ServiceProvider提供的LoggerFactory來創建它。下麵的代碼片段展示了Logger<T>的這兩種創建方式。
1: IServiceProvider serviceProvider = new ServiceCollection()
2: .AddLogging()
3: .BuildServiceProvider();
4:
5: ILogger<Foobar> logger1 = serviceProvider.GetService<ILoggerFactory>().CreateLogger<Foobar>();
6: ILogger<Foobar> logger2 = serviceProvider.GetService<ILogger<Foobar>>();
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-25 14:04:21