ベストプラクティス

OpenTelemetry .NET でログを使用する際のベストプラクティスを学びます

以下のベストプラクティスに従って、OpenTelemetry .NET のログを最大限に活用しましょう。

Logging API

ILogger

.NET は、アプリケーションの動作を監視し問題を診断するために、Microsoft.Extensions.Logging.ILogger インターフェイス(ILogger<TCategoryName> を含む)を通じて、高パフォーマンスな構造化ログをサポートしています。

パッケージバージョン

使用している .NET ランタイムのバージョンに関係なく、Microsoft.Extensions.Logging パッケージの最新の安定バージョンの ILogger インターフェイス(ILogger<TCategoryName> を含む)を使用してください。

  • OpenTelemetry .NET SDK の最新の安定バージョンを使用している場合、Microsoft.Extensions.Logging パッケージのバージョンはパッケージの依存関係を通じてすでに管理されているため、気にする必要はありません。
  • バージョン 3.1.0 以降、.NET ランタイムチームはメジャーバージョンの更新時でも Microsoft.Extensions.Logging の後方互換性に高い基準を設けているため、互換性について心配する必要はありません。

ロガーの取得

ILogger インターフェイスを使用するには、まずロガーを取得する必要があります。 ロガーの取得方法は次の2つの要素に依存します。

  • 構築しているアプリケーションの種類。
  • ログを記録したい場所。

一般的なルールとして、以下のようになります。

ログカテゴリ名にはドット区切りの UpperCamelCase を使用すると、ログのフィルタリングに便利です。 一般的な方法は完全修飾クラス名を使用することであり、さらに分類が必要な場合はサブカテゴリ名を追加します。 詳しくは .NET 公式ドキュメントを参照してください。 たとえば、以下のようになります。

loggerFactory.CreateLogger<MyClass>(); // これは CreateLogger("MyProduct.MyLibrary.MyClass") と同等
loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass"); // 完全修飾クラス名を使用
loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass.DatabaseOperations"); // サブカテゴリ名を追加
loggerFactory.CreateLogger("MyProduct.MyLibrary.MyClass.FileOperations"); // 別のサブカテゴリ名を追加

ロガーの作成を頻繁に行いすぎないようにしてください。 ロガーは非常に高コストではありませんが、CPU とメモリのコストがかかるため、アプリケーション全体で再利用することを目的としています。

ログメッセージの書き方

構造化ログを使用してください。

  • 構造化ログは非構造化ログよりも効率的です。
    • 個々のキーバリューペアに対してフィルタリングやリダクションを行えるため、ログメッセージ全体に対して行う必要がありません。
    • ストレージとインデックス作成がより効率的です。
  • 構造化ログによりログの管理と利用が容易になります。

たとえば、以下のようになります。

var food = "tomato";
var price = 2.99;

logger.LogInformation("Hello from {food} {price}.", food, price);

文字列補間は避けてください。 たとえば、以下のようになります。

var food = "tomato";
var price = 2.99;

logger.LogInformation($"Hello from {food} {price}.");

最高のパフォーマンスを得るには、コンパイル時ログソース生成パターンを使用してください。 たとえば、以下のようになります。

var food = "tomato";
var price = 2.99;

logger.SayHello(food, price);

internal static partial class LoggerExtensions
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
    public static partial void SayHello(this ILogger logger, string food, double price);
}

複雑なオブジェクトをログに記録する必要がある場合は、Microsoft.Extensions.Telemetry.AbstractionsLogPropertiesAttribute を使用してください。 詳しくは複雑なオブジェクトのログ記録チュートリアルを参照してください。

LoggerExtensions の拡張メソッドの使用は避けてください。 これらのメソッドはパフォーマンスに最適化されていません。 たとえば、以下のようになります。

var food = "tomato";
var price = 2.99;

logger.LogInformation("Hello from {food} {price}.", food, price);

ILogger.IsEnabled の使用には高い基準を持ってください。

ロギング API は、ほとんどのロガーが特定のログレベルに対して無効になっているシナリオに高度に最適化されています。 ログ記録の前に IsEnabled を追加で呼び出しても、パフォーマンスの向上は得られません。 たとえば、以下のようになります。

var food = "tomato";
var price = 2.99;

if (logger.IsEnabled(LogLevel.Information)) // これはしないでください、パフォーマンスの向上はありません
{
    logger.SayHello(food, price);
}

internal static partial class LoggerExtensions
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
    public static partial void SayHello(this ILogger logger, string food, double price);
}

IsEnabled は、引数の評価にコストがかかる場合にパフォーマンス上の利点をもたらすことがあります。 たとえば、以下のコードではロガーが有効でない場合、Database.GetFoodPrice の呼び出しはスキップされます。

