2026.01.20Observability

trace_id を最初に通しておくと、後で必ず楽になる

CMスポット PoC をやっていて、自分の中で一番「最初から入れてよかった」と思っているのが trace_id の伝播。Main Agent + 6 Sub Agent の構成で、Frontend → Backend → 各 Agent → LLM 呼び出しまで一気通貫で trace_id を通すようにした。これだけで、後の調査速度が桁違いに変わる。

最初に入れないと、絶対に後付けになる

PoC は仕様が動く。Agent が増える。LLM 呼び出しが入れ子になる。動いてから「あれ、どのリクエストが詰まったんだ?」を調べる段階で、trace_id が無いと grep ですら追えない。

trace_id は 後付けがダルい。途中の関数全部に伝播の引数を足し直すことになる。だから「最初に通しておく」だけが現実解。

通す場所

最低限ここを貫通させれば十分。

W3C の traceparent フォーマットに乗せておけば、後で OpenTelemetry SDK を入れることになっても破綻しない。PoC のうちは ID を伝播させるだけで十分。

最小実装(FastAPI)

middleware で受け口を作って contextvars に持つ。Agent 間で受け渡しするときは、ログとプロンプトに混ぜるだけ。

import contextvars
from uuid import uuid4
from fastapi import FastAPI, Request

trace_id_ctx = contextvars.ContextVar('trace_id', default='')

app = FastAPI()

@app.middleware('http')
async def trace_middleware(request: Request, call_next):
    incoming = request.headers.get('traceparent')
    trace_id = (
        incoming.split('-')[1] if incoming else uuid4().hex
    )
    trace_id_ctx.set(trace_id)
    return await call_next(request)

@app.get('/agents/run')
async def run():
    trace_id = trace_id_ctx.get()
    logger.info({
        'traceId': trace_id,
        'evt': 'agents.start',
    })
    # ... Main Agent → 6 Sub Agent

これだけで、後の grep が一発で済むようになる。

$ cat app.log | jq 'select(.traceId == "9f2a1c…")'

PoC のうちは Sentry も OTel Collector もいらない。 ID と JSON 構造さえ揃えておけば、jq で十分追える


「後で楽することに先に投資する」種類のコードがあって、trace_id はその代表例。CMスポット PoC で一番リターンが大きかった設計判断だった気がする。