NET Core的日志模型主要由三個核心對象構成,它們分別是Logger、LoggerProvider和LoggerFactory。總的來說,LoggerProvider提供一個具體的Logger對象將格式化的日志消息寫入相應的目的地,但是我們在編程過程中使用的Logger對象則由LoggerFactory創建,這個Logger利用注冊到LoggerFactory的LoggerProvider來提供真正具有日志寫入功能的Logger,并委托后者來記錄日志。
目錄
一、Logger
擴展方法LogXxx與BeginScope
Logger<TCategoryName>
二、LoggerProvider
三、LoggerFactory
Logger提供的同一性
Logger類型
LoggerFactory類型
依賴注入
一、Logger
日志模型的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方法的調用。
擴展方法LogXxx與BeginScope
當我們調用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: }
Logger<TCategoryName>
每條日志消息都關聯著一個具體的類型(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對象。
三、LoggerFactory
從命名的角度來講,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體現了它們之間的關系。
Logger提供的同一性
上圖所示的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類的實現邏輯。如下面的代碼片段所示,我們創建一個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類型
我們同樣采用最精簡的代碼來模擬實現在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>>();
文章列表