次の方法で共有


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

ソース生成ログは、最新の.NET アプリに対して、非常に使いやすくパフォーマンスの高いログ記録ソリューションを提供するように設計されています。 自動生成されたソースコードは、 インターフェイスと 機能を組み合わせて使用します。

ソース ジェネレーターは、 が ログ メソッドで使用されるとトリガーされます。 トリガーされると、装飾する メソッドの実装が自動生成されます。 問題がある場合は、適切な使用方法に関するヒントを含むコンパイル時の診断が生成されます。 このコンパイル時のログ記録ソリューションは、以前に使用可能なログ記録方法よりも実行時に大幅に高速です。 ボックス化、一時的な割り当て、コピーを可能な限り最大限に排除します。

基本的な使用方法

を使うには、使用するクラスとメソッドが である必要があります。 コード ジェネレーターはコンパイル時にトリガーされ、 メソッドの実装を生成します。

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

前の例では、ログメソッドは であり、ログレベルは属性定義で指定されています。 静的コンテキストで属性を使用する場合は、 インスタンスがパラメーターとして必要になるか、 キーワードを使用して該当のメソッドを拡張メソッドとするように定義を変更します。

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

フィールドとプライマリ コンストラクター パラメーターの両方がある場合、ログ メソッドはフィールドからロガーを取得します。

場合によっては、コードに静的に組み込むのではなく、ログ レベルを動的にする必要があります。 これは、属性のログ レベルを省略し、代わりにログ メソッドのパラメーターとしてそれを要求することで行うことができます。

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

フォーマッタを使用する場合のログ出力の例を考えてみましょう。

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

ログ メソッドの制約

で修飾されたログ記録方法は、次の要件を満たす必要があります。

  • ログ メソッドは であり、 を返す必要があります。
  • ログメソッド名はアンダースコアで始まらないようにしなければなりません。
  • ログメソッドのパラメーター名はアンダースコアで始めてはいけません。
  • ログ メソッドはジェネリック型パラメーターをサポートしていますが、C# 13 の制約対策はサポートされていません。
  • ログメソッドのパラメーターは、 、 、または 修飾子を使用できません。また、 型にすることはできません。
  • ログ メソッドが の場合、 インスタンスがパラメーターとして必要です。

コード生成モデルは、最新の C# コンパイラ (バージョン 9 以降) を使用してコンパイルされるコードに依存します。 言語バージョンの変更については、「 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);
}

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

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

Important

出力される警告は、`LoggerMessageAttribute` の正しい使用法に関する詳細を提供します。 前述の例では、がのを報告します。

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

大文字と小文字を区別しないテンプレート名のサポート

ジェネレーターでは、メッセージ テンプレート内の項目とログ メッセージ内の引数名の間で、大文字と小文字を区別しない比較を行います。 つまり、 により状態が列挙されると、引数がメッセージ テンプレートによって取得され、ログが使いやすくなります。

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

フォーマッタを使用する場合のログ出力の例を考えてみましょう。

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

不確定なパラメーターの順序

ログ メソッド パラメーターの順序に関する制約はありません。 開発者は、 を最後のパラメーターとして定義できますが、少し厄介に見える場合があります。

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

ヒント

ログ メソッドのパラメーターの順序は、テンプレート プレースホルダーの順序に対応する必要はありません。 代わりに、テンプレート内のプレースホルダー名がパラメーターと一致する必要があります。 次の 出力とエラーの順番について考えてみます。

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

その他のログの例

次のサンプルからは、イベント名の取得方法、ログ レベルを動的に設定する方法、ログ パラメーターを書式設定する方法がわかります。 ログ方法は次のとおりです。

  • : 属性を使ってイベント名を取得します。
  • : 構成入力に基づいてログ レベルを動的に設定できるように、ログ レベルを設定します。
  • : 書式指定子を使用してログパラメーターを整形します。
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);
    }
}

フォーマッタを使用する場合のログ出力の例を考えてみましょう。

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

フォーマッタを使用する場合のログ出力の例を考えてみましょう。

{
  "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 を使用する場合でも、編集されたデータのみがログに記録されます。 データの種類や分類ごとに異なる編集機能を使用し、編集ロジックを一元的に更新できます。

データを分類する方法の詳細については、「data classification in .NET」を参照してください。 編集と編集者の詳細については、「 .NET におけるデータの編集」を参照してください。

概要

C# ソース ジェネレーターが登場すると、パフォーマンスの高いログ記録 API の記述が簡単になります。 ソース ジェネレーターの方法を使用することには、いくつかの重要なベネフィットがあります。

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

さらに、 を手動で使用する場合よりも利点があります。

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

こちらも参照ください

  • .NETでのログ記録
  • .NETでの高性能ログ記録
  • コンソール ログの書式設定
  • .NET
  • .NET
  • NuGet: Microsoft.Extensions.Logging.Abstractions