編譯時間記錄來源產生

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

partial 記錄方法上使用 LoggerMessageAttribute 會觸發來源產生器。 觸發後,系統就能自動產生裝飾的 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);
}

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

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.Log 特徵標記會接受 LogLevel 並選擇性接受 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 會回報 SYSLIB0025DiagnosticSeverity.Warning

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}"
  }
}

摘要

C# 來源產生器推出後,撰寫高效能記錄 API 變得容易許多。 使用來源產生器方法有幾個重要優勢:

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

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

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

另請參閱