紀錄檔記錄是什麼?簡單而言,就是通過一些方式記錄應用程式執行中的某一時刻的狀態,保留應用程式當時的資訊。這對於我們進行應用程式的分析、審計以及維護有很大的作用。
作為程式設計師,我們恐怕誰也不敢保證我們開發的軟體應用一定不存在BUG,一定不會出現故障,而當故障出現的時候,紀錄檔就是我們排查故障的首要依據,排查故障的第一步一定是檢視故障發生時的紀錄檔資訊。
當然,紀錄檔也不僅僅只是在排查故障的時候有用,這類稱為錯誤紀錄檔,比較常談的還有安全紀錄檔、審計紀錄檔等等,它根據應用場景、企業團隊對其認知和需要有不同的應用。紀錄檔記錄在軟體工程中更是一種思想,而不止是一種開發技術實現,它被認為是產品團隊對其產品需求沒有特別要求的非功能性使用場景,在軟體框架、開發實現中基本是一種必備的橫切功能點,現在的各種開發語言、框架中基本都具備紀錄檔記錄的實現。
.NET Core 框架中內建了紀錄檔記錄系統,支援通過統一的 API 進入紀錄檔的記錄,並且支援通過設定各種紀錄檔提供程式以各種不同的方式儲存紀錄檔資訊,不僅有多種內建的紀錄檔提供程式,也相容各種按照標準規範實現的第三方框架。以下演示程式碼基於 .NET 7 。
當我們通過 VS 應用模板建立一個 ASP.NET Core 的應用時,預設將紀錄檔記錄系統新增到應用中,內部實際上時在建立 HostApplicationBuilder
的過程中,通過 AddLogging()
註冊了紀錄檔相關的服務,並設定了 Console
、Debug
、EventSource
和 EventLog
(僅Windows)共四種紀錄檔記錄提供程式。
var builder = WebApplication.CreateBuilder(args);
除此之外,我們也可以引入 Microsoft.Extensions.Hosting
包,自行通過通用主機建立應用,以下程式碼中也預設新增了紀錄檔記錄系統:
var host = Host.CreateDefaultBuilder().Build();
通過檢視原始碼,可以看到:
這兩種方式最終都是通過 HostingHostBuilderExtensions
中的 AddDefaultServices()
方法向容器中注入紀錄檔相關的服務,並根據不同平臺設定了不同的紀錄檔提供程式。
除了預設的幾種提供程式,我們也可以通過以下兩種方式根據自己的實際需要,新增其他的紀錄檔提供程式,如比較常用的檔案記錄提供程式將紀錄檔輸出到文字檔案中,或者清除預設的提供程式進行自定義。在.NET 6、.NET 7 中,微軟推進用第二種方式替代第一種方式。
builder.Host.ConfigureLogging(logging =>
{
// 清除已經注入的紀錄檔提供程式
logging.ClearProviders();
logging.AddConsole();
});
// 清除已經注入的紀錄檔提供程式
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddJsonConsole();
關於紀錄檔記錄提供程式,下面再進行詳細的介紹。
將紀錄檔記錄系統整合到應用之後,使用方式就非常簡單了。.NET Core 紀錄檔記錄系統提供統一的 API 進行紀錄檔的記錄,無論底層使用的是什麼紀錄檔提供程式,無論最終是將紀錄檔記錄在哪裡。
我們可以通過依賴注入,在需要記錄紀錄檔的類中從容器中解析出 ILogger<TCategoryName>
這樣一個紀錄檔記錄器範例。紀錄檔記錄器在建立的時候需要指定紀錄檔類別,它會與該記錄器的記錄的每一條紀錄檔關聯,方便我們在眾多的紀錄檔資訊中查詢特定的紀錄檔。ILogger<TCategoryName>
中的泛型會作為該記錄器的紀錄檔類別,按照 .NET 體系下不成文的約定,一般情況下使用注入記錄器的類作為泛型型別,最終在紀錄檔資訊中會以該類的全類名作為紀錄檔類別。
[ApiController]
[Route("[controller]/[action]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
}
我們也可以顯式指定紀錄檔類別,紀錄檔類別實質就是一個字串,這時我們可以注入 ILoggerFactory
範例,之後通過 ILoggerFactory.CreateLogger
方法自行建立記錄器。
[ApiController]
[Route("[controller]/[action]")]
public class WeatherForecastController : ControllerBase
{
private ILogger _logger;
public WeatherForecastController(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger("MyLogger");
}
}
之後,就是使用記錄器在我們需要的位置記錄紀錄檔資訊,記錄器提供了豐富的 API 方便我們記錄各種紀錄檔:
[ApiController]
[Route("[controller]/[action]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public Task Get()
{
// 各種紀錄檔API對應各種紀錄檔級別
// 斷點
_logger.LogTrace("這是一個斷點紀錄檔");
//偵錯
_logger.LogDebug("this is a debug.");
//資訊
_logger.LogInformation("this is an info.");
//警告
_logger.LogWarning("this is a warning.");
//錯誤
_logger.LogError("this is an error.");
//當機
_logger.LogCritical("this is Critical");
// 多個過載方法,支援字串預留位置
_logger.LogInformation("this is an info {date} {level}.", DateTime.Now, 1);
// 支援傳入異常,記錄異常資訊
_logger.LogError(new Exception(), "this is an error.");
// 指定紀錄檔ID,方便同類異常的篩選, 常用的紀錄檔id可以定義為常數
_logger.LogError(1001, new Exception(), "this is an error with eventId.");
// 自行指定紀錄檔級別
_logger.Log(LogLevel.Information, "loging an info.");
return Task.CompletedTask;
}
}
當呼叫該介面的時候,可以看到控制檯中輸出了我們記錄的紀錄檔內容:
這裡沒有輸出 Trace
和 Debug
紀錄檔,是因為預設設定中輸出的最低紀錄檔級別是 Information
,要使 Trace
和 Debug
這兩類紀錄檔可以正常輸出,需要我們進行設定。
上面的程式碼中講到,記錄紀錄檔時我們可以指定當前紀錄檔資訊的級別,紀錄檔級別表示紀錄檔的嚴重程度,.NET Core 框架紀錄檔系統中紀錄檔級別如下,一共分為7個等級,從輕到重為(最後的None較為特殊):
紀錄檔級別 | 值 | 描述 |
---|---|---|
Trace | 0 | 追蹤級別,包含最詳細的資訊。這些資訊可能包含敏感資料,預設情況下是禁用的,並且絕不能出現在生產環境中。 |
Debug | 1 | 偵錯級別,用於開發人員開發和偵錯。資訊量一般比較大,在生產環境中一定要慎用。 |
Information | 2 | 資訊級別,該級別平時使用較多。 |
Warning | 3 | 警告級別,一些意外的事件,但這些事件並不對導致程式出錯。 |
Error | 4 | 錯誤級別,一些無法處理的錯誤或異常,這些事件會導致當前操作或請求失敗,但不會導致整個應用出錯。 |
Critical | 5 | 致命錯誤級別,這些錯誤會導致整個應用出錯。例如記憶體不足等。 |
None | 6 | 指示不記錄任何紀錄檔 |
我們可以在記錄紀錄檔的時候指定紀錄檔的級別,但是並不是我們記錄的任何一個級別的紀錄檔都會輸出儲存,還得配合紀錄檔記錄系統的設定,就像上面的例子中,最開始 Debug
和 Trace
級別的紀錄檔是不輸出的。
紀錄檔記錄設定通常通過組態檔進行設定,在 appsettings.json
檔案有關於紀錄檔設定的相關節點 Logging,在我們通過 ASP.NET Core 應用模板建立專案時,就會自動生成:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
在組態檔中,我們可以通過 LogLevel
節點對紀錄檔記錄系統全域性輸出的紀錄檔最低階別進行設定,紀錄檔記錄系統最終會輸出大於等於我們設定的級別的紀錄檔資訊,而其他資訊則不會輸出。
可以看到,在 LogLevel
節點下還有一些欄位,通過這些欄位我們還可以進行更具體的設定。其中「Default」
欄位顧名思義就是預設設定,如上面設定中設定了紀錄檔系統預設輸出的最低階別紀錄檔為 Information
,沒有進行過特殊設定的紀錄檔記錄器全部按照這一個設定進行輸出。
我們還可以針對某一特定的紀錄檔記錄器進行專門的設定,通過紀錄檔記錄器建立時傳入的名稱進行篩選,支援模糊匹配(字串 StartWith 判斷),如上面設定中的 「Microsoft.AspNetCore」
,這個欄位一看就是名稱空間書寫方式,也就是說全類名以該欄位開始的紀錄檔記錄器記錄的紀錄檔按照這個設定設定的最低紀錄檔記錄進行記錄。如果還有更加具體的設定,如「Microsoft.AspNetCore.Mvc」
,一個紀錄檔記錄器名稱同時匹配 「Microsoft.AspNetCore」
和「Microsoft.AspNetCore.Mvc」
,則以 「Microsoft.AspNetCore.Mvc」
的設定為準,因為 「Microsoft.AspNetCore.Mvc」
更具體。
這也是為什麼約定使用 ILogger<TCategoryName>
介面注入紀錄檔記錄器的原因,這種方式下我們可以通過有規律的名稱空間快速設定篩選最終需要輸出儲存的紀錄檔資訊。當然,如果自定義的紀錄檔記錄器名稱字串比較有規律,那也沒有問題。
在日常的應用開發中,往往我們都會使用不止一種方式記錄紀錄檔,通常會同時整合多個紀錄檔記錄提供程式,LogLevel
節點是針對所有紀錄檔記錄提供程式的統一設定,它適用於所有沒有進行單獨設定的紀錄檔記錄提供程式(Windows EventLog 除外。EventLog 必須顯式地進行設定,否則會使用其預設的 LogLevel.Warning)。當然,我們也可以針對不同的紀錄檔提供程式進行單獨的設定。如:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
},
"Console": {
"LogLevel": {
"Default": "Error"
}
},
"Debug": {
"LogLevel": {
"Microsoft": "None"
}
},
"EventSource": {
"LogLevel": {
"Default": "Trace",
"Microsoft": "Trace",
"Microsoft.Hosting.Lifetime": "Trace"
}
}
}
}
就像 appsettings.{Environment}.json
和 appsettings.json
之間的關係一樣,Logging.{Provider}.LogLevel
中的設定將會覆蓋 Logging.LogLevel
中的設定。例如 Logging.Console.LogLevel.Default
將會覆蓋 Logging.LogLevel.Default
,Console
紀錄檔記錄器將預設記錄 Error
及其以上級別的紀錄檔。
剛才提到了,Windows EventLog
比較特殊,它不會繼承 Logging.LogLevel
的設定。EventLog 預設紀錄檔級別為 LogLevel.Warning
,如果想要修改,則必須顯式進行指定。
以上講到的紀錄檔設定方式,都是通過 appsettings.json
設定的,實際上 .NET Core 框架下設定來源不僅僅是 appsettings.json
檔案,只不過它是最常用的,這一塊的內容在之前的設定系統的文章中已經詳細講過了。我們也可以通過其他的設定來源進行紀錄檔相關的設定,例如命令列、環境變數等。
除了通過設定進行紀錄檔記錄系統的設定之外,我們還可以在程式碼中通過 AddFilter
方法顯式地設定紀錄檔系統的相關行為設定,該方法有多個過載,如:
var builder = WebApplication.CreateBuilder(args);
// 相當於 Logging:LogLevel:Default:Information
builder.Logging.AddFilter(logging => logging >= LogLevel.Information);
// 相當於 Logging:LogLevel:Microsoft:Warning
builder.Logging.AddFilter("Microsoft", LogLevel.Warning);
// 相當於 Logging:Console:LogLevel:Microsoft:Information
builder.Logging.AddFilter<ConsoleLoggerProvider>("Microsoft", LogLevel.Information);
// 也可以更加靈活地通過篩選器設定紀錄檔記錄規則
builder.Logging.AddFilter((provider, category, logLevel) =>
{
if (provider.Contains("ConsoleLoggerProvider")
&& category.Contains("Controller")
&& logLevel >= LogLevel.Information)
{
return true;
}
else if (provider.Contains("ConsoleLoggerProvider")
&& category.Contains("Microsoft")
&& logLevel >= LogLevel.Information)
{
return true;
}
else
{
return false;
}
});
// 設定全域性的紀錄檔輸出最小級別,紀錄檔記錄系統預設最低階別是 Information
builder.Logging.SetMinimumLevel(LogLevel.Debug);
這種方式相對於設定會比較固化,不利於動態調整,一般來說,紀錄檔記錄的相關設定還是組態檔中設定,所以這裡就簡單地講一下,大家知道有這種方式就行了。
當建立 ILogger<TCategoryName>
的物件範例時,ILoggerFactory
根據不同的紀錄檔記錄提供程式,將會:
如上面講到的,我們可以使用 SetMinimumLevel
方法設定 MinimumLevel
。
對於到的 .NET Core 中的原始碼是這一段:
在建立 ILoggerFactory
範例、建立 ILogger
範例和設定重新整理的時候,都會對每一個提供程式的設定規則根據優先順序進行篩選,只有最小級別不為 None
,才會建立最終的紀錄檔記錄書寫器,否則甚至不會有書寫器。
而在具體的規則過濾邏輯中,可以看到微軟的註釋:
internal static class LoggerRuleSelector
{
public static void Select(LoggerFilterOptions options, Type providerType, string category, out LogLevel? minLevel, out Func<string?, string?, LogLevel, bool>? filter)
{
filter = null;
minLevel = options.MinLevel;
// Filter rule selection:
// 1. Select rules for current logger type, if there is none, select ones without logger type specified
// 2. Select rules with longest matching categories
// 3. If there nothing matched by category take all rules without category
// 3. If there is only one rule use it's level and filter
// 4. If there are multiple rules use last
// 5. If there are no applicable rules use global minimal level
string? providerAlias = ProviderAliasUtilities.GetAlias(providerType);
LoggerFilterRule? current = null;
foreach (LoggerFilterRule rule in options.RulesInternal)
{
if (IsBetter(rule, current, providerType.FullName, category)
|| (!string.IsNullOrEmpty(providerAlias) && IsBetter(rule, current, providerAlias, category)))
{
current = rule;
}
}
if (current != null)
{
filter = current.Filter;
minLevel = current.LogLevel;
}
}
private static bool IsBetter(LoggerFilterRule rule, LoggerFilterRule? current, string? logger, string category)
{
// Skip rules with inapplicable type or category
// 別名或者全類名與當前紀錄檔提供程式對不上的則跳過
if (rule.ProviderName != null && rule.ProviderName != logger)
{
return false;
}
// 對紀錄檔類別進行判斷,這裡會同時判斷通用的設定和針對特定紀錄檔提供程式的設定
// 也就是說某個類別,如果通用的LogLevel中設定了,如果特定的紀錄檔特工程式中沒有重新設定覆蓋,則會使用通用設定
// 支援萬用字元 * ,但 * 只能有一個
string? categoryName = rule.CategoryName;
if (categoryName != null)
{
const char WildcardChar = '*';
int wildcardIndex = categoryName.IndexOf(WildcardChar);
if (wildcardIndex != -1 &&
categoryName.IndexOf(WildcardChar, wildcardIndex + 1) != -1)
{
throw new InvalidOperationException(SR.MoreThanOneWildcard);
}
ReadOnlySpan<char> prefix, suffix;
if (wildcardIndex == -1)
{
prefix = categoryName.AsSpan();
suffix = default;
}
else
{
prefix = categoryName.AsSpan(0, wildcardIndex);
suffix = categoryName.AsSpan(wildcardIndex + 1);
}
if (!category.AsSpan().StartsWith(prefix, StringComparison.OrdinalIgnoreCase) ||
!category.AsSpan().EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
// 如果相同的類別,則以特定提供程式的設定優先
if (current?.ProviderName != null)
{
if (rule.ProviderName == null)
{
return false;
}
}
else
{
// We want to skip category check when going from no provider to having provider
if (rule.ProviderName != null)
{
return true;
}
}
// 特定的類別優先於預設的 Default 類別
if (current?.CategoryName != null)
{
if (rule.CategoryName == null)
{
return false;
}
// 類別名稱更詳細的優先
if (current.CategoryName.Length > rule.CategoryName.Length)
{
return false;
}
}
return true;
}
}
而 LoggerFilterOptions
中的規則是怎麼來的呢?在通過主機構建應用的時候會通過組態檔載入相關的設定,並將設定轉化為規則
最終,組態檔中的每一個紀錄檔類別的設定都會結合紀錄檔提供程式轉化為一項規則,預設的 LogLevel
中的設定轉換成的規則中 ProviderName
為 null
,預設的 Default
類別對於的規則 CategoryName
為 null
。
internal sealed class LoggerFilterConfigureOptions : IConfigureOptions<LoggerFilterOptions>
{
private const string LogLevelKey = "LogLevel";
private const string DefaultCategory = "Default";
private readonly IConfiguration _configuration;
public LoggerFilterConfigureOptions(IConfiguration configuration)
{
_configuration = configuration;
}
public void Configure(LoggerFilterOptions options)
{
LoadDefaultConfigValues(options);
}
private void LoadDefaultConfigValues(LoggerFilterOptions options)
{
if (_configuration == null)
{
return;
}
options.CaptureScopes = GetCaptureScopesValue(options);
foreach (IConfigurationSection configurationSection in _configuration.GetChildren())
{
if (configurationSection.Key.Equals(LogLevelKey, StringComparison.OrdinalIgnoreCase))
{
// Load global category defaults
LoadRules(options, configurationSection, null);
}
else
{
IConfigurationSection logLevelSection = configurationSection.GetSection(LogLevelKey);
if (logLevelSection != null)
{
// Load logger specific rules
string logger = configurationSection.Key;
LoadRules(options, logLevelSection, logger);
}
}
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
Justification = "IConfiguration.GetValue is safe when T is a bool.")]
bool GetCaptureScopesValue(LoggerFilterOptions options) => _configuration.GetValue(nameof(options.CaptureScopes), options.CaptureScopes);
}
private static void LoadRules(LoggerFilterOptions options, IConfigurationSection configurationSection, string? logger)
{
foreach (System.Collections.Generic.KeyValuePair<string, string?> section in configurationSection.AsEnumerable(true))
{
if (TryGetSwitch(section.Value, out LogLevel level))
{
string? category = section.Key;
if (category.Equals(DefaultCategory, StringComparison.OrdinalIgnoreCase))
{
category = null;
}
var newRule = new LoggerFilterRule(logger, category, level, null);
options.Rules.Add(newRule);
}
}
}
private static bool TryGetSwitch(string? value, out LogLevel level)
{
if (string.IsNullOrEmpty(value))
{
level = LogLevel.None;
return false;
}
else if (Enum.TryParse(value, true, out level))
{
return true;
}
else
{
throw new InvalidOperationException(SR.Format(SR.ValueNotSupported, value));
}
}
}
而這些是對設定中的規則的處理,最終得到的是 miniLevel
,每次寫紀錄檔的時候會先將當前紀錄檔資訊的級別和設定的最低階別進行比較,如果我們還有在程式碼中通過 AddFilter
擴充套件方法增加的額外的規則的化,會在設定規則過濾完成之後再過濾(也就是說,Filter 中是不會有低於設定的級別的紀錄檔的),如果都不通過,則不會轉到最終的記錄器。
有些時候,我們可能希望某一些紀錄檔集中在一起顯示,或者在進行一些強關聯的邏輯操作時,希望記錄的紀錄檔中保留有關聯資訊,這時候就可以使用紀錄檔作用域。紀錄檔作用域依賴於特定的紀錄檔記錄提供程式的支援,並不是所有的提供程式都支援,內建的提供程式中 Console、AzureAppServicesFile 和 AzureAppServicesBlob 提供了相應的支援。可以通過以下的方式啟用紀錄檔作用域:
(1) 通過紀錄檔記錄器的 BeginScope 建立作用域,並使用 using 塊包裝。
[ApiController]
[Route("[controller]/[action]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public Task Get()
{
// 建立一個紀錄檔域,以下紀錄檔會被當作一個整體
using (_logger.BeginScope("this is a log scope"))
{
// 除了使用特定級別的API,也可以使用Log方法,動態指定級別
_logger.Log(LogLevel.Information, "logging a scope info.");
_logger.Log(LogLevel.Warning, "logging a scope warning.");
}
return Task.CompletedTask;
}
}
(2) 在設定中針對紀錄檔提供程式新增 "IncludeScopes: true" 設定
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft.AspNetCore": "Warning"
},
"Console": {
"IncludeScopes": true,
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
}
從最終的輸出中可以看到,同一個作用域中記錄的紀錄檔都帶上了建立作用域時設定的標記。同時也可以看到,記錄的紀錄檔中多了 SpanId、TraceId、ParentId 這些內容,這是紀錄檔記錄系統隱式建立範圍物件,這些資訊源自於每一次的Http 請求,方便對一次 Http 請求中各個步驟的跟蹤。對於這些資訊的設定,可以通過 ActivityTrackingOptions
設定。
var builder = WebApplication.CreateBuilder(args);
builder.Logging.Configure(option =>
{
option.ActivityTrackingOptions = ActivityTrackingOptions.SpanId | ActivityTrackingOptions.TraceId;
});
以下是一些注意點:
Startup.Configure
方法中記錄紀錄檔,直接在引數上注入 ILogger<Startup>
即可。Startup.ConfigureServices
方法中使用 ILogger
,因為此時 DI 容器還未設定完成。參考文章:
.NET Core 和 ASP.NET Core 中的紀錄檔記錄 | Microsoft Learn
理解ASP.NET Core - 紀錄檔(Logging) - xiaoxiaotank - 部落格園 (cnblogs.com)
ASP.NET Core 系列:
目錄:ASP.NET Core 系列總結
上一篇:ASP.NET Core - 快取之分散式快取
下一篇:[ASP.NET Core - 紀錄檔記錄系統(二)]