好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

.Net Core日志记录的核心机制

一、前言

回顾: 日志记录之日志配置揭秘  
在 上一篇 中,我们已经了解了内置系统的默认配置和自定义配置的方式,在学习了配置的基础上,我们进一步的对日志在程序中是如何使用的深入了解学习。所以在这一篇中,主要是对日志记录的核心机制进行学习说明。

二、说明

在 上一篇 中,我们留下了两个问题

日志记录的输出可以在哪里查看?而又由什么实现决定的呢?

如何管理输出不同的日志呢?都有哪些方式呢?

第一个问题:在官方的实现有:Console 、Debug 、EventSource 、EventLog 、TraceSource 、Azure App Service,还有一些第三方实现,当然了我们自己也是可以实现的。 是由 ILoggerProvider  接口来决定实现的。

第二个问题:由 log Level、EventId、Logger Provider、Log filtering、Log category、Log scopes 合作解决。

由上面的问题可以发现,我们可以实现多种不同的输出目标方式来实现写日志记录,但是又如何控制在写日志这个操作不变的情况下,实现不同的输入目标,这个时候我们就会想到,可以通过抽象的方式, 将写日志这个操作动作抽象出来,而输出目标依赖这个动作实现具体的操作。所以当我们调用写日志操作方法的时候,由此依次调用对应的具体实现方法,把日志写到具体的目标上。

这个过程具体是怎么实现的呢?我们接着往下看。

三、开始

其实在学习之前,我们应该都已经了解.net core框架有一个重要的特征就是依赖注入,通过在应用启动时候,将各种定义好的实现类型放入到一个集合容器中,通过在运行时,将从集合容器中取出放入对应的类型中。

日志记录的的实现方式也离不开这个。下面让我们一起来看看。

3.1 日志记录器工厂

3.1.1 ILoggerFactory 接口

public interface ILoggerFactory : IDisposable
{
    ILogger CreateLogger(string categoryName);

    void AddProvider(ILoggerProvider provider);
}

ILoggerFactory 是日志记录器的工厂接口类,用于配置日志记录系统并创建Logger实例的类,默认实现两个接口方法为,通过 CreateLogger() 方法来创建 ILogger 实例,(其中参数 categoryName 是一个日志类别,用于调用 Logger 所在类的全名,类别指明日志消息是谁写入的,一般我们将日志所属的的组件、服务或者消息类型名称作为日志类别。) 而 AddProvider() 添加日志记录提供程序,向日志系统注册添加一个 ILoggerProvider 。工厂接口类的默认实现类为 LoggerFactory  , 我们继续往下看:

3.1.2 LoggerFactory 实现

ILoggerFactory  的默认实现是  LoggerFactory  ,在构造函数中,如下:

    public class LoggerFactory : ILoggerFactory
    {
        private static readonly LoggerRuleSelector RuleSelector = new LoggerRuleSelector();

        private readonly Dictionary<string, Logger> _loggers = new Dictionary<string, Logger>(StringComparer.Ordinal);
        
        private readonly List<ProviderRegistration> _providerRegistrations = new List<ProviderRegistration>();
        
        private readonly object _sync = new object();
        
        private volatile bool _disposed;
        
        private IDisposable _changeTokenRegistration;
        
        private LoggerFilterOptions _filterOptions;
        
        private LoggerExternalScopeProvider _scopeProvider;
        
        public LoggerFactory() : this(Enumerable.Empty<ILoggerProvider>())
        {
        }
        
        public LoggerFactory(IEnumerable<ILoggerProvider> providers) : this(providers, new StaticFilterOptionsMonitor(new LoggerFilterOptions()))
        {
        }
        
        public LoggerFactory(IEnumerable<ILoggerProvider> providers, LoggerFilterOptions filterOptions) : this(providers, new StaticFilterOptionsMonitor(filterOptions))
        {
        }
        
        public LoggerFactory(IEnumerable<ILoggerProvider> providers, IOptionsMonitor<LoggerFilterOptions> filterOption)
        {
            foreach (var provider in providers)
            {
                AddProviderRegistration(provider, dispose: false);
            }

            _changeTokenRegistration = filterOption.OnChange(RefreshFilters);
            RefreshFilters(filterOption.CurrentValue);
        }
        
        private void AddProviderRegistration(ILoggerProvider provider, bool dispose)
        {
            _providerRegistrations.Add(new ProviderRegistration
            {
                Provider = provider,
                ShouldDispose = dispose
            });

            if (provider is ISupportExternalScope supportsExternalScope)
            {
                if (_scopeProvider == null)
                {
                    _scopeProvider = new LoggerExternalScopeProvider();
                }

                supportsExternalScope.SetScopeProvider(_scopeProvider);
            }
        }
    }

