Asp.NetCore源码学习[2-1]:日志

Asp.NetCore源码学习[2-1]:日志

在一个系统中,日志是不可或缺的部分。对于.net而言有许多成熟的日志框架,包括Log4NetNLogSerilog 等等。你能够在系统中直接使用这些第三方的日志框架,也能够经过这些框架去适配ILoggerProviderILogger接口。适配接口的好处在于,若是想要切换日志框架,只要实现并注册新的 ILoggerProvider 就能够,而不影响日志使用方的代码。这就是在日志系统中使用门面模式的优势。html

本系列源码地址

1、.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"};
    }
}

2、源码解读

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"
      }
    }
  }

一、 日志相关的接口

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等等。asp.net

public interface ILoggerProvider : IDisposable
{
    /// 建立一个新的ILogger实例(具体日志写入类)
    ILogger CreateLogger(string categoryName);
}

1.3 ILogger 接口

虽然Logger和具体日志实现类都实现ILogger接口,可是它们的做用是彻底不一样的。其二者的区别在于:Logger是系统中写入日志的统一入口,而 具体日志实现类 表明了不一样的日志写入途径,好比ConsoleLoggerFileLogger等等。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 日志工厂类的实现

在构造函数中作了两件事情:

  • 获取在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 的详细特性能够参照此处
  • 有两个地方开启了日志做用域:
  • 一、经过 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

相关文章
相关标签/搜索