Asp.NetCore源碼學習[2-1]:日誌
在一個系統中,日誌是不可或缺的部分。對於.net而言有許多成熟的日誌框架,包括
Log4Net
、NLog
、Serilog
等等。你能夠在系統中直接使用這些第三方的日誌框架,也能夠經過這些框架去適配ILoggerProvider
和ILogger
接口。適配接口的好處在於,若是想要切換日誌框架,只要實現並註冊新的ILoggerProvider
就能夠,而不影響日誌使用方的代碼。這就是在日誌系統中使用門面模式的優勢。html
.NetCore
中日誌的基本使用在控制層,咱們能夠直接經過ILogger
直接獲取日誌實例,也能夠經過ILoggerFactory.CreateLogger()
方法獲取日誌實例Logger
。無論使用哪一種方法獲取日誌實例,對於相同的categoryName
,返回的是同一個Logger
對象。git
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() { //雖然 _logger一、_logger二、_logger3 是不一樣的對象 //可是 _logger一、_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
進行配置,該集合用來保存須要被注入的接口、實現類、生命週期等等。github
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
實例。app
//注入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" } } }
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); }
ILoggerProvider
接口ILoggerProvider
用於提供 具體日誌實現類,好比ConsoleLogger、FileLogger等等。asp.net
public interface ILoggerProvider : IDisposable { /// 建立一個新的ILogger實例(具體日誌寫入類) ILogger CreateLogger(string categoryName); }
ILogger
接口雖然Logger
和具體日誌實現類都實現ILogger
接口,可是它們的做用是徹底不一樣的。其二者的區別在於:Logger
是系統中寫入日誌的統一入口,而 具體日誌實現類 表明了不一樣的日誌寫入途徑,好比ConsoleLogger
、FileLogger
等等。socket
/// 用於執行日誌記錄的類 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); }
LoggerFactory
日誌工廠類的實現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
的詳細特性能夠參照此處。
socket監聽到請求後,將KestrelConnection
加入線程池,線程池調度執行IThreadPoolWorkItem.Execute()
方法。在這裏開啓了一次HostingApplication.CreateContext()
),開啓了一次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
源碼一塊兒說才能理解,爭取可以寫出來吧。ide