从 LoggerFactory  中 的构造函数中可以发现,通过注入的方式获取到 ILoggerProvider (这个在下文中会说明),并调用 AddProviderRegistration 方法添加注册程序,将 ILoggerProvider 保存到 ProviderRegistration 集合中。

AddProviderRegistration  方法:

这是一个日志程序提供器,将 ILoggerProvider 保存到 ProviderRegistration 集合中。当日志提供器实现  ISupportExternalScope  接口将单例  LoggerExternalScopeProvider  保存到 provider._scopeProvider 中。

ProviderRegistration 集合:

private struct ProviderRegistration
{
   public ILoggerProvider Provider;
   public bool ShouldDispose;
}

其中的  ShouldDispose  字段标识在在 LoggerFactory 生命周期结束之后,该 ILoggerProvider 是否需要释放。虽然在系统中 LoggerFactory 为单例模式,但是其提供了一个静态方法生成一个可释放的 DisposingLoggerFactory 。

在 LoggerFactory  实现默认的接口方法 CreateLogger() , AddProvider()

查看源码如下:

CreateLogger

创建 ILogger 实例, CreateLogger()  源码如下:

    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 struct ProviderRegistration
        {
            public ILoggerProvider Provider;
            public bool ShouldDispose;
        }
        
        public ILogger CreateLogger(string categoryName)
        {
            if (CheckDisposed())
            {
                throw new ObjectDisposedException(nameof(LoggerFactory));
            }
            lock (_sync)
            {
                if (!_loggers.TryGetValue(categoryName, out var logger))
                {
                    logger = new Logger
                    {
                        Loggers = CreateLoggers(categoryName),
                    };
                    (logger.MessageLoggers, logger.ScopeLoggers) = ApplyFilters(logger.Loggers);

                    _loggers[categoryName] = logger;
                }
                return logger;
            }
        }
        
        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;
        }
    }

从源码可以看出, CreateLogger 方法中,会检测资源是否被释放,在方法中,根据内部定义的字典集合 Dictionary<string, Logger> _loggers ,判断字典中是否存在对应的 Logger 属性对象,如果不存在,会调用 CreateLoggers 方法根据之前注册的的所有 ILoggerProvider  所创建出来  ProviderRegistration  集合来实现创建 Logger 属性集合(根据日志类别生成了对应实际的日志写入类 FileLogger 、 ConsoleLogger 等),并通过字典集合的方式保存 categoryName 和对应的 Logger 。

创建  Logger  需要的  LoggerInformation[]

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; }
    
    public Type ProviderType { get; }
    
    public bool ExternalScope { get; }
}

根据注册的 ILoggerProvider ,创建 ILogger  其中的字段说明:

Logger :具体日志类别写入途径实现类

Category : 日志类别名称

ProviderType : 日志提供器Type

ExternalScope :是否支持 ExternalScope

继续看 CreateLogger 方法,在创建 Logger 之后,还调用了 ApplyFilters 方法:

        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)
            {
                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));

                if (!loggerInformation.ExternalScope)
                {
                    scopeLoggers?.Add(new ScopeLogger(logger: loggerInformation.Logger, externalScopeProvider: null));
                }
            }

            if (_scopeProvider != null)
            {
                scopeLoggers?.Add(new ScopeLogger(logger: null, externalScopeProvider: _scopeProvider));
            }

            return (messageLoggers.ToArray(), scopeLoggers?.ToArray());
        }

由源码可以看出,

