Azure Service Bus と .NET 8 バックグラウンドサービスの処理時間を Application Insights で可視化する方法

Azure Service Bus のコンシューマ処理を .NET 8 のバックグラウンドサービスで実装していると、「Web API の応答時間は Application Insights の[パフォーマンス]に出ているのに、コンシューマの平均処理時間が見えない…」という悩みによくぶつかります。本記事ではその理由と、Request テレメトリ化とカスタムメトリックの 2 パターンで、処理時間をきれいに可視化する具体的な実装手順を詳しく解説します。

目次

Azure Service Bus コンシューマの処理時間が[パフォーマンス]に出ない理由

まずは現状の整理から始めましょう。

  • .NET 8 の BackgroundService や IHostedService で Service Bus のメッセージを受信している。
  • AddApplicationInsightsTelemetryWorkerService(...) は設定済み。
  • Web API の HTTP リクエストは Application Insights の [パフォーマンス] ブレードに出ている。
  • しかし バックグラウンドのメッセージ処理 は[パフォーマンス]に出てこない。

これは、Application Insights が標準で収集するテレメトリの種類と、その扱い方が理由です。簡単に整理すると次のようになります。

テレメトリ種別主な用途[パフォーマンス]に出るか.NET Worker での自動収集
RequestHTTP リクエストやキュー処理などの「入口」○(メイン対象)HTTP(ASP.NET Core)は自動。バックグラウンドは手動で送信する必要あり
DependencyDB、Service Bus、HTTP クライアントなど外部サービスへの呼び出し一部ビューに表示多くは自動収集される
Traceログ出力(情報 / 警告 / エラーなど)直接[パフォーマンス]の対象にはならないILogger 経由で自動収集
MetricCPU、メモリ、カスタム KPI など[メトリック]で表示標準 + カスタムで自由に設計

ポイントは、[パフォーマンス]ブレードは基本的に「Request テレメトリ」を前提に作られているという点です。
バックグラウンド処理は HTTP リクエストではないため、何もせずに使うと Request テレメトリが作成されず、[パフォーマンス]に何も映らない、という状況になります。

そこで本記事では、次の 2 パターンのアプローチを整理します。

アプローチ概要向いているケース
A:Request テレメトリ化各メッセージ処理を RequestTelemetry として送信し、[パフォーマンス]に載せる運用チームが Web API と同じ画面で処理時間を見たい場合
B:メトリック / ログで可視化カスタムメトリックや KQL を使って柔軟に可視化・アラートする高度な分析やコスト最適化を重視したい場合

アプローチ A:メッセージ処理を Request テレメトリとして送信する

全体の考え方

アプローチ A では、「Service Bus の 1 メッセージ処理」を Web API の 1 リクエストに見立てる、という発想をとります。
具体的には、メッセージハンドラの先頭で TelemetryClient.StartOperation<RequestTelemetry> を呼び出し、処理が終わったタイミングで自動的に Duration や Success を記録させます。

こうすることで、Application Insights から見ると「HTTP かどうかはさておき、1 つのリクエスト」として扱われるため、次のようなメリットがあります。

  • [パフォーマンス]に「平均処理時間」「呼び出し回数」「失敗率」が表示される。
  • [失敗]ブレードで例外付きのメッセージ処理を簡単に絞り込める。
  • API の Request と同じ UI / 概念で運用でき、チーム間の認識をそろえやすい。

ServiceBusProcessor のメッセージハンドラ実装例

以下は、ServiceBusProcessor のハンドラ内で Request テレメトリを開始する例です。


using System.Diagnostics;
using Azure.Messaging.ServiceBus;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;

public class MessageProcessor
{
    private readonly TelemetryClient _telemetry;

    public MessageProcessor(TelemetryClient telemetry)
    {
        _telemetry = telemetry;
    }

