分散トレーシングアーキテクチャ
トレーシング階層(4層)
フロントエンドからバックエンドまで、4つの階層でトレーシングを構成する:
| 階層 | ID 種別 | 説明 | 生成元 |
|---|---|---|---|
| Session | Session ID (UUID) | ユーザーセッション単位。Sentry Session Replay の単位 | Sentry SDK |
| Route | Route Span | TanStack Router のルート遷移単位。画面表示の所要時間を計測 | OpenTelemetry + Router hook |
| User Action | Action Span | ボタンクリック等のユーザー操作単位。複数の API 呼び出しの親 Span | 手動計装 |
| API Call | traceparent ヘッダー | fetch() 単位。バックエンドの Activity として継続 | OpenTelemetry FetchInstrumentation |
TraceId 伝播フロー
ユーザー操作からバックエンド処理、エラー表示までの TraceId 伝播の全体フローを示す:
バックエンド 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;
}