MessageLogger[]  集合取值:

在获取 LoggerInformation[] 后进行传参,进行遍历,根据 RuleSelector 过滤器,从配置文件中读取对应的日志级别,过滤器会返回获取最低级别和对应的一条过滤规则,如果配置文件中没有对应的配置,默认取全局最低级别(MinLevel),如果读取到的日志级别大于 LogLevel.Critical ,则将其加入 MessageLogger[] 。

过滤器的规则:

选择当前记录器类型的规则,如果没有,请选择未指定记录器类型的规则

选择最长匹配类别的规则

如果没有与类别匹配的内容,则采用所有没有类别的规则

如果只有一条规则,则使用它的级别和过滤器

如果有多个规则,请选择使用最后一条。

如果没有适用的规则,请使用全局最低级别

通过 MessageLogger[] 添加消息日志集合

internal readonly struct MessageLogger
{
    public MessageLogger(ILogger logger, string category, string providerTypeFullName, LogLevel? minLevel, Func<string, string, LogLevel, bool> filter)
    {
        Logger = logger;
        Category = category;
        ProviderTypeFullName = providerTypeFullName;
        MinLevel = minLevel;
        Filter = filter;
    }

    public ILogger Logger { get; }

    public string Category { get; }

    private string ProviderTypeFullName { get; }

    public LogLevel? MinLevel { get; }

    public Func<string, string, LogLevel, bool> Filter { get; }

    public bool IsEnabled(LogLevel level)
    {
        if (MinLevel != null && level < MinLevel)
        {
            return false;
        }

        if (Filter != null)
        {
            return Filter(ProviderTypeFullName, Category, level);
        }

        return true;
    }
}

internal readonly struct ScopeLogger
{
    public ScopeLogger(ILogger logger, IExternalScopeProvider externalScopeProvider)
    {
        Logger = logger;
        ExternalScopeProvider = externalScopeProvider;
    }

    public ILogger Logger { get; }

    public IExternalScopeProvider ExternalScopeProvider { get; }

    public IDisposable CreateScope<TState>(TState state)
    {
        if (ExternalScopeProvider != null)
        {
            return ExternalScopeProvider.Push(state);
        }
        return Logger.BeginScope<TState>(state);
    }
}

在 MessageLogger[] 中带有 MinLevel 属性和 Filter 委托两种过滤配置,而这两种配置的来源,在上一章中可以看到,分别是从配置文件(AddConfiguration)和直接使用委托(AddFilter)来进行配置的。

再由上面的 IsEnabled 方法可以看出,会先使用  MinLevel  过滤,再使用  Filter  进行过滤。所以这两者存在优先级。

ScopeLogger[ ]  取值 :

如果  ILoggerProvider 实现了 ISupportExternalScope 接口,那么使用 LoggerExternalScopeProvider 作为 Scope 功能的实现。反之,使用 ILogger 作为其 Scope 功能的实现。

LoggerExternalScopeProvider  :

通过  Scope  组成了一个单向链表,每次  beginscope  向链表末端增加一个新的元素, Dispose 的时候,删除链表最末端的元素。我们知道 LoggerExternalScopeProvider  在系统中是单例模式,多个请求进来,加入线程池处理。通过使用 AsyncLoca 来实现不同线程间数据独立。 有两个地方开启了日志作用域: 1、通过 socket 监听到请求后,将 KestrelConnection 加入线程池,线程池调度执行 IThreadPoolWorkItem.Execute() 方法。在这里开启了一次 2、在构建请求上下文对象的时候( HostingApplication.CreateContext() ),开启了一次

由上源码可以得出:在工厂记录器类中,通过系统 依赖注入 的方式解析所有注册的 ILoggerProvider ,然后调用其中的 CreateLogger 方法实现创建一个 Logger 实例对象,而这个 Logger 实例对象会根据根据注册的 ILoggerProvider 创建需要的  LoggerInformation[] ,并将此对象作为参数进行 ApplyFilters 过滤器筛选,得到对应的最低等级或过滤规则,最后通过调用 Log 方法日志记录的时候,会遍历 MessageLogger[] 集合,根据 logger 日志类别对应实际不同的日志写入类,调用 ILoggerProvider 具体实现类 (可以看下文说明) 中的 Log 方法。