    public async Task ProcessMessageAsync(ProcessMessageEventArgs args)
    {
        // Request 名をわかりやすく付ける
        var requestName = $"SB Process: {args.EntityPath}";

        // Request テレメトリ開始
        using var operation =
            _telemetry.StartOperation&lt;RequestTelemetry&gt;(requestName);

        var sw = Stopwatch.StartNew();

        try
        {
            // 処理対象メッセージの情報をプロパティとして付与(任意)
            operation.Telemetry.Properties["MessageId"] = args.Message.MessageId;
            operation.Telemetry.Properties["SessionId"] = args.Message.SessionId ?? string.Empty;
            operation.Telemetry.Properties["CorrelationId"] = args.Message.CorrelationId ?? string.Empty;
            operation.Telemetry.Properties["EnqueuedTimeUtc"] = args.Message.EnqueuedTime.ToString("o");

            // 実際のビジネスロジック
            await HandleAsync(args.Message);

            // 成功時の情報
            operation.Telemetry.Success = true;
            operation.Telemetry.ResponseCode = "OK";
        }
        catch (Exception ex)
        {
            // 失敗扱いにして例外テレメトリも送信
            operation.Telemetry.Success = false;
            operation.Telemetry.ResponseCode = "Error";

            // Request にひもづいた Exception として記録される
            _telemetry.TrackException(ex);

            // 再スローして Service Bus 側の再試行ポリシーに任せる
            throw;
        }
        finally
        {
            sw.Stop();

            // Duration は StartOperation / Dispose の時点で自動計測されるが、
            // より厳密にしたい場合は自前の Stopwatch で上書きも可能
            operation.Telemetry.Duration = sw.Elapsed;
        }
    }

    private Task HandleAsync(ServiceBusReceivedMessage message)
    {
        // TODO: メッセージの実処理
        return Task.CompletedTask;
    }
}

この実装を入れると、Application Insights の[パフォーマンス]画面で次のような Request 名が並ぶようになります。

  • SB Process: orders-queue
  • SB Process: payment-queue
  • SB Process: deadletter-queue

Request 名のつけ方は自由ですが、キュー名 / トピック名が一目でわかることと、API の Request 名と混ざりにくいことを意識すると運用が楽になります。

Cloud Role 名(コンシューマ側のサービス名)を設定する

Web API と Service Bus コンシューマが同じ Application Insights リソースに送信している場合、そのままだと同じ「アプリケーション」として表示されてしまいます。
視認性を上げるため、Cloud Role 名(アプリケーション名に相当)を明示的に設定しておきましょう。


// Program.cs

using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddApplicationInsightsTelemetryWorkerService(options =&gt;
{
    options.ConnectionString =
        builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
    // options.EnableAdaptiveSampling = true; // 既定値。必要に応じて調整
});

// Cloud Role 名を固定する TelemetryInitializer を登録
builder.Services.AddSingleton&lt;ITelemetryInitializer&gt;(
    new CloudRoleNameInitializer("consumer-service"));

var app = builder.Build();

// 省略...

app.Run();

public sealed class CloudRoleNameInitializer : ITelemetryInitializer
{
    private readonly string _roleName;

    public CloudRoleNameInitializer(string roleName)
    {
        _roleName = roleName;
    }

    public void Initialize(ITelemetry telemetry)
    {
        telemetry.Context.Cloud.RoleName = _roleName;
    }
}

Web API 側では web-api、コンシューマ側では consumer-service のように区別しておくと、Application Map や[パフォーマンス]のフィルタで簡単に切り替えられるようになります。

アプローチ A のメリット・デメリット

観点メリットデメリット / 注意点
可視性Web API と同じ[パフォーマンス]で平均処理時間を確認できるRequest 数が多いとデータ量が増え、コストに影響する可能性
学習コスト既存の Request ベースの監視と同じ概念で運用できるRequest = HTTP ではないので命名や設計を誤解されないように説明が必要
実装StartOperation をハンドラに追加するだけで比較的簡単すべてのハンドラで漏れなく実装されているかレビューが必要

アプローチ B:メトリック / ログで平均処理時間を可視化する

