共用方式為


編譯時間記錄來源產生

.NET 6 推出 LoggerMessageAttribute 型別。 這項屬性屬於 Microsoft.Extensions.Logging 命名空間的一部份,使用時,系統會從來源產生具效能的記錄 API。 來源產生記錄支援的設計目的是為現代化 .NET 用程式,提供高度可用且高效能的記錄解決方案。 自動產生的原始程式碼依賴 ILogger 介面搭配 LoggerMessage.Define 功能。

LoggerMessageAttribute 記錄方法上使用 partial 會觸發來源產生器。 觸發時,它能夠自動產生它所裝飾方法的 partial 實作,或產生編譯階段診斷,並提供正確使用的提示。 編譯時日誌解決方案在執行時速度遠快於現有的日誌方法。 之所以能夠如此,是因系統盡可能排除 boxing、暫時配置和複本。

基本使用方式

若要使用 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,記錄等級在屬性定義中指定。 在靜態內容中使用屬性時,若不是需要 ILogger 執行個體作為參數,就是需要修改定義來使用 this 關鍵字,將方法定義為擴充方法。

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 開始,記錄方法可以額外從包含類別中的 ILogger 主要建構函式參數取得記錄器。

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, /* Dynamic log level as parameter, rather than defined in attribute. */
        string hostName);
}

您可以省略記錄訊息,而使用 String.Empty 來替代該訊息。 狀態包含引數,格式化為索引鍵值組。

using System.Text.Json;
using Microsoft.Extensions.Logging;

using ILoggerFactory loggerFactory = LoggerFactory.Create(
    builder =>
    builder.AddJsonConsole(
        options =>
        options.JsonWriterOptions = new JsonWriterOptions()
        {
            Indented = true
        }));

ILogger<SampleObject> logger = loggerFactory.CreateLogger<SampleObject>();
logger.PlaceOfResidence(logLevel: LogLevel.Information, name: "Liana", city: "Seattle");

readonly file record struct SampleObject { }

public static partial class Log
{
    [LoggerMessage(EventId = 23, Message = "{Name} lives in {City}.")]
    public static partial void PlaceOfResidence(
        this ILogger logger,
        LogLevel logLevel,
        string name,
        string city);
}

請於使用 JsonConsole 格式器時考慮使用範例記錄輸出。

{
  "EventId": 23,
  "LogLevel": "Information",
  "Category": "\u003CProgram\u003EF...9CB42__SampleObject",
  "Message": "Liana lives in Seattle.",
  "State": {
    "Message": "Liana lives in Seattle.",
    "name": "Liana",
    "city": "Seattle",
    "{OriginalFormat}": "{Name} lives in {City}."
  }
}

記錄方法的限制

在記錄方法上使用 LoggerMessageAttribute 時,必須遵守部分限制:

  • 記錄方法必須為 partial 並傳回 void
  • 記錄方法名稱不可以底線開頭。
  • 記錄方法的參數名稱不可以底線開頭。
  • 記錄方法 不能 是泛型的。
  • 如果記錄方法為 static,則須以 ILogger 執行個體作為參數。

程式碼產生模型需要以版本 9 或更新版本的新式 C# 編譯器編譯程式碼。 .NET 5 可使用 C# 9.0 編譯器。 若要升級至新式 C# 編譯器,請將您的專案檔編輯為以 C# 9.0 為目標。

<PropertyGroup>
  <LangVersion>9.0</LangVersion>
</PropertyGroup>

如需詳細資訊,請參閱 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 連結庫。 此程式庫提供 編輯器,即轉換敏感資料的元件 (例如,透過清除、遮罩或雜湊),以便安全輸出。 根據 數據分類來選擇編輯器,這樣可讓您根據數據的敏感性來標記數據(例如個人、私人或公共)。

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

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

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

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

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

using Microsoft.Extensions.Telemetry;
using Microsoft.Extensions.Compliance.Redaction;

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);
});

public void TestLogging()
{
    LogPrivateInformation("MySSN");
}

輸出應該如下所示:

User SSN: *****

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

如需如何分類資料的詳細資訊,請參閱 .NET 中的資料分類。 如需密文和編輯器的詳細資訊,請參閱 .NET 中的資料密文

摘要

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

  • 允許保留記錄結構,並啟用訊息範本 需要的確切格式語法。
  • 允許為範本預留位置提供替代名稱,並使用格式指定名稱。
  • 允許以原狀傳遞所有原始資料,而不要求資料受到處理前應有的儲存方式 (建立 string 除外)。
  • 提供記錄特定的診斷,並針對重複的事件識別碼發出警告。

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

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

另請參閱