AddProviderRegistration→CreateLoggers→LoggerInformation[]→ApplyFilters→MessageLogger[]→Log→ILoggerProvider ( 执行具体类中的Log方法 )

ILoggerFactory   来源 :

在 上一篇 中我们在对日志配置进行说明的时候,应用程序在启动初始化的时候会通过注入的方式 CreateDefaultBuilder → ConfigureLogging → AddLogging

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

实现将把 ILoggerFactory 对象以依赖注入的方式托管到集合容器中,为程序调用提供使用。

3.2日志记录提供器

3.2.1 ILoggerProvider 接口

创建 ILogger 实例的类型,根据日志类别名称创建一个新的 ILogger 实例

public interface ILoggerProvider : IDisposable
{
    ILogger CreateLogger(string categoryName);
}

这个是具体的日志写入类,在工厂记录器中我们已经提到了这个,在 LoggerInformation[] 中会根据日志类别注册对应的 ILoggerProvider ,在系统中我们就可以通过 ILogger 同时向多个途经写入日志信息。(这也是对上一篇中留下的问题进行再次说明)

ILoogerProvider 继承了 IDisposable 接口,如果某个具体的 ILoggerProvider 对象需要释放资源,就可以将相关的操作实现在 Dispose 方法中。

默认的实现方式为多个,官方实现的由 ConsoleLoggerProvider  、 DebugLoggerProvider  、 EventSourceLoggerProvider 、 EventLogLoggerProvider  、 TraceSourceLoggerProvider

以 ConsoleLoggerProvider 为列

    [ProviderAlias("Console")]
    public class ConsoleLoggerProvider : ILoggerProvider, ISupportExternalScope
    {
        private readonly IOptionsMonitor<ConsoleLoggerOptions> _options;
        private readonly ConcurrentDictionary<string, ConsoleLogger> _loggers;
        private readonly ConsoleLoggerProcessor _messageQueue;

        private IDisposable _optionsReloadToken;
        private IExternalScopeProvider _scopeProvider = NullExternalScopeProvider.Instance;
 
        public ConsoleLoggerProvider(IOptionsMonitor<ConsoleLoggerOptions> options)
        {
            _options = options;
            _loggers = new ConcurrentDictionary<string, ConsoleLogger>();

            ReloadLoggerOptions(options.CurrentValue);
            _optionsReloadToken = _options.OnChange(ReloadLoggerOptions);

            _messageQueue = new ConsoleLoggerProcessor();
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                _messageQueue.Console = new WindowsLogConsole();
                _messageQueue.ErrorConsole = new WindowsLogConsole(stdErr: true);
            }
            else
            {
                _messageQueue.Console = new AnsiLogConsole(new AnsiSystemConsole());
                _messageQueue.ErrorConsole = new AnsiLogConsole(new AnsiSystemConsole(stdErr: true));
            }
        }
        private void ReloadLoggerOptions(ConsoleLoggerOptions options)
        {
            foreach (var logger in _loggers)
            {
                logger.Value.Options = options;
            }
        }
        public ILogger CreateLogger(string name)
        {
            return _loggers.GetOrAdd(name, loggerName => new ConsoleLogger(name, _messageQueue)
            {
                Options = _options.CurrentValue,
                ScopeProvider = _scopeProvider
            });
        } 
        public void Dispose()
        {
            _optionsReloadToken?.Dispose();
            _messageQueue.Dispose();
        }
 
        public void SetScopeProvider(IExternalScopeProvider scopeProvider)
        {
            _scopeProvider = scopeProvider;

            foreach (var logger in _loggers)
            {
                logger.Value.ScopeProvider = _scopeProvider;
            }

        }
    }

在 ConsoleLoggerProvider 类型定义中,标注了 ProviderAliasAttribute 特性,并设置别名为 Console ,所以在配置过滤规则的时候,可以直接使用这个名称。 ILogger 的创建实现了具体日志类 ConsoleLogger 。 

3.3 日志记录器

