編譯時間記錄來源產生

原始碼生成的日誌設計旨在為現代 .NET 應用程式提供高度可用且高效能的日誌解決方案。 自動產生的原始程式碼依賴 ILogger 介面搭配 LoggerMessage.Define 功能。

LoggerMessageAttribute 記錄方法上使用 partial 會觸發來源產生器。 當觸發時,它會自動生成所裝飾的 partial 方法的實作。 如果有問題,它會產生編譯時診斷,並提示正確使用方式。 這種編譯時日誌解決方案在執行時比先前可用的日誌方法快得多。 它盡可能消除了包裝、臨時分配和複製。

基本用法

小提示

本文的範例使用以下命名空間:

using Microsoft.Extensions.Logging;

若要使用 LoggerMessageAttribute,消費者的類別和方法必須是 partial。 程式碼產生器會在編譯時觸發,並產生該方法的實作 partial

public static partial class Log
{
    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public static partial void CouldNotOpenSocket(
        ILogger logger, string hostName);
}

在上述範例中,記錄方法為 static,記錄等級在屬性定義中指定。 當你在靜態上下文中使用 LoggerMessageAttribute 時, ILogger 該實例必須以參數形式傳遞。 或者,在參數中加上 this 修飾符 ILogger ,定義該方法為擴展方法。

public static partial class Log
{
    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public static partial void CouldNotOpenSocket(
        this ILogger logger, string hostName);
}

您也可以選擇在非靜態內容中使用屬性。 請考慮下列範例,其中記錄方法已宣告為執行個體方法。 在此內容中,記錄方法會存取包含類別中的 ILogger 欄位來獲取記錄器。

public partial class InstanceLoggingExample
{
    private readonly ILogger _logger;

    public InstanceLoggingExample(ILogger logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public partial void CouldNotOpenSocket(string hostName);
}

從 .NET 9 開始,記錄方法還可以從包含類別中的主建構函式參數取得記錄器。

public partial class InstanceLoggingExample(ILogger logger)
{
    [LoggerMessage(
        EventId = 0,
        Level = LogLevel.Critical,
        Message = "Could not open socket to `{HostName}`")]
    public partial void CouldNotOpenSocket(string hostName);
}

如果既有 ILogger 欄位又有主要建構函式參數,記錄方法會從該欄位取得記錄器。

動態對數層級

有時,內建於程式碼的記錄等級須為動態而非靜態。 若要這麼做,您可以省略屬性中的記錄等級,並改而在記錄方法中將之要求為參數。

public static partial class Log
{
    [LoggerMessage(
        EventId = 0,
        Message = "Could not open socket to `{HostName}`")]
    public static partial void CouldNotOpenSocket(
        ILogger logger,
        LogLevel level,
        string hostName);
}

Message 屬性

Message 上的 LoggerMessageAttribute 屬性是可選的。 如果你省略了,String.Empty 就會用於訊息中。 然而,如果日誌方法的參數沒有對應的模板佔位符,編譯器會發出 SYSLIB1015 警告。 這些參數會儲存在日誌狀態,但格式化的日誌輸出中不會出現。 只有列舉日誌狀態的結構化日誌提供者會顯示這些日誌。

記錄方法的限制

使用 LoggerMessageAttribute 裝飾的日誌記錄方法必須符合以下要求:

  • 記錄方法必須為 partial 並傳回 void
  • 記錄的方法名稱不可以底線開頭。
  • 記錄方法的參數名稱不可以底線開頭。
  • 日誌方法支援通用型別參數,但不支援 C# 13 allows ref struct 的反約束。
  • 日誌方法的參數不能使用 paramsscopedout 修飾符,也不能是 ref struct 型別。
  • 如果記錄方法為 static,則須以 ILogger 執行個體作為參數。

程式碼產生模型依賴於使用現代 C# 編譯器編譯,也就是版本 9 或更新版本。 關於更改語言版本的資訊,請參見 C# 語言版本管理

記錄方法剖析

簽章接受 ILogger.LogLogLevel,以及選擇性的 Exception,如下列程式碼範例所示。

public interface ILogger
{
    void Log<TState>(
        Microsoft.Extensions.Logging.LogLevel logLevel,
        Microsoft.Extensions.Logging.EventId eventId,
        TState state,
        System.Exception? exception,
        Func<TState, System.Exception?, string> formatter);
}

一般規則是,ILoggerLogLevelException 的第一個實例會在來源產生器的記錄方法特徵標記中特別處理。 後續案例將按照訊息範本中的一般參數進行處理:

// This is a valid attribute usage
[LoggerMessage(
    EventId = 110, Level = LogLevel.Debug, Message = "M1 {Ex3} {Ex2}")]
public static partial void ValidLogMethod(
    ILogger logger,
    Exception ex,
    Exception ex2,
    Exception ex3);

以下展示產生編譯器診斷的一種使用方法:

// This causes a warning
[LoggerMessage(
    EventId = 0, Level = LogLevel.Debug, Message = "M1 {Ex} {Ex2}")]
public static partial void WarningLogMethod(
    ILogger logger,
    Exception ex,
    Exception ex2);

這很重要

系統發出的警告會提供 LoggerMessageAttribute 正確使用方式的詳細資料。 在上述範例中,WarningLogMethod 報告了一個 DiagnosticSeverity.WarningSYSLIB0025

Don't include a template for `ex` in the logging message since it is implicitly taken care of.

不區分大小寫的範本名稱支援

產生器會為訊息範本內的項目和記錄訊息中的引數名稱進行不區分大小寫的比較。 也就是說,ILogger 列舉狀態時,訊息範本會挑選引數,使記錄更易於使用:

public partial class LoggingExample
{
    private readonly ILogger _logger;