「[パフォーマンス]ブレードに出なくてもよいが、柔軟なダッシュボードやアラートを作りたい」「コストを細かくコントロールしたい」という場合は、カスタムメトリック + ログ(Kusto クエリ) を中心に設計するアプローチが有効です。

カスタムメトリックで「MessageProcessingTimeMs」を送る

メッセージ処理時間だけを、シンプルな数値メトリックとして送信する方法です。
キューごとの処理時間を見たい場合は、キュー名をディメンションとして追加しておくと便利です。


public async Task ProcessMessageAsync(ProcessMessageEventArgs args)
{
    var sw = Stopwatch.StartNew();
    try
    {
        await HandleAsync(args.Message);

        // 正常終了
    }
    catch (Exception ex)
    {
        // ログや Request の失敗処理は別途
        throw;
    }
    finally
    {
        sw.Stop();

        // キュー別のカスタムメトリック
        var metric =
            _telemetry.GetMetric("MessageProcessingTimeMs", "Queue");

        metric.TrackValue(sw.Elapsed.TotalMilliseconds, args.EntityPath);
    }
}

この実装を追加すると、Application Insights ポータルの[メトリック]ブレードで、次のように設定できます。

  1. スコープに対象の Application Insights リソースを選択。
  2. メトリックに「MessageProcessingTimeMs」を指定。
  3. 集計方法を「平均」に設定。
  4. 分割(ディメンション)に「Queue」を指定して、キューごとの平均処理時間を表示。

さらに、このメトリックに対してアラートを設定することで、

  • 特定のキューの平均処理時間が 1 分間平均で 5 秒を超えたら通知
  • 全体の平均処理時間が 10 分間で急上昇したらメール / Teams 通知

といった運用ルールを簡単に実現できます。

ログ(Kusto クエリ)で柔軟に分析する

アプローチ A で Request テレメトリを送る場合も、アプローチ B のみでカスタムメトリックを送る場合も、最終的には Kusto クエリ(KQL) を使うことでかなり柔軟な分析が可能になります。

データソース代表的な用途主なテーブル
Request テレメトリ(A)メッセージ処理時間、成功 / 失敗率、エラーの多いキューの特定requests
カスタムメトリック(B)キュー別平均処理時間、処理時間の分位数(p95, p99)customMetrics
依存関係テレメトリService Bus への送信 / 受信パフォーマンス、下流サービスの遅延dependencies

Request テレメトリ(アプローチ A)のクエリ例


requests
| where cloud_RoleName == "consumer-service"
| where name startswith "SB Process:"
| summarize
    AvgDurationMs = avg(duration),
    P95DurationMs = percentile(duration, 95),
    Count = count()
  by bin(timestamp, 5m), name
| order by timestamp desc

このクエリで、

  • コンシューマサービス(cloud_RoleName == "consumer-service")の中から、
  • SB Process: で始まる Request(メッセージ処理)だけを絞り込み、
  • 5 分ごとに平均処理時間 / 95 パーセンタイル / 回数を集計

という分析ができます。これをワークブックに貼り付ければ、キュー別の時間推移グラフを簡単に作成できます。

カスタムメトリック(アプローチ B)のクエリ例


customMetrics
| where name == "MessageProcessingTimeMs"
| extend Queue = tostring(customDimensions.Queue)
| summarize
    AvgDurationMs = avg(value),
    P95DurationMs = percentile(value, 95),
    Count = count()
  by bin(timestamp, 5m), Queue
| order by timestamp desc

このクエリでは、Queue ごとの平均処理時間と 95 パーセンタイルを 5 分ごとに可視化できます。ディメンション名を増やして、

  • テナント ID
  • 重要度(High / Low)
  • 処理結果種別(成功 / リトライ済み / デッドレター)

といった軸で集計すれば、ビジネス的にも意味のあるメトリックを作ることができます。

メッセージ滞留時間(キューに溜まっている時間)もあわせて測る