3.3.1 ILogger 接口

表示用于执行日志记录的类型,是系统中写入日志的统一入口。

public interface ILogger
{ 
    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);
}

定义了三个方法, Log<TState>()  用于写入日志, IsEnabled() 用于检查判断日志级别是否开启, BeginScope()  用于指日志作用域。

3.3.2 Logger 实现

ILogger 执行记录接口类的具体实现 Logger 如下:

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);
            }
        }
    }
    public bool IsEnabled(LogLevel logLevel)
    {
        var loggers = MessageLoggers;
        if (loggers == null)
        {
            return false;
        }

        List<Exception> exceptions = null;
        var i = 0;
        for (; i < loggers.Length; i++)
        {
            ref readonly var loggerInfo = ref loggers[i];
            if (!loggerInfo.IsEnabled(logLevel))
            {
                continue;
            }

            if (LoggerIsEnabled(logLevel, loggerInfo.Logger, ref exceptions))
            {
                break;
            }
        }

        if (exceptions != null && exceptions.Count > 0)
        {
            ThrowLoggingError(exceptions);
        }

        return i < loggers.Length ? true : false;

        static bool LoggerIsEnabled(LogLevel logLevel, ILogger logger, ref List<Exception> exceptions)
        {
            try
            {
                if (logger.IsEnabled(logLevel))
                {
                    return true;
                }
            }
            catch (Exception ex)
            {
                if (exceptions == null)
                {
                    exceptions = new List<Exception>();
                }

                exceptions.Add(ex);
            }

            return false;
        }
    }
}

源码中 MessageLogger[] 在上文已经提到了,其中保存了在配置中启用的那些对应的 ILogger 。

需要注意的是,由于配置文件更改后,会调用 ApplyFilters() 方法,并为 MessageLogger[] 赋新值,所以在遍历之前,需要保存当前值,再进行处理。否则会出现修改异常。

在系统中统一写入日志的入口,通过日志等级作为参数调用其 IsEnabled 方法来确定当前日志是否执行对应具体日志的实现类,当符合条件执行具体日志输出到对应的写入途径中会调用对应的 Log 方法(需要提供一个 EventId 来标识当前日志事件)

ILogger 默认的实现方式为多个,官方实现的由 ConsoleLogger  、 DebugLogger  、 EventSourceLogger 、 EventLogLogger 、 TraceSourceLogger 具体日志实现类代表不同的日志写入途径。

四、总结 在 ILoggerFactory 和 ILoggerProvider 中都会通过方法创建 ILogger 对象,但两者是不相同的。在工厂默认实现 LoggerFactory 类型中它创建的 ILogger 对象是由注册到 LoggerFactory 对象上的所有ILoggerProvider对象提供一组  ILogger 对象组合而成。而日志提供器 ILoggerProvider 创建的 ILogger 是日志实现输出到对应的渠道目标,写入日志。 日志记录器 ILogger 中的 Log() 方法会记录执行日志,在日志记录器工厂 ILoggerFactory 和日志记录提供器 ILoggerProvider 中两种不同的 ILogger 实现对应的 Log() 方法实现的意思也是不同的。在 ILoggerFactory 产生的是 ILogger 类型(也就是我们最终使用的 Logger ),其 Log() 方法是依次调用 Logger 中包含的 LoggerInformation[] 数组中的 ILogger 。而 ILoggerProvider 产生的为各类不同的XxxLogger(也就是上面说的 Logger 中的 LoggerInformation 数组包含的如ConsoleLogger、 DebugLogger ),其 Log() 方法是把日志写到具体的目标上去。 由上文可以发现,在asp.net core提供的日志记录的组件,通过工厂的一种方式,将日志记录器和日志记录提供器都放入到工厂这样的容器中,满足定义多个不同的记录方式。在后续我们可以通过自定义 ILoggerProvider 集成到 Logger 中,实现自己需要的日志记录输出方式。 如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。 官方 源码  和  参考资料

到此这篇关于.Net Core日志记录核心机制的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持。

查看更多关于.Net Core日志记录的核心机制的详细内容...

  阅读:39次