在 .NET 6 及更新版本中處理高效能日誌方案時,請使用 LoggerMessageAttribute 並結合編譯時原始碼生成。 此方法透過消除盒裝、臨時配置及執行時訊息模板解析,提供最佳效能。
源碼生成的日誌與記錄器擴充方法(例如LogInformation 和 LogDebug)相比,提供以下效能優勢:
-
淘汰拳擊: 記錄器擴充方法需要進行「盒裝」(轉換)值型別,例如
int,轉換為object。 來源生成的日誌通過使用強類型參數來避免拳擊。 - 在編譯時解析範本: 記錄器擴充方法必須在每次寫入日誌訊息時解析訊息範本(命名為格式字串)。 原始碼產生的日誌會在編譯時解析一次範本。
- 減少配額: 原始碼產生器會產生優化的程式碼,以最小化物件配置與暫時記憶體使用。
範例應用程式展示了高效能日誌功能,並具備優先佇列處理工作者服務。 應用程式會依優先順序處理工作項目。 當這些操作發生時,日誌訊息會透過來源生成的記錄記錄下來。
小提示
所有記錄範例原始程式碼都可在範例瀏覽器中下載。 如需詳細資訊,請參閱 瀏覽程式碼範例: 在 .NET 中記錄。
定義帶有來源產生的記錄訊息
要在 .NET 6 及以後的版本中建立高效日誌訊息,請定義以 LoggerMessageAttribute 裝飾的 partial 方法。 原始碼產生器會在編譯時建立實作。
基本伐木方法
對於簡單的日誌訊息,定義一個部分方法,屬性指定事件 ID、日誌層級及訊息範本:
public static partial class Log
{
[LoggerMessage(
EventId = 13,
Level = LogLevel.Critical,
Message = "Epic failure processing item!")]
public static partial void FailedToProcessWorkItem(
ILogger logger, Exception ex);
}
訊息範本使用由方法參數填充的佔位符。 佔位符名稱應該描述性強且在不同範本間保持一致。 它們作為結構化日誌資料中的屬性名稱。 我們建議 Pascal 大小寫格式 作為佔位名稱。 例如,{Item}、{DateTime}。
從你的程式碼中呼叫日誌方法。 例如,當工作項目處理過程中發生異常時:
try
{
// Process work item.
}
catch (Exception ex)
{
Log.FailedToProcessWorkItem(logger, ex);
}
此程式碼產生的主控台輸出如下:
crit: WorkerServiceOptions.Example.Worker[13]
Epic failure processing item!
System.Exception: Failed to verify communications.
使用參數進行日誌記錄
要將參數傳給日誌訊息,請將它們加入為方法參數。 參數名稱與訊息範本中的佔位符相符:
public static partial class Log
{
[LoggerMessage(
EventId = 1,
Level = LogLevel.Information,
Message = "Processing priority item: {Item}")]
public static partial void PriorityItemProcessed(
ILogger logger, WorkItem item);
}
呼叫此方法,並使用記錄器和參數值:
var workItem = queue.Dequeue();
Log.PriorityItemProcessed(logger, workItem);
此程式碼產生的主控台輸出如下:
info: WorkerServiceOptions.Example.Worker[1]
Processing priority item: Priority-Extreme (50db062a-9732-4418-936d-110549ad79e4): 'Verify communications'
結構化日誌儲存庫可以在事件名稱與事件 ID 一同提供時,使用該名稱來豐富日誌內容。 例如, Serilog 使用事件名稱。
定義記錄器訊息範圍與來源產生
你可以定義 日誌範圍, 將一系列日誌訊息包裝並附加上下文。 在原始碼生成的記錄中,你將這些 LoggerMessageAttribute 方法與標準 ILogger.BeginScope 方法結合起來。
在 IncludeScopes的主控台記錄器區段啟用:
{
"Logging": {
"Console": {
"IncludeScopes": true
},
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
建立來源生成的日誌方法,並使用 BeginScope 將其包裝成作用域:
public static partial class Log
{
[LoggerMessage(
EventId = 1,
Level = LogLevel.Information,
Message = "Processing priority item: {Item}")]
public static partial void PriorityItemProcessed(
ILogger logger, WorkItem item);
}
在您的應用程式碼中的某個範圍內,使用日誌紀錄方法:
using (_logger.BeginScope("Processing scope, started at: {DateTime}", DateTime.Now))
{
Log.PriorityItemProcessed(_logger, workItem);
}
檢查應用程式主控台輸出中的日誌訊息。 以下結果顯示包含日誌範圍訊息的日誌訊息優先順序:
info: WorkerServiceOptions.Example.Worker[1]
=> Processing scope, started at: 04/11/2024 11:27:52
Processing priority item: Priority-Extreme (7d153ef9-8894-4282-836a-8e5e38319fb3): 'Verify communications'
舊有方法:LoggerMessage.Define(適用於 .NET Framework 與 .NET Core 3.1)
在 .NET 6 引入原始碼產生日誌之前,推薦的高效能日誌方法是使用此 LoggerMessage.Define 方法建立可快取代理。 雖然此方法仍支援向下相容,但新程式碼應改用原始碼產生的日誌 LoggerMessageAttribute 。
該 LoggerMessage 類別提供建立可快取代理的功能,這些代理比起 記錄器擴充方法(如 LogInformation 和 LogDebug)需要更少的物件配置並降低計算負擔。 LoggerMessage 相較於記錄器擴充方法,提供以下效能優勢:
- 記錄器擴充方法需要 "boxing" (轉換) 實值型別,例如將
int轉換為object。 LoggerMessage 模式可使用靜態 Action 欄位和擴充方法搭配強型別參數來避免 boxing。 - 記錄器擴充方法在每次寫入記錄訊息時,都必須剖析訊息範本 (具名格式字串)。 LoggerMessage 只需在定義訊息時剖析範本一次。
備註
如果你維護的程式碼使用 LoggerMessage.Define,可以考慮遷移到 原始碼生成的日誌。 對於 .NET Framework 或 .NET Core 3.1 應用程式,請繼續使用 LoggerMessage.Define。
定義記錄器訊息
使用 Define(LogLevel, EventId, String) 來建立 Action 一個代理來記錄訊息。 Define 過載允許將最多六個型別參數傳遞給一個命名的格式字串(模板)。
提供給 Define 方法的字串是模板,而非插值字串。 佔位符依照類型指定的順序填充。 模板中的佔位名稱應具描述性且跨模板保持一致。 它們作為結構化日誌資料中的屬性名稱。 我們建議使用 Pascal 大寫命名法 作為佔位名稱。 例如,{Item}、{DateTime}。
每個日誌訊息都保存 Action 在由 LoggerMessage.Dedefine 建立的靜態欄位中。 例如,範例應用程式會建立一個欄位來描述工作項目處理的日誌訊息:
private static readonly Action<ILogger, Exception> s_failedToProcessWorkItem;
對於 Action,請指定:
- 記錄層級。
- 一個帶有靜態擴充方法名稱的唯一事件識別碼(EventId)。
- 訊息範本(命名為格式字串)。
當工作項目從佇列中移出以進行處理時,工作者服務應用程式會設定:
- 日誌等級為 LogLevel.Critical。
- 事件 ID 與方法名稱
13FailedToProcessWorkItem相關聯。 - 訊息範本(稱為格式字串)轉換為字串。
s_failedToProcessWorkItem = LoggerMessage.Define(
LogLevel.Critical,
new EventId(13, nameof(FailedToProcessWorkItem)),
"Epic failure processing item!");
此 LoggerMessage.Define 方法用於配置與定義代理 Action ,代表日誌訊息。
結構化日誌儲存庫可以在事件名稱與事件 ID 一同提供時,使用該名稱來豐富日誌內容。 例如, Serilog 使用事件名稱。
Action 是透過強型別擴充方法呼叫的。 該 PriorityItemProcessed 方法每次處理工作項目時都會記錄訊息。 當發生例外時,將調用 FailedToProcessWorkItem。
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
using (IDisposable? scope = logger.ProcessingWorkScope(DateTime.Now))
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
WorkItem? nextItem = priorityQueue.ProcessNextHighestPriority();
if (nextItem is not null)
{
logger.PriorityItemProcessed(nextItem);
}
}
catch (Exception ex)
{
logger.FailedToProcessWorkItem(ex);
}
await Task.Delay(1_000, stoppingToken);
}
}
}
檢查應用程式的主控台輸出:
crit: WorkerServiceOptions.Example.Worker[13]
Epic failure processing item!
System.Exception: Failed to verify communications.
at WorkerServiceOptions.Example.Worker.ExecuteAsync(CancellationToken stoppingToken) in
..\Worker.cs:line 27
要傳遞參數給日誌訊息,建立靜態欄位時可定義最多六種類型。 範例應用程式在處理項目時,透過定義 WorkItem 欄位類型 Action 來記錄工作項目的詳細資料:
private static readonly Action<ILogger, WorkItem, Exception> s_processingPriorityItem;
代理的日誌訊息範本會從所提供的型別中接收預留值。 範例應用程式定義了一個代理,用於新增工作項目,其中項目參數為:WorkItem
s_processingPriorityItem = LoggerMessage.Define<WorkItem>(
LogLevel.Information,
new EventId(1, nameof(PriorityItemProcessed)),
"Processing priority item: {Item}");
用於記錄工作項目正在處理的靜態擴充方法, PriorityItemProcessed會接收工作項目的參數值並將其傳達給 Action 代理:
public static void PriorityItemProcessed(
this ILogger logger, WorkItem workItem) =>
s_processingPriorityItem(logger, workItem, default!);
在 worker service 的 ExecuteAsync 方法中,PriorityItemProcessed 被呼叫來記錄訊息:
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
using (IDisposable? scope = logger.ProcessingWorkScope(DateTime.Now))
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
WorkItem? nextItem = priorityQueue.ProcessNextHighestPriority();
if (nextItem is not null)
{
logger.PriorityItemProcessed(nextItem);
}
}
catch (Exception ex)
{
logger.FailedToProcessWorkItem(ex);
}
await Task.Delay(1_000, stoppingToken);
}
}
}
檢查應用程式的主控台輸出:
info: WorkerServiceOptions.Example.Worker[1]
Processing priority item: Priority-Extreme (50db062a-9732-4418-936d-110549ad79e4): 'Verify communications'
日誌層級的防護優化
你可以在呼叫對應的Log*方法前,先檢查LogLevel和ILogger.IsEnabled(LogLevel)來優化效能。 當LogLevelILogger.Log的日誌未設定時,就不會被呼叫。 此外,避免了值型箱控制及 object[] 配置(以表示參數)。
如需詳細資訊,請參閱: