コンパイル時のログ ソース生成

.NET 6 では、`LoggerMessageAttribute` 型が導入されています。 この属性は `Microsoft.Extensions.Logging` 名前空間の一部であり、使用すると、パフォーマンスの高いログ API がソース生成されます。 ソース生成ログのサポートは、最新の .NET アプリケーションに対して、非常に使いやすくパフォーマンスの高いログ ソリューションを提供するように設計されています。 自動生成されたソース コードは、 機能と組み合わせて使用される インターフェイスに依存します。

ソース ジェネレーターは、`partial` ログ メソッドで `LoggerMessageAttribute` が使用されるとトリガーされます。 トリガーされると、装飾している `partial` メソッドの実装を自動生成するか、適切な使用方法に関するヒントを含むコンパイル時診断を生成することができます。 コンパイル時ログ ソリューションの場合、通常は既存のログ方法よりも実行時の速度が大幅に増します。 ボックス化、一時的な割り当て、およびコピーを可能な限り排除することで、これを実現します。

基本的な使用方法

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

ログ メッセージは省略できます。そのメッセージに対しては、 が指定されます。 状態には、キーと値のペアとして書式設定された引数が含まれます。

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 インスタンスがパラメーターとして必要です。

コード生成モデルは、最新の C# コンパイラ バージョン 9 以降でコンパイルされるコードに依存します。 C# 9.0 コンパイラは、.NET 5 で使用できるようになりました。 最新の C# コンパイラにアップグレードするには、C# 9.0 をターゲットとするようにプロジェクト ファイルを編集します。

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

詳細については、「C# 言語のバージョン管理」を参照してください。

ログ メソッドの構造

シグネチャでは、以下に示すように と必要に応じて を受け入れます。

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

一般的な規則として、`ILogger``LogLevel`、および `Exception` の最初のインスタンスは、ソース ジェネレーターのログ メソッド シグネチャで特別に処理されます。 後続のインスタンスは、メッセージ テンプレートの通常のパラメーターと同様に処理されます。

// 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` により、`SYSLIB0025``DiagnosticSeverity.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 の作成がはるかに簡単になりました。 ソース ジェネレーターの方法を使用することには、いくつかの重要なベネフィットがあります。

  • ログ構造を保持でき、[メッセージ テンプレート](https://messagetemplates.org)で必要とされる正確な形式の構文が有効になります。
  • テンプレート プレースホルダーの代替名を指定し、書式指定子を使用できます。
  • 元のデータをすべてそのまま渡すことができ、(string を作成する以外に) そのデータで何かを行う前にどのように格納するかといった複雑さもありません。
  • ログ固有の診断を提供し、重複するイベント ID に関する警告を出力します。

さらに、 を使用する手動の場合よりベネフィットがあります。

  • より短く単純な構文: 定型コーディングではなく、宣言属性を使用します。
  • ガイド付き開発者エクスペリエンス: ジェネレーターにより、開発者が適切な作業を行うのに役立つ警告が示されます。
  • 任意の数のログ パラメーターのサポート。 `LoggerMessage.Define` では最大 6 つがサポートされます。
  • 動的ログ レベルのサポート。 これは `LoggerMessage.Define` のみでは不可能です。

関連項目