実運用では「処理時間」だけでなく、「メッセージがキューに滞留している時間」も重要な指標になります。
Service Bus のメッセージには EnqueuedTime が含まれているため、現在時刻との差をとるだけで滞留時間が計れます。


public async Task ProcessMessageAsync(ProcessMessageEventArgs args)
{
    var now = DateTimeOffset.UtcNow;
    var enqueuedTime = args.Message.EnqueuedTime;
    var queueLatency = (now - enqueuedTime).TotalMilliseconds;

    var latencyMetric =
        _telemetry.GetMetric("MessageQueueLatencyMs", "Queue");
    latencyMetric.TrackValue(queueLatency, args.EntityPath);

    // あとは処理時間の計測など...
}

これに対して KQL で分析すると、


customMetrics
| where name == "MessageQueueLatencyMs"
| extend Queue = tostring(customDimensions.Queue)
| summarize
    AvgLatencyMs = avg(value),
    P95LatencyMs = percentile(value, 95)
  by bin(timestamp, 5m), Queue
| order by timestamp desc

のように、「どのキューがどれだけ滞留しているか」を時系列で把握できます。
処理時間は短いのに滞留時間が長い場合は、「コンサンプションの並列度が足りない」「スケールアウトが追いついていない」などの別の問題を疑うことができます。

Program.cs の最小構成サンプル

ここまでの内容をまとめ、最小限の構成として次のような Program.cs をイメージしておくと、全体像がつかみやすくなります。


using Azure.Messaging.ServiceBus;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

// Application Insights(Worker Service 用)
builder.Services.AddApplicationInsightsTelemetryWorkerService(options =&gt;
{
    options.ConnectionString =
        builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
    // options.EnableAdaptiveSampling = true; // 既定のままで OK。必要なら無効化してロスを防ぐ
});

// Cloud Role 名を設定
builder.Services.AddSingleton&lt;ITelemetryInitializer&gt;(
    new CloudRoleNameInitializer("consumer-service"));

// Service Bus クライアント
builder.Services.AddSingleton(sp =&gt;
{
    var connectionString = builder.Configuration["SERVICEBUS_CONNECTION_STRING"];
    return new ServiceBusClient(connectionString);
});

// メッセージプロセッサ
builder.Services.AddSingleton&lt;MessageProcessor&gt;();

// バックグラウンドサービス登録
builder.Services.AddHostedService&lt;ServiceBusWorker&gt;();

var app = builder.Build();
await app.RunAsync();

// Cloud Role 名 Initializer
public sealed class CloudRoleNameInitializer : ITelemetryInitializer
{
    private readonly string _roleName;
    public CloudRoleNameInitializer(string roleName) =&gt; _roleName = roleName;

    public void Initialize(ITelemetry telemetry)
        =&gt; telemetry.Context.Cloud.RoleName = _roleName;
}

// Service Bus Worker
public sealed class ServiceBusWorker : BackgroundService
{
    private readonly ServiceBusClient _client;
    private readonly MessageProcessor _processor;
    private ServiceBusProcessor? _sbProcessor;

    public ServiceBusWorker(ServiceBusClient client, MessageProcessor processor)
    {
        _client = client;
        _processor = processor;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _sbProcessor = _client.CreateProcessor(
            queueName: "sample-queue",
            new ServiceBusProcessorOptions
            {
                MaxConcurrentCalls = 10,
                AutoCompleteMessages = false
            });

        _sbProcessor.ProcessMessageAsync += args =&gt; _processor.ProcessMessageAsync(args);
        _sbProcessor.ProcessErrorAsync += ErrorHandlerAsync;

        await _sbProcessor.StartProcessingAsync(stoppingToken);

        // ホストが止まるまで待機
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }

    private Task ErrorHandlerAsync(ProcessErrorEventArgs args)
    {
        // Application Insights の ILogger 経由でログ
        Console.WriteLine(args.Exception.ToString());
        return Task.CompletedTask;
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        if (_sbProcessor != null)
        {
            await _sbProcessor.StopProcessingAsync(cancellationToken);
            await _sbProcessor.DisposeAsync();
        }
        await base.StopAsync(cancellationToken);
    }
}

