Asp.NetCore源碼學習[2-1]:日誌

  • 2019 年 10 月 14 日
  • 筆記

Asp.NetCore源碼學習[2-1]:日誌

在一個系統中,日誌是不可或缺的部分。對於.net而言有許多成熟的日誌框架,包括Log4NetNLogSerilog 等等。你可以在系統中直接使用這些第三方的日誌框架,也可以通過這些框架去適配ILoggerProviderILogger介面。適配介面的好處在於,如果想要切換日誌框架,只要實現並註冊新的 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是系統中寫入日誌的統一入口,而 具體日誌實現類 代表了不同的日誌寫入途徑,比如ConsoleLoggerFileLogger等等。

/// 用於執行日誌記錄的類  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源碼一起說才能理解,爭取能夠寫出來吧。