    public LoggingExample(ILogger logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 10,
        Level = LogLevel.Information,
        Message = "Welcome to {City} {Province}!")]
    public partial void LogMethodSupportsPascalCasingOfNames(
        string city, string province);

    public void TestLogging()
    {
        LogMethodSupportsPascalCasingOfNames("Vancouver", "BC");
    }
}

考慮使用 JsonConsole 格式器時的範例記錄輸出:

{
  "EventId": 13,
  "LogLevel": "Information",
  "Category": "LoggingExample",
  "Message": "Welcome to Vancouver BC!",
  "State": {
    "Message": "Welcome to Vancouver BC!",
    "City": "Vancouver",
    "Province": "BC",
    "{OriginalFormat}": "Welcome to {City} {Province}!"
  }
}

不確定的參數順序

記錄方法參數的順序沒有限制。 開發人員可以將 定義 ILogger 為最後一個參數,儘管它可能看起來有點尷尬。

[LoggerMessage(
    EventId = 110,
    Level = LogLevel.Debug,
    Message = "M1 {Ex3} {Ex2}")]
static partial void LogMethod(
    Exception ex,
    Exception ex2,
    Exception ex3,
    ILogger logger);

小提示

記錄方法上的參數順序不需要對應至範本預留位置的順序。 反之,範本內的預留位置名稱應與參數保持一致。 請考慮下列 JsonConsole 輸出以及錯誤順序。

{
  "EventId": 110,
  "LogLevel": "Debug",
  "Category": "ConsoleApp.Program",
  "Message": "M1 System.Exception: Third time's the charm. System.Exception: This is the second error.",
  "State": {
    "Message": "M1 System.Exception: Third time's the charm. System.Exception: This is the second error.",
    "ex2": "System.Exception: This is the second error.",
    "ex3": "System.Exception: Third time's the charm.",
    "{OriginalFormat}": "M1 {Ex3} {Ex2}"
  }
}

更多記錄範例

下列範例示範如何擷取事件名稱、動態設定記錄等級,並為記錄參數設定格式。 記錄方法包括:

  • LogWithCustomEventName:透過 LoggerMessage 屬性擷取事件名稱。
  • LogWithDynamicLogLevel:動態設定日誌等級,以允許根據配置輸入來設定日誌等級。
  • UsingFormatSpecifier:使用格式規範來設定記錄參數的格式。
public partial class LoggingSample
{
    private readonly ILogger _logger;

    public LoggingSample(ILogger logger)
    {
        _logger = logger;
    }

    [LoggerMessage(
        EventId = 20,
        Level = LogLevel.Critical,
        Message = "Value is {Value:E}")]
    public static partial void UsingFormatSpecifier(
        ILogger logger, double value);

    [LoggerMessage(
        EventId = 9,
        Level = LogLevel.Trace,
        Message = "Fixed message",
        EventName = "CustomEventName")]
    public partial void LogWithCustomEventName();

    [LoggerMessage(
        EventId = 10,
        Message = "Welcome to {City} {Province}!")]
    public partial void LogWithDynamicLogLevel(
        string city, LogLevel level, string province);

    public void TestLogging()
    {
        LogWithCustomEventName();

        LogWithDynamicLogLevel("Vancouver", LogLevel.Warning, "BC");
        LogWithDynamicLogLevel("Vancouver", LogLevel.Information, "BC");

        UsingFormatSpecifier(_logger, 12345.6789);
    }
}

考慮使用 SimpleConsole 格式器時的範例記錄輸出:

trce: LoggingExample[9]
      Fixed message
warn: LoggingExample[10]
      Welcome to Vancouver BC!
info: LoggingExample[10]
      Welcome to Vancouver BC!
crit: LoggingExample[20]
      Value is 1.234568E+004

考慮使用 JsonConsole 格式器時的範例記錄輸出:

