Skip to content

分散トレーシングアーキテクチャ

トレーシング階層(4層)

フロントエンドからバックエンドまで、4つの階層でトレーシングを構成する:

階層ID 種別説明生成元
SessionSession ID (UUID)ユーザーセッション単位。Sentry Session Replay の単位Sentry SDK
RouteRoute SpanTanStack Router のルート遷移単位。画面表示の所要時間を計測OpenTelemetry + Router hook
User ActionAction Spanボタンクリック等のユーザー操作単位。複数の API 呼び出しの親 Span手動計装
API Calltraceparent ヘッダーfetch() 単位。バックエンドの Activity として継続OpenTelemetry FetchInstrumentation
Session 層Route 層User Action 層API Call 層Session ID (UUID)Sentry Session Replay の単位Route SpanTanStack Router ルート遷移単位画面表示の所要時間を計測Action Spanボタンクリック等のユーザー操作単位複数 API 呼び出しの親 Spantraceparent ヘッダーfetch() 単位バックエンドの Activity として継続
Session 層Route 層User Action 層API Call 層Session ID (UUID)Sentry Session Replay の単位Route SpanTanStack Router ルート遷移単位画面表示の所要時間を計測Action Spanボタンクリック等のユーザー操作単位複数 API 呼び出しの親 Spantraceparent ヘッダーfetch() 単位バックエンドの Activity として継続

TraceId 伝播フロー

ユーザー操作からバックエンド処理、エラー表示までの TraceId 伝播の全体フローを示す:

alt [正常レスポンス][エラーレスポンス]ボタンクリックAction Span 開始fetch() + traceparent ヘッダーHTTP リクエスト (traceparent 転送)構造化ログ (TraceId 付与)レスポンス + X-Trace-Id ヘッダーレスポンス + X-Trace-IdSpan 終了ApiError に TraceId を格納エラー送信 (TraceId タグ付き)エラー UI に TraceId 表示ユーザー操作Frontend (React)OpenTelemetry SDKBFF ServerApplication APISentryCloud Logging
alt [正常レスポンス][エラーレスポンス]ボタンクリックAction Span 開始fetch() + traceparent ヘッダーHTTP リクエスト (traceparent 転送)構造化ログ (TraceId 付与)レスポンス + X-Trace-Id ヘッダーレスポンス + X-Trace-IdSpan 終了ApiError に TraceId を格納エラー送信 (TraceId タグ付き)エラー UI に TraceId 表示ユーザー操作Frontend (React)OpenTelemetry SDKBFF ServerApplication APISentryCloud Logging

バックエンド TraceId ミドルウェア

すべての HTTP レスポンスに X-Trace-Id ヘッダーを付与するミドルウェア:

csharp
public class TraceIdResponseMiddleware
{
    private readonly RequestDelegate _next;

    public TraceIdResponseMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Activity.Current は OpenTelemetry / W3C Trace Context により自動設定される
        var traceId = Activity.Current?.TraceId.ToString()
            ?? context.TraceIdentifier;

        // レスポンスヘッダーに TraceId を付与
        context.Response.OnStarting(() =>
        {
            context.Response.Headers["X-Trace-Id"] = traceId;
            return Task.CompletedTask;
        });

        await _next(context);
    }
}

CORS 設定で X-Trace-Id ヘッダーをフロントエンドに公開する:

csharp
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(policy =>
    {
        policy
            .WithOrigins("https://app.tashika.example")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .WithExposedHeaders("X-Trace-Id"); // フロントエンドから参照可能にする
    });
});

フロントエンド TraceId 取得

TanStack Query の queryFn / mutationFn でエラーレスポンスから X-Trace-Id を抽出し、ApiError クラスに格納する:

typescript
export class ApiError extends Error {
  readonly status: number;
  readonly traceId: string | null;
  readonly errorCode: string | null;
  readonly problemDetails: ProblemDetails | null;

  constructor(
    message: string,
    status: number,
    traceId: string | null,
    errorCode: string | null,
    problemDetails: ProblemDetails | null,
  ) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.traceId = traceId;
    this.errorCode = errorCode;
    this.problemDetails = problemDetails;
  }
}

async function fetchWithTraceId(
  input: RequestInfo | URL,
  init?: RequestInit,
): Promise<Response> {
  const response = await fetch(input, init);

  if (!response.ok) {
    const traceId = response.headers.get("X-Trace-Id");
    let problemDetails: ProblemDetails | null = null;

    try {
      problemDetails = (await response.json()) as ProblemDetails;
    } catch {
      // JSON パース失敗時は problemDetails を null のまま
    }

    throw new ApiError(
      problemDetails?.detail ?? `HTTP ${response.status}`,
      response.status,
      traceId ?? problemDetails?.traceId ?? null,
      problemDetails?.errorCode ?? null,
      problemDetails,
    );
  }

  return response;
}