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