Asp.NetCore原始碼學習[2-1]:日誌
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
原始碼一起說才能理解,爭取能夠寫出來吧。
- ActivityUserGuide
- DiagnosticSourceUsersGuide