if (logger.IsEnabled(LogLevel.Information))
{
    logger.SayHello(food, Database.GetFoodPrice(food));
}

上記のシナリオでは IsEnabled がパフォーマンス上の利点をもたらす場合がありますが、ほとんどのユーザーにとってはより多くの問題を引き起こす可能性があります。 たとえば、コードのパフォーマンスがどのロガーが有効かに依存するようになり、さらに引数の評価にはロギング設定に依存する重大な副作用がある可能性もあります。

コンパイル時ソースジェネレーターを使用する場合は、例外をログに記録するための専用パラメーターを使用してください。 たとえば、以下のようになります。

var food = "tomato";
var price = 2.99;

try
{
    // ロジックを実行

    logger.SayHello(food, price);
}
catch (Exception ex)
{
    logger.SayHelloFailure(ex, food, price);
}

internal static partial class LoggerExtensions
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
    public static partial void SayHello(this ILogger logger, string food, double price);

    [LoggerMessage(Level = LogLevel.Error, Message = "Could not say hello from {food} {price}.")]
    public static partial void SayHelloFailure(this ILogger logger, Exception exception, string food, double price);
}

ロギング拡張メソッドを使用する場合は、例外をログに記録するための専用オーバーロードを使用するべきです。

var food = "tomato";
var price = 2.99;

try
{
    // ロジックを実行

    logger.LogInformation("Hello from {food} {price}.", food, price);
}
catch (Exception ex)
{
    logger.LogError(ex, "Could not say hello from {food} {price}.", food, price);
}

メッセージテンプレートに例外の詳細を追加することは避けてください。 たとえば、以下のようになります。

OpenTelemetry の仕様では Exception の詳細について専用の属性を定義しているため、正しい Exception API を使用する必要があります。 以下の例はやってはいけないことを示しています。 これらのケースでは詳細は失われませんが、専用の属性も追加されません。

var food = "tomato";
var price = 2.99;

try
{
    // ロジックを実行

    logger.SayHello(food, price);
}
catch (Exception ex)
{
    logger.SayHelloFailure(food, price, ex.Message);
}

internal static partial class LoggerExtensions
{
    [LoggerMessage(Level = LogLevel.Information, Message = "Hello from {food} {price}.")]
    public static partial void SayHello(this ILogger logger, string food, double price);

    // 悪い例 - Exception はメッセージテンプレートの一部にすべきではありません。専用パラメーターを使用してください。
    [LoggerMessage(Level = LogLevel.Error, Message = "Could not say hello from {food} {price} {message}.")]
    public static partial void SayHelloFailure(this ILogger logger, string food, double price, string message);
}
var food = "tomato";
var price = 2.99;

try
{
    // ロジックを実行

    logger.LogInformation("Hello from {food} {price}.", food, price);
}
catch (Exception ex)
{
    // 悪い例 - Exception はメッセージテンプレートの一部にすべきではありません。専用パラメーターを使用してください。
    logger.LogError("Could not say hello from {food} {price} {message}.", food, price, ex.Message);
}

LoggerFactory

多くの場合、Microsoft.Extensions.Logging.LoggerFactory と直接やり取りせずに ILogger を使用できます。 このセクションは、LoggerFactory を明示的に作成および管理する必要があるユーザーを対象としています。

LoggerFactory インスタンスの作成を頻繁に行いすぎないようにしてください。 LoggerFactory はかなりのコストがかかり、アプリケーション全体で再利用することを目的としています。 ほとんどのアプリケーションでは、プロセスごとに1つの LoggerFactory インスタンスで十分です。

LoggerFactory インスタンスを自分で作成した場合は、そのライフサイクルを管理してください。

  • アプリケーション終了前に LoggerFactory インスタンスの破棄を忘れると、適切なフラッシュが行われないためログが欠落する可能性があります。
  • LoggerFactory インスタンスを早すぎるタイミングで破棄すると、そのロガーファクトリに関連付けられた後続のロギング API 呼び出しは no-op になる可能性があります(つまり、ログが出力されなくなります)。

ログの相関

OpenTelemetry では、ログは自動的にトレースと相関付けられます。 詳しくはログの相関チュートリアルを参照してください。

ログのフィルタリング

より高度なフィルタリングやサンプリングについては、.NET チームが .NET 9 の期間内に対応する計画があります。 進捗状況の追跡やフィードバック・提案の提供には、このランタイムイシューを使用してください。

ログのリダクション

ログにはパスワードやクレジットカード番号などの機密情報が含まれる場合があり、プライバシーとセキュリティのインシデントを防ぐために適切なリダクションが必要です。 詳しくはログのリダクションチュートリアルを参照してください。