{
  "EventId": 9,
  "LogLevel": "Trace",
  "Category": "LoggingExample",
  "Message": "Fixed message",
  "State": {
    "Message": "Fixed message",
    "{OriginalFormat}": "Fixed message"
  }
}
{
  "EventId": 10,
  "LogLevel": "Warning",
  "Category": "LoggingExample",
  "Message": "Welcome to Vancouver BC!",
  "State": {
    "Message": "Welcome to Vancouver BC!",
    "city": "Vancouver",
    "province": "BC",
    "{OriginalFormat}": "Welcome to {City} {Province}!"
  }
}
{
  "EventId": 10,
  "LogLevel": "Information",
  "Category": "LoggingExample",
  "Message": "Welcome to Vancouver BC!",
  "State": {
    "Message": "Welcome to Vancouver BC!",
    "city": "Vancouver",
    "province": "BC",
    "{OriginalFormat}": "Welcome to {City} {Province}!"
  }
}
{
  "EventId": 20,
  "LogLevel": "Critical",
  "Category": "LoggingExample",
  "Message": "Value is 1.234568E+004",
  "State": {
    "Message": "Value is 1.234568E+004",
    "value": 12345.6789,
    "{OriginalFormat}": "Value is {Value:E}"
  }
}

在日誌中遮蔽敏感資訊

記錄敏感數據時,請務必防止意外暴露。 即使使用在編譯時產生的日誌記錄方法,記錄原始的敏感數據值也可能導致資料洩露和合規問題。

Microsoft.Extensions.Telemetry函式庫為.NET應用提供進階的日誌記錄與遙測豐富功能。 它會擴充記錄管線,以在寫入記錄時自動將修訂套用至分類數據。 它可讓您藉由將修訂整合到記錄工作流程,在整個應用程式中強制執行數據保護原則。 它專為需要複雜遙測和日誌記錄見解的應用程式而構建。

若要啟用修訂,請使用 Microsoft.Extensions.Compliance.Redaction 連結庫。 此程式庫提供 編輯器,即轉換敏感資料的元件 (例如,透過清除、遮罩或雜湊),以便安全輸出。 根據 數據分類來選擇編輯器,這樣可讓您根據數據的敏感性來標記數據(例如個人、私人或公共)。

這些刪節範例需要以下命名空間:

using Microsoft.Extensions.Compliance.Classification;
using Microsoft.Extensions.Compliance.Redaction;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

若要搭配來源產生的記錄方法使用修訂,您應該:

  1. 使用數據分類系統來分類敏感數據。
  2. 註冊並配置 DI 容器中每個分類的編輯器。
  3. 在記錄管線中啟用遮蔽功能。
  4. 請檢查您的記錄,以確保不會公開任何敏感數據。

例如,如果您有記錄訊息,其中包含被視為私用的參數:

public static partial class LogRedactionExample
{
    [LoggerMessage(0, LogLevel.Information, "User SSN: {SSN}")]
    public static partial void LogPrivateInformation(
        this ILogger logger,
        [MyTaxonomyClassifications.Private] string SSN);
}

您必須有類似以下的設定:

var services = new ServiceCollection();
services.AddLogging(builder =>
{
    // Enable redaction.
    builder.EnableRedaction();
});

services.AddRedaction(builder =>
{
    // Configure redactors for your data classifications.
    builder.SetRedactor<StarRedactor>(MyTaxonomyClassifications.Private);
});

輸出應該是這樣: User SSN: *****

此方法可確保即使使用編譯時間產生的記錄 API,也只會記錄已修訂的數據。 您可以針對不同的數據類型或分類使用不同的重新執行者,並集中更新您的修訂邏輯。

欲了解更多如何分類資料的資訊,請參閱.NET<>中的資料分類。 若要了解有關資料編輯與編輯者的更多資訊,請參閱.NET中的資料編輯

總結

隨著 C# 來源產生器的出現,撰寫高效能的日誌記錄 API 變得更加容易。 使用來源產生器方法有幾個重要優勢:

  • 允許保留記錄結構,並啟用訊息範本 需要的確切格式語法。
  • 允許為範本預留位置提供替代名稱,並使用格式規範。
  • 允許將所有原始資料如實傳遞,且不涉及處理前如何儲存的複雜要求(建立 string 除外)。
  • 提供針對日誌的專屬診斷,並對重複事件 ID 發出警告。

此外,手動使用 LoggerMessage.Define 也有以下好處:

  • 語法較短且較單純:用宣告式屬性取代重複使用程式碼。
  • 引導式開發人員體驗:產生器會提出警告,幫助開發人員採取正確動作。
  • 支援任意數量的記錄參數。 LoggerMessage.Define 支援最多六個參數。
  • 支援動態日誌級別。 這單靠 LoggerMessage.Define 是不可能的。

另請參閱