この構成に、先ほどの Request テレメトリ化(アプローチ A)やカスタムメトリック(アプローチ B)を組み合わせていくイメージです。

よくあるハマりどころとチェックリスト

実際に運用していると、「データが出てこない」「思ったより数値が少ない / 多い」といったトラブルに遭遇します。代表的なチェックポイントを表にまとめておきます。

症状原因候補確認ポイント
[パフォーマンス]に何も出ないRequest テレメトリが送られていないStartOperation<RequestTelemetry> を呼んでいるか Cloud Role 名でフィルタしすぎていないか
Web API しか見えずコンシューマが見えない別の Application Insights リソースに送っているConnectionString が API / コンシューマで同じか インストルメンテーションキーを間違えていないか
データが間引かれている気がするアダプティブ サンプリングの影響EnableAdaptiveSampling の設定値 高スループットなキューではサンプリング無効化も検討
そもそもデータが一切来ないアウトバウンドのファイアウォール / プロキシ設定Azure Monitor の送信先エンドポイントが許可されているか アプリケーションの起動ログに送信エラーが出ていないか
API とコンシューマが混ざって見づらいCloud Role 名が未設定ITelemetryInitializer で RoleName を明示しているか API とコンシューマで別の Role 名を使っているか

ダッシュボード構成の一例

最後に、実運用でよく使われるダッシュボード構成の一例を紹介します。Azure Portal の「ワークブック」や「ダッシュボード」に貼り付けておくと、監視が楽になります。

レベル 1:日常監視用のシンプルビュー

  • キュー別平均処理時間(直近 1 時間 / 24 時間)
  • キュー別メッセージ滞留時間(平均 / p95)
  • Request の失敗率(Success == false の割合)
  • 例外数トップ N(例外の種類 / メッセージ)

ここでは詳細なドリルダウンは行わず、「赤くなったら深掘りする」という運用を想定します。

レベル 2:トラブルシューティング用の詳細ビュー

  • 特定キューの Request 一覧(処理時間長い順)
  • 該当 Request に紐づく Dependency(DB / 外部 API)のトレース
  • CorrelationId や MessageId でのクロスコンポーネント検索

このレベルでは、Request と Dependency の関連性が重要になるため、メッセージに含まれる CorrelationId を API 側と合わせる設計にしておくと、原因究明がかなり楽になります。

どちらのアプローチを選ぶべきか?

最後に、アプローチ A(Request 化)と B(メトリック / ログ)の選び方を整理しておきます。

状況おすすめの方針
まずは「見える化」することが最優先A:Request 化 を優先。最小限のコード追加で[パフォーマンス]に載せる
運用チームが Web API の[パフォーマンス]に慣れているやはり A:Request 化 がスムーズ。API と同じ UI で監視できる
高トラフィックでテレメトリコストを強く意識したいB:メトリック / ログ中心 に設計し、必要なメトリックだけを送る
滞留時間やビジネス指標まで含めて細かく分析したいRequest + カスタムメトリックのハイブリッド。A と B を組み合わせる

実務的には、次のようなステップがおすすめです。

  1. アプローチ A(Request 化)を導入して、まず「処理時間」と「失敗」を見える化する。
  2. 運用の中で「何が知りたいか」がはっきりしてきたら、アプローチ B のカスタムメトリックを追加していく。
  3. 最終的に、処理時間・滞留時間・失敗率・例外の中身を一つのダッシュボードにまとめる。

ここまで実装できれば、Azure Service Bus コンシューマ(.NET 8 バックグラウンドサービス)の動きが Application Insights 上で「Web アプリと同じ感覚」で把握できるようになり、障害対応や性能チューニングのスピードが大きく向上します。
ぜひ、まずは Request テレメトリ化から試してみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次