【.NET 10】OpenTelemetryで実現する実践的オブザーバビリティ:ノイズ除去とゼロ・アロケーションログ

Uncategorized

最近のクラウドネイティブな開発において、システムの内部状態を可視化する「オブザーバビリティ(可観測性)」の重要性が高まっています。そのデファクトスタンダードとなっているのが OpenTelemetry です。

しかし、チュートリアル通りにOpenTelemetryを導入すると、ヘルスチェックやバックグラウンドの定期実行処理(ポーリング)など、不要な通信ログ(ノイズ)まで大量にクラウドに送信され、ログストレージ(Azure Monitor等)のコストが高騰してしまうという実務特有の罠があります。

本記事では、ASP.NET Core 10 (.NET 10) をターゲットに、単なる導入にとどまらない「実運用を見据えたOpenTelemetryの実践的な実装例」をご紹介します。

💡 ソースコード一式 本記事で解説しているプロジェクトの完全なソースコードは、以下のGitHubリポジトリで公開しています。sgent-dev/OpenTelemetryExample


1. ハイブリッドな計装(Instrumentation)アプローチ

ログを記録する際、すべてを手動で実装するのは非効率です。実務では「ライブラリによる自動計装」と「ビジネスロジックの手動計装」を組み合わせるのがベストプラクティスです。

  • データベース通信の自動計装 MySqlConnector など、モダンなライブラリは標準でOpenTelemetryに対応しています。Program.cs でソース名(MySqlConnector)を登録するだけで、実行されたSQL文や実行時間が自動的にトレースされます。
  • ユースケース単位の手動計装 コントローラー側では独自の ActivitySource を使い、「ユーザー登録フロー」などのビジネスロジック全体を親トレースとして記録します。これにより、ビジネス側の動きとDB側の動きが階層化され、非常に分析しやすいログになります。

2. コストを最適化する「カスタムProcessor」の実装

OpenTelemetry最大の強みは、トレースがエクスポートされる前に処理を介入できる「Processor(プロセッサ)」の仕組みです。本プロジェクトでは、徹底的なノイズ除去を行っています。

動的なノイズフィルタリング(ポーリング処理の除外)

例えば、「DBを定期的に監視し、変更があった時だけログを残したい」という要件があるとします。変更がない時の空振りログをすべてクラウドに送ると、莫大なコストになります。

これを解決するため、処理完了後に動的にログを破棄するカスタムプロセッサを実装しました。

C#

using OpenTelemetry;
using System.Diagnostics;

namespace OpenTelemetryExample.Traces;

public class PollingNoiseFilterProcessor : BaseProcessor<Activity>
{
    public override void OnEnd(Activity activity)
    {
        // 親(ポーリング本体)に "polling.no_change" タグが付いていれば、
        // その中で発生した自動計装のSQLログなども道連れにして破棄する
        var parent = activity.Parent;
        if (parent != null)
        {
            var parentNoChangeTag = parent.GetTagItem("polling.no_change") as string;
            if (parentNoChangeTag == "true")
            {
                activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded;
            }
        }
    }
}

コントローラー側で activity?.SetTag("polling.no_change", "true"); とセットするだけで、不要なトレースとDB通信ログが綺麗に消去されます。

その他のフィルター

他にも、ロードバランサーからの無駄な /health アクセスを除外する HealthCheckFilterProcessor や、Blazorの内部通信を識別してカテゴリ分けする BlazorFilterProcessor などを組み込んでいます。


3. 分散トレーシングを強力にする「タグの自動継承」

複数の処理をまたぐ際、「この重いSQLはどのユーザーが実行したものか?」を特定するのは困難です。 そこで、親のアクティビティ(コントローラー)で付与した enduser.id を、子のアクティビティ(DBアクセス)へ自動的に伝播させる TagInheritanceProcessor を実装しました。

C#

public override void OnStart(Activity activity)
{
    var parent = activity.Parent;
    if (parent != null)
    {
        // 親が持っている "enduser.id" などを子にも自動コピー
        foreach (var tag in parent.Tags)
        {
            if (s_InheritTags.Contains(tag.Key))
            {
                activity.SetTag(tag.Key, tag.Value);
            }
        }
    }
}

これにより、Azure Monitor等でのKQL(Kusto Query Language)によるログ検索が圧倒的に容易になります。


4. [LoggerMessage] によるゼロ・アロケーションログ

トレースだけでなく、標準の ILogger によるログ出力のパフォーマンスにもこだわるべきです。

従来の _logger.LogInformation("UserId: {Id}", userId) のような書き方は、内部でボクシング(Boxing)やオブジェクト配列の生成を伴い、ガベージコレクション(GC)の負荷を上げます。 本プロジェクトでは、C#のソースジェネレーター機能である [LoggerMessage] 属性を採用し、メモリアロケーションを一切発生させない高速なログ出力を実現しています。

C#

public partial class HomeController(ILogger<HomeController> logger) : Controller
{
    // コンパイル時に最適なログ出力コードが自動生成される
    [LoggerMessage(
        EventId = 1,
        Level = LogLevel.Information,
        Message = "ユーザーデータの取得を開始します。UserId: {UserId}")]
    private partial void LogUserDataFetchStarted(int userId);

    public async Task<IActionResult> Index()
    {
        int testUserId = 12345;
        
        // 呼び出し側はこれだけ。ボクシングもGC負荷もゼロ!
        LogUserDataFetchStarted(testUserId);
        
        // ... (後続のビジネスロジック)
    }
}

まとめ

OpenTelemetryは非常に強力なツールですが、実運用で活かすためには「何を記録し、何を捨てるか」という設計(計装戦略)が不可欠です。

今回紹介したカスタムプロセッサやパフォーマンスを意識したロギング手法は、Azure Monitorだけでなく、AWS X-RayやDatadogなど他のバックエンドを使用する場合でもそのまま応用できる汎用的なテクニックです。

ぜひ、プロジェクトに最適な監視基盤を構築してみてください。

👉 GitHubで完全なソースコードを見る

コメント

タイトルとURLをコピーしました