共用方式為


.NET 中的高效能日誌

在 .NET 6 及更新版本中處理高效能日誌方案時,請使用 LoggerMessageAttribute 並結合編譯時原始碼生成。 此方法透過消除盒裝、臨時配置及執行時訊息模板解析,提供最佳效能。

源碼生成的日誌與記錄器擴充方法(例如LogInformationLogDebug)相比,提供以下效能優勢:

  • 淘汰拳擊: 記錄器擴充方法需要進行「盒裝」(轉換)值型別,例如 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 類別提供建立可快取代理的功能,這些代理比起 記錄器擴充方法(如 LogInformationLogDebug)需要更少的物件配置並降低計算負擔。 LoggerMessage 相較於記錄器擴充方法,提供以下效能優勢:

  • 記錄器擴充方法需要 "boxing" (轉換) 實值型別,例如將 int 轉換為 objectLoggerMessage 模式可使用靜態 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*方法前,先檢查LogLevelILogger.IsEnabled(LogLevel)來優化效能。 當LogLevelILogger.Log的日誌未設定時,就不會被呼叫。 此外,避免了值型箱控制及 object[] 配置(以表示參數)。

如需詳細資訊,請參閱:

另請參閱