1. 程式人生 > >Asp.NetCore原始碼學習[2-1]:日誌

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

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原始碼一起說才能理解,爭取能夠寫出來吧。

  • ActivityUserGuide
  • DiagnosticSourceUsersGuide