Asp.NetCore源碼學習[2-1]:日誌
- 2019 年 10 月 14 日
- 筆記
Asp.NetCore源碼學習[2-1]:日誌
在一個系統中,日誌是不可或缺的部分。對於.net而言有許多成熟的日誌框架,包括
Log4Net
、NLog
、Serilog
等等。你可以在系統中直接使用這些第三方的日誌框架,也可以通過這些框架去適配ILoggerProvider
和ILogger
介面。適配介面的好處在於,如果想要切換日誌框架,只要實現並註冊新的ILoggerProvider
就可以,而不影響日誌使用方的程式碼。這就是在日誌系統中使用門面模式的優點。
本系列源碼地址
一、.NetCore
中日誌的基本使用
在控制層,我們可以直接通過ILogger
直接獲取日誌實例,也可以通過ILoggerFactory.CreateLogger()
方法獲取日誌實例Logger
。不管使用哪種方法獲取日誌實例,對於相同的categoryName
,返回的是同一個Logger
對象。
public class ValuesController : ControllerBase { private readonly ILogger _logger1; private readonly ILogger _logger2; private readonly ILogger _logger3; public ValuesController(ILogger<ValuesController> logger, ILoggerFactory loggerFactory) { //_logger1是 Logger<T>類型 _logger1 = logger; //_logger2是 Logger類型 _logger2 = loggerFactory.CreateLogger(typeof(ValuesController)); //_logger3是 Logger<T>類型 該方法每次新建Logger<T>實例 _logger3 = loggerFactory.CreateLogger<ValuesController>(); } public ActionResult<IEnumerable<string>> Get() { //雖然 _logger1、_logger2、_logger3 是不同的對象 //但是 _logger1、_logger3 中的 Logger實例 和 _logger2 是同一個對象 var hashCode1 = _logger1.GetHashCode(); var hashCode2 = _logger2.GetHashCode(); var hashCode3 = _logger3.GetHashCode(); _logger1.LogDebug("Test Logging"); return new string[] { "value1", "value2"}; } }
二、源碼解讀
WebHostBuilder
內部維護了_configureServices
欄位,其類型是 Action<WebHostBuilderContext, IServiceCollection>
,該委託用於對集合ServiceCollection
進行配置,該集合用來保存需要被注入的介面、實現類、生命周期等等。
public class WebHostBuilder { private Action<WebHostBuilderContext, IServiceCollection> _configureServices; public IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices) { _configureServices += configureServices; return this; } public IWebHost Build() { var services = new ServiceCollection();//該集合用於保存需要注入的服務 services.AddLogging(services, builder => { }); _configureServices?.Invoke(_context, services);//配置ServiceCollection //返回Webhost } }
首先在CreateDefaultBuilder
方法中通過調用ConfigureLogging
方法對日誌模組進行配置,在這裡我們可以註冊需要的 ILoggerProvider
實現。
public static IWebHostBuilder CreateDefaultBuilder(string[] args) { var builder = new WebHostBuilder(); builder.ConfigureLogging((hostingContext, logging) => { logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); }). return builder; }
從 ConfigureLogging
方法開始,到ConfigureServices
,最後到AddLogging
,雖然看上去有點繞,但實際上只是構建了一個委託,並將委託保存到WebHostBuilder._configureServices
欄位中,該委託用於把日誌模組需要的一系列對象類型保存到ServiceCollection
中,最終構建依賴注入模組。
public static IWebHostBuilder ConfigureLogging(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, ILoggingBuilder> configureLogging) { return hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder))); } /// 向IServiceCollection中注入日誌系統需要的類 public static IServiceCollection AddLogging(this IServiceCollection services, Action<ILoggingBuilder> configure) { if (services == null) { throw new ArgumentNullException(nameof(services)); } services.AddOptions(); services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>()); services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(new DefaultLoggerLevelConfigureOptions(LogLevel.Information))); configure(new LoggingBuilder(services)); return services; }
上面和日誌模組相關的注入看起來比較混亂,在這裡匯總一下:
可以看到,IConfigureOptions
注入了兩個不同的實例,由於在IOptionsMonitor
中會順序執行,所以先通過 默認的DefaultLoggerLevelConfigureOptions
去配置LoggerFilterOptions
實例,然後讀取配置文件的"Logging"
節點去配置LoggerFilterOptions
實例。
//注入Options,使得在日誌模組中可以讀取配置 services.AddOptions(); //注入日誌模組 services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>()); services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); //註冊默認的配置 LoggerFilterOptions.MinLevel = LogLevel.Information services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<LoggerFilterOptions>>(new DefaultLoggerLevelConfigureOptions(LogLevel.Information))); var logging = new LoggingBuilder(services); logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); logging.AddConsole(); public static ILoggingBuilder AddConfiguration(this ILoggingBuilder builder, IConfiguration configuration) { // builder.Services.TryAddSingleton<ILoggerProviderConfigurationFactory, LoggerProviderConfigurationFactory>(); builder.Services.TryAddSingleton(typeof(ILoggerProviderConfiguration<>), typeof(LoggerProviderConfiguration<>)); //註冊LoggerFactory中IOptionsMonitor<LoggerFilterOptions>相關的依賴 //這樣可以在LoggerFactory中讀取配置文件,並在文件發生改變時,對已生成的Logger實例進行相應規則改變 builder.Services.AddSingleton<IConfigureOptions<LoggerFilterOptions>>(new LoggerFilterConfigureOptions(configuration)); builder.Services.AddSingleton<IOptionsChangeTokenSource<LoggerFilterOptions>>(new ConfigurationChangeTokenSource<LoggerFilterOptions>(configuration)); // builder.Services.AddSingleton(new LoggingConfiguration(configuration)); return builder; }
日誌配置文件
Logging::LogLevel
節點,適用於所有ILoggerProvider
的規則。Logging::{ProviderName}::LogLevel
節點,適用於名稱為{ProviderName}
的ILoggerProvider
- 在
LogLevel
節點下,"Default"
節點值代表了適用於所有CategoryName的日誌級別 - 在
LogLevel
節點下,非"Default"
節點使用節點名去匹配CategoryName,最多支援一個"*"
"Logging": { "CaptureScopes": true, "LogLevel": { // 適用於所有 ILoggerProvider "Default": "Information", "Microsoft": "Warning" }, "Console": { // 適用於 ConsoleLoggerProvider[ProviderAlias("Console")] "LogLevel": { // 對於 CategoryName = "Microsoft.Hosting.Lifetime" 優先等級從上到下遞減: // 1.開頭匹配 等效於 "Microsoft.Hosting.Lifetime*" "Microsoft.Hosting.Lifetime": "Information", // 2.首尾匹配 "Microsoft.*.Lifetime": "Information", // 3.開頭匹配 "Microsoft": "Warning", // 4.結尾匹配 "*Lifetime": "Information", // 5.匹配所有 "*": "Information", // 6.CategoryName 全局配置 "Default": "Information" } } }
1、 日誌相關的介面
1.1 ILoggerFactory
介面
ILoggerFactory
是日誌工廠類,用於註冊需要的ILoggerProvider
,並生成Logger
實例。Logger
對象是日誌系統的門面類,通過它我們可以寫入日誌,卻不需要關心具體的日誌寫入實現。只要註冊了相應的ILoggerProvider
, 在系統中我們就可以通過Logger
同時向多個路徑寫入日誌資訊,比如說控制台、文件、資料庫等等。
/// 用於配置日誌系統並創建Logger實例的類 public interface ILoggerFactory : IDisposable { /// 創建一個新的Logger實例 /// <param name="categoryName">消息類別,一般為調用Logger所在類的全名</param> ILogger CreateLogger(string categoryName); /// 向日誌系統註冊一個ILoggerProvider void AddProvider(ILoggerProvider provider); }
1.2 ILoggerProvider
介面
ILoggerProvider
用於提供 具體日誌實現類,比如ConsoleLogger、FileLogger等等。
public interface ILoggerProvider : IDisposable { /// 創建一個新的ILogger實例(具體日誌寫入類) ILogger CreateLogger(string categoryName); }
1.3 ILogger
介面
雖然Logger
和具體日誌實現類都實現ILogger
介面,但是它們的作用是完全不同的。其兩者的區別在於:Logger
是系統中寫入日誌的統一入口,而 具體日誌實現類 代表了不同的日誌寫入途徑,比如ConsoleLogger
、FileLogger
等等。
/// 用於執行日誌記錄的類 public interface ILogger { /// 寫入一條日誌條目 /// <typeparam name="TState">日誌條目類型</typeparam> /// <param name="logLevel">日誌級別</param> /// <param name="eventId">事件ID</param> /// <param name="state">將會被寫入的日誌條目(可以為對象)</param> /// <param name="exception">需要記錄的異常</param> /// <param name="formatter">格式化器:將state和exception格式化為字元串</param> void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter); /// 判斷該日誌級別是否啟用 bool IsEnabled(LogLevel logLevel); /// 開始日誌作用域 IDisposable BeginScope<TState>(TState state); }
2、 LoggerFactory
日誌工廠類的實現
在構造函數中做了兩件事情:
- 獲取在DI模組中已經注入的
ILoggerProvider
,將其保存到集合中。類型ProviderRegistration
擁有欄位ShouldDispose
,其含義為:在LoggerFactory
生命周期結束之後,該ILoggerProvider
是否需要釋放。雖然在系統中LoggerFactory
為單例模式,但是其提供了一個靜態方法生成一個可釋放的DisposingLoggerFactory
。 - 通過
IOptionsMonitor
綁定更改回調,在配置文件發生更改時,執行相應動作。
public class LoggerFactory : ILoggerFactory { private readonly Dictionary<string, Logger> _loggers = new Dictionary<string, Logger>(StringComparer.Ordinal); private readonly List<ProviderRegistration> _providerRegistrations = new List<ProviderRegistration>(); private IDisposable _changeTokenRegistration; private LoggerExternalScopeProvider _scopeProvider; public LoggerFactory(IEnumerable<ILoggerProvider> providers, IOptionsMonitor<LoggerFilterOptions> filterOption) { foreach (var provider in providers) { AddProviderRegistration(provider, dispose: false); } _changeTokenRegistration = filterOption.OnChange((o, _) => RefreshFilters(o)); RefreshFilters(filterOption.CurrentValue); } /// 註冊日誌提供器 private void AddProviderRegistration(ILoggerProvider provider, bool dispose) { _providerRegistrations.Add(new ProviderRegistration { Provider = provider, ShouldDispose = dispose }); // 如果日誌提供器 實現 ISupportExternalScope 介面 if (provider is ISupportExternalScope supportsExternalScope) { if (_scopeProvider == null) { _scopeProvider = new LoggerExternalScopeProvider(); } //將單例 LoggerExternalScopeProvider 保存到 provider._scopeProvider 中 //將單例 LoggerExternalScopeProvider 保存到 provider._loggers.ScopeProvider 裡面 supportsExternalScope.SetScopeProvider(_scopeProvider); } } }
CreateLogger
方法:
- 內部使用字典保存
categoryName
和對應的Logger
。 Logger
內部維護三個數組:LoggerInformation[]、MessageLogger[]、ScopeLogger[]
- 在
LoggerInformation
的構造函數中生成了實際的日誌寫入類(FileLogger、ConsoleLogger
)
/// 創建 Logger 日誌門面類 public ILogger CreateLogger(string categoryName) { lock (_sync) { if (!_loggers.TryGetValue(categoryName, out var logger))// 如果字典中不存在新建Logger { logger = new Logger { Loggers = CreateLoggers(categoryName), }; (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers);// 根據配置應用過濾規則 _loggers[categoryName] = logger;// 加入字典 } return logger; } } /// 根據註冊的ILoggerProvider,創建Logger需要的 LoggerInformation[] private LoggerInformation[] CreateLoggers(string categoryName) { var loggers = new LoggerInformation[_providerRegistrations.Count]; for (var i = 0; i < _providerRegistrations.Count; i++) { loggers[i] = new LoggerInformation(_providerRegistrations[i].Provider, categoryName); } return loggers; } internal readonly struct LoggerInformation { public LoggerInformation(ILoggerProvider provider, string category) : this() { ProviderType = provider.GetType(); Logger = provider.CreateLogger(category); Category = category; ExternalScope = provider is ISupportExternalScope; } /// 具體日誌寫入途徑實現類 public ILogger Logger { get; } /// 日誌類別名稱 public string Category { get; } /// 日誌提供器Type public Type ProviderType { get; } /// 是否支援 ExternalScope public bool ExternalScope { get; } }
ApplyFilters
方法:
MessageLogger[]
取值邏輯:遍歷LoggerInformation[]
,從配置文件中讀取對應的日誌級別, 如果在配置文件中沒有對應的配置,默認取_filterOptions.MinLevel
。如果讀取到的日誌級別大於LogLevel.Critical
,則將其加入MessageLogger[]
。ScopeLogger[]
取值邏輯:如果ILoggerProvider
實現了ISupportExternalScope
介面,那麼使用LoggerExternalScopeProvider
作為Scope
功能的實現。反之,使用ILogger
作為其Scope
功能的實現。- 多個
ILoggerProvider
共享同一個LoggerExternalScopeProvider
/// 根據配置應用過濾 private (MessageLogger[] MessageLoggers, ScopeLogger[] ScopeLoggers) ApplyFilters(LoggerInformation[] loggers) { var messageLoggers = new List<MessageLogger>(); var scopeLoggers = _filterOptions.CaptureScopes ? new List<ScopeLogger>() : null; foreach (var loggerInformation in loggers) { // 通過 ProviderType Category從 LoggerFilterOptions 中匹配對應的配置 RuleSelector.Select(_filterOptions, loggerInformation.ProviderType, loggerInformation.Category, out var minLevel, out var filter); if (minLevel != null && minLevel > LogLevel.Critical) { continue; } messageLoggers.Add(new MessageLogger(loggerInformation.Logger, loggerInformation.Category, loggerInformation.ProviderType.FullName, minLevel, filter)); // 不支援 ExternalScope: 啟用 ILogger 自身實現的scope if (!loggerInformation.ExternalScope) { scopeLoggers?.Add(new ScopeLogger(logger: loggerInformation.Logger, externalScopeProvider: null)); } } // 只要其中一個Provider支援 ExternalScope:將 _scopeProvider 加入 scopeLoggers if (_scopeProvider != null) { scopeLoggers?.Add(new ScopeLogger(logger: null, externalScopeProvider: _scopeProvider)); } return (messageLoggers.ToArray(), scopeLoggers?.ToArray()); }
LoggerExternalScopeProvider
大概的實現邏輯:
- 通過
Scope
組成了一個單向鏈表,每次beginscope
向鏈表末端增加一個新的元素,Dispose
的時候,刪除鏈表最末端的元素。我們知道LoggerExternalScopeProvider
在系統中是單例模式,多個請求進來,加入執行緒池處理。通過使用AsyncLoca
來實現不同執行緒間數據獨立。AsyncLocal
的詳細特性可以參照此處。 - 有兩個地方開啟了日誌作用域:
- 1、通過
socket監聽到請求後,將
KestrelConnection
加入執行緒池,執行緒池調度執行IThreadPoolWorkItem.Execute()
方法。在這裡開啟了一次 - 2、在構建請求上下文對象的時候(
HostingApplication.CreateContext()
),開啟了一次
3、Logger
日誌門面類的實現
MessageLogger[]
保存了在配置文件中啟用的那些ILogger
- 需要注意的是,由於配置文件更改後,會調用
ApplyFilters()
方法,並為MessageLogger[]
賦新值,所以在遍歷之前,需要保存當前值,再進行處理。否則會出現修改異常。
internal class Logger : ILogger { public LoggerInformation[] Loggers { get; set; } public MessageLogger[] MessageLoggers { get; set; } public ScopeLogger[] ScopeLoggers { get; set; } public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { var loggers = MessageLoggers; if (loggers == null) { return; } List<Exception> exceptions = null; for (var i = 0; i < loggers.Length; i++) { ref readonly var loggerInfo = ref loggers[i]; if (!loggerInfo.IsEnabled(logLevel)) { continue; } LoggerLog(logLevel, eventId, loggerInfo.Logger, exception, formatter, ref exceptions, state); } if (exceptions != null && exceptions.Count > 0) { ThrowLoggingError(exceptions); } static void LoggerLog(LogLevel logLevel, EventId eventId, ILogger logger, Exception exception, Func<TState, Exception, string> formatter, ref List<Exception> exceptions, in TState state) { try { logger.Log(logLevel, eventId, state, exception, formatter); } catch (Exception ex) { if (exceptions == null) { exceptions = new List<Exception>(); } exceptions.Add(ex); } } } }
最後
這篇文章也壓在箱底一段時間了,算是匆忙結束。還有挺多想寫的,包括 Diagnostics、Activity、Scope
等等,這些感覺需要結合SkyAPM-dotnet
源碼一起說才能理解,爭取能夠寫出來吧。