Asp.NetCore源碼學習[2-1]:日志
在一個系統中,日志是不可或缺的部分。對於.net而言有許多成熟的日志框架,包括
Log4Net
、NLog
、Serilog
等等。你可以在系統中直接使用這些第三方的日志框架,也可以通過這些框架去適配ILoggerProvider
和ILogger
接口。適配接口的好處在於,如果想要切換日志框架,只要實現並注冊新的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
是系統中寫入日志的統一入口,而 具體日志實現類 代表了不同的日志寫入途徑,比如ConsoleLogger
、FileLogger
等等。
/// 用於執行日志記錄的類
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
源碼一起說才能理解,爭取能夠寫出來吧。