Claude Media
Claude Codeでシーケンス図をMermaidで起こす — 実行トレースとコードの食い違い検出

Claude Codeでシーケンス図をMermaidで起こす — 実行トレースとコードの食い違い検出

テスト実行時のOpenTelemetryトレースを小さなスクリプトでMermaidのsequenceDiagramに変換し、Claude Codeにコードと突き合わせて食い違いを指摘させる手順です。テスト直後に図を作るhookも載せます。

設計書のシーケンス図は、コードが変わるたびに実態から離れていきます。テストを1回走らせたときのOpenTelemetryトレースから図を起こせば、実際に起きた呼び出しの順序がそのまま手に入ります。その図を設計意図やコードと突き合わせる作業に、Claude Codeが向いています。

この記事では、OTLP形式のトレースをMermaidのsequenceDiagramに変換するスクリプトを用意し、Claude Codeにコードとの食い違いを指摘させる流れを示します。テストが終わるたびに図を作るhookと、図を作る段階で踏みやすいつまずきも扱います。

トレース起点の図は、設計書の図と何が違うのか

コードや設計書から描いた図は、作者が「こう動くはず」と考えた構造です。トレースから起こした図は、あるテスト1回で実際に通った経路です。両者のずれが、調べる価値のある箇所になります。

ただし、トレースから起こした図が全体像になるわけではありません。トレースに映るのは、そのテストが通した分岐だけです。条件付きの呼び出しがトレースに無くても、コードに無いとは言えません。逆にトレースにあってコードに見当たらなければ、ミドルウェアやHTTPクライアントのリトライ設定など、別の層が呼んでいる可能性を疑います。

この非対称性が、依頼の書き方を決めます。Claude Codeに「図とコードが一致しているか」を問うのではなく、「トレースにあってコードに説明が付かない呼び出し」と「順序の食い違い」を探させます。

材料にするトレースの形を決める

変換の入力は、OTLPのJSON表現です。OTLP/JSONには、通常のProtobufのJSON変換と異なる決まりがいくつかあります。スクリプトを書く前に押さえておくと、読み違いを防げます。

  • traceIdとspanIdはbase64ではなく、16進文字列で表される
  • enumは名前ではなく整数で表される。kindがSPAN_KIND_SERVERなら2
  • 64ビット整数は10進の文字列で表される。startTimeUnixNanoもこの扱い
  • キー名はlowerCamelCaseになる。parent_span_idではなくparentSpanId

スパンの種類(kind)は、1がINTERNAL、2がSERVER、3がCLIENT、4がPRODUCER、5がCONSUMERです。ルートスパンのparentSpanIdは空になります。

この記事のスクリプトは、resourceSpansの中にサービス名(service.name)をリソース属性として持つ、次の形のJSONを読みます。以降の例では、注文APIが在庫・決済・クーポンの各サービスを呼ぶテストのトレースを使います。サンプル用に手で作った値です。

{
  "resourceSpans": [
    {
      "resource": {
        "attributes": [
          { "key": "service.name", "value": { "stringValue": "order-api" } }
        ]
      },
      "scopeSpans": [
        {
          "spans": [
            {
              "traceId": "5b8efff798038103d269b633813fc60c",
              "spanId": "b2",
              "parentSpanId": "b1",
              "name": "reserve",
              "kind": 3,
              "startTimeUnixNano": "1700000000010000000",
              "endTimeUnixNano": "1700000000060000000",
              "status": { "code": 0 }
            }
          ]
        }
      ]
    }
  ]
}

トレースをJSONファイルに落とす方法は、使っているSDKやコレクターによって変わります。テスト用のインメモリエクスポーターから書き出す構成でも、コレクターのファイル出力でも、上の形に揃えば同じスクリプトで読めます。

スパンをsequenceDiagramに変換するスクリプト

トレースが数十スパンを超えると、LLMに直接読ませて図を書かせるのは割に合いません。親子関係と時刻の整合を毎回正しく保てる保証がなく、同じ入力でも図が変わります。そこで、図を作る部分は決定的なスクリプトにし、Claude Codeには図を読んで判断する部分を任せます。

変換の考え方は単純です。

トレース側Mermaid側
service.nameMermaid側participant
親と別サービスにあるスパンの開始Mermaid側呼び出しの矢印(->>+)
そのスパンの終了Mermaid側戻りの矢印(-->>-)
ステータスコード2(ERROR)Mermaid側戻りのラベルをERRORにする

同じサービスの中のスパン(関数呼び出しやDBアクセスの内部スパン)は、図が長くなりすぎるので落とします。サービスをまたぐ呼び出しだけを描くのが、食い違いを見つけやすい粒度です。

import json, sys
 
data = json.load(open(sys.argv[1]))
svc_of, spans = {}, []
for rs in data["resourceSpans"]:
    attrs = {a["key"]: a["value"].get("stringValue", "")
             for a in rs["resource"]["attributes"]}
    for ss in rs["scopeSpans"]:
        for s in ss["spans"]:
            svc_of[s["spanId"]] = attrs["service.name"]
            s["svc"] = attrs["service.name"]
            spans.append(s)
 
events = []  # (時刻, 種別, span)
for s in spans:
    parent = s.get("parentSpanId")
    if parent and svc_of.get(parent) not in (None, s["svc"]):
        events.append((int(s["startTimeUnixNano"]), 1, s))
        events.append((int(s["endTimeUnixNano"]), 0, s))
events.sort(key=lambda e: (e[0], e[1]))
 
def pid(name):  # 矢印の記号と紛れないよう、ハイフンは ID から外す
    return name.replace("-", "_")
 
lines = ["sequenceDiagram", "    autonumber"]
seen = []
for _, _, s in events:
    for name in (svc_of[s["parentSpanId"]], s["svc"]):
        if name not in seen:
            seen.append(name)
            lines.append(f"    participant {pid(name)} as {name}")
for _, is_call, s in events:
    caller = pid(svc_of[s["parentSpanId"]])
    callee = pid(s["svc"])
    if is_call:
        lines.append(f"    {caller}->>+{callee}: {s['name']}")
    else:
        mark = "ERROR" if s.get("status", {}).get("code") == 2 else "OK"
        lines.append(f"    {callee}-->>-{caller}: {mark}")
print("\n".join(lines))

ハイフン入りのサービス名は、participant order_api as order-apiのように別名(エイリアス)を付けて受けます。Mermaidではasで、短い識別子と表示用のラベルを分けられます。矢印の記号にも-が使われるので、識別子側からハイフンを外しておけば、読み違いの余地がなくなります。

用意したサンプルのトレースで実行すると、次の出力になります。

python3 scripts/trace2mmd.py traces/last.json > traces/last.mmd
cat traces/last.mmd
sequenceDiagram
    autonumber
    participant test_runner as test-runner
    participant order_api as order-api
    participant inventory as inventory
    participant payment as payment
    participant coupon as coupon
    test_runner->>+order_api: POST /orders
    order_api->>+inventory: Inventory.Reserve
    inventory-->>-order_api: OK
    order_api->>+payment: Payment.Charge
    payment-->>-order_api: ERROR
    order_api->>+payment: Payment.Charge
    payment-->>-order_api: OK
    order_api->>+coupon: Coupon.Lookup
    coupon-->>-order_api: OK
    order_api-->>-test_runner: OK

autonumberで矢印に番号が付くので、Claude Codeとのやり取りで「3番と5番」のように箇所を指せます。->>+と-->>-の組は、Mermaidの活性化(アクティベーション)の省略記法で、呼び出し中のサービスに縦の帯が描かれます。

この図で目に付くのは3点です。在庫確保が決済より先に起きていること、決済が2回呼ばれて1回目がERRORであること、クーポン参照が最後に走っていることです。

並行呼び出しは図に出さない

上のスクリプトは、開始時刻の順に矢印を並べます。2つのサービスを同時に呼ぶ実装では、時間が重なるスパンも直列の矢印になり、実装とは違う順序の印象を与えます。

Mermaidには並行を表すpar ... and ... endがあります。重なりを検出してparに包む処理を足すこともできますが、最初から作り込む必要はありません。図が直列に見えるときは、Claude Codeへの依頼に「時刻が重なるスパンは並行として扱って」と添えれば、parで書き直させられます。その場合でも、重なりの判定に使った時刻は、元のトレースのスパンから読ませます。

Claude Codeに食い違いを指摘させる

図ができたら、コードの該当箇所と並べて依頼します。ここでは注文APIの処理が、次のように書かれているとします(サンプルです)。

def create_order(req):
    payment_client.charge(req.amount)
    inventory_client.reserve(req.items)
    if req.coupon_code:
        coupon_client.lookup(req.coupon_code)

セッションでは、たとえば次のように頼みます。

traces/last.mmd は、テスト1回分の実行トレースから起こしたシーケンス図。
order-api の create_order と、図の呼び出しを突き合わせて。
- 図にあってコードから説明が付かない呼び出し
- コードと図で順序が違う呼び出し
この2種類を、根拠のファイル名と行番号つきで挙げて。
- コードにあって図に無い呼び出しは、まず「このテストが通さなかった分岐」
  の可能性を考えて、テストコードで確かめてから報告して
- 推測と確認済みの事実は分けて書いて

最後の2項目が、この依頼の要点です。トレースに無いという事実だけでは、コードの誤りにはなりません。テストが分岐を通したかどうかを確かめさせると、誤検知が減ります。

返ってくる内容の例を、表の形で示します。実際の出力ではなく、このサンプルに対して期待できる指摘の形です。

種別指摘次に確かめること
順序指摘コードは決済→在庫確保だが、図は在庫確保→決済次に確かめること在庫確保を呼ぶ別の層(デコレータやミドルウェア)の有無
過剰な呼び出し指摘決済が2回あり、1回目がERROR次に確かめることHTTPクライアントのリトライ設定。コードに再試行の記述が無い
条件付き指摘クーポン参照はcoupon_codeがある場合のみ次に確かめることテストがcoupon_codeを渡しているか。これだけでは食い違いと言えない

3行目が「指摘しない」ための行です。コードのifの中にある呼び出しがトレースに現れたのは、テストが条件を満たしたからで、食い違いではありません。

順序の食い違いが見つかったとき、設計意図と実装のどちらが正しいかはトレースからは決まりません。決済の失敗時に在庫を戻す補償処理があるかどうかは、図の外で確かめる事項です。Claude Codeには、補償処理の有無をコードから探させ、見つからなければその旨を報告させます。

テストの直後に自動で図を作るhook

テストを走らせるたびに変換コマンドを手で打つのは面倒です。hookで自動化します。hooksの仕組みの基本は別の記事にあり、ここでは今回の用途に必要な設定だけを扱います。

使うイベントはPostToolUseです。Bashツールのnpm testの完了後に発火し、入力にはツールの引数(tool_input)と結果(tool_response)が入ります。発火の絞り込みは、matcherでツール名を、ifでコマンドの形を指定します。ifには権限ルールの書式を1つだけ書けます。

注意点が1つあります。PostToolUseは、ツールが成功して完了したときだけ動きます。テストが失敗した回は、PostToolUseFailureが発火します。失敗したテストのトレースこそ読みたいので、同じhookを両方のイベントに登録します。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(npm test*)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/trace-diagram.sh"
          }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(npm test*)",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/trace-diagram.sh"
          }
        ]
      }
    ]
  }
}

この設定は.claude/settings.jsonに書きます。スクリプトの参照には${CLAUDE_PROJECT_DIR}を使うと、Claudeが別のディレクトリに移動していても、プロジェクトルートのスクリプトを呼べます。

hookのスクリプトは、標準入力でイベントのJSONを受け取ります。ここでは変換して、結果のパスをClaudeに伝えます。

#!/bin/bash
# テスト直後に、新しいトレースがあれば Mermaid に変換して Claude に知らせる
EVENT=$(jq -r '.hook_event_name')
TRACE="${CLAUDE_PROJECT_DIR}/traces/last.json"
OUT="${CLAUDE_PROJECT_DIR}/traces/last.mmd"
 
# 1分以内に更新されたトレースだけを対象にする(古い図を見せない)
[ -n "$(find "$TRACE" -mmin -1 2>/dev/null)" ] || exit 0
python3 "${CLAUDE_PROJECT_DIR}/scripts/trace2mmd.py" "$TRACE" > "$OUT" || exit 0
 
jq -n --arg event "$EVENT" --arg path "$OUT" '{
  hookSpecificOutput: {
    hookEventName: $event,
    additionalContext: ("直前のテストのトレースを " + $path + " に図にしました。")
  }
}'

additionalContextに入れた文字列は、システムリマインダーとして、ツールの結果の隣でClaudeの文脈に追加されます。図そのものではなくファイルのパスだけを渡すのは、大きな図で文脈を埋めないためです。Claudeが必要と判断したときに、そのファイルを読みます。

更新時刻を確かめる行は、テストがトレースを書き出さなかった回の対策です。古いlast.jsonが残っていると、前回のテストの図を今回のものとして見せてしまいます。

Claude Code自身のトレースとの関係

Claude Code自体も、OpenTelemetryのトレースを出せます。ただし、ベータの機能です。CLAUDE_CODE_ENABLE_TELEMETRY=1とCLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1を設定し、OTEL_TRACES_EXPORTERで送信先を選びます。ユーザーのプロンプトは1回ごとにclaude_code.interactionをルートとするトレースになり、API呼び出し(claude_code.llm_request)とツール呼び出し(claude_code.tool)がその子として記録されます。

この記事で扱うのは、テスト対象のアプリが出すトレースです。両者は別物ですが、つなぐ手段はあります。トレースが有効なとき、BashやPowerShellの子プロセスにはTRACEPARENT環境変数が渡されます。W3Cトレースコンテキスト形式で、実行中のツールのスパンを指します。テスト対象のアプリがこの変数を読んで親として使う実装なら、アプリのスパンはClaude Codeのツール実行スパンの下に並びます。

つなぐと、「どのプロンプトで走ったテストの、どの呼び出しか」を1つのトレースで追えます。つながなくても、この記事の手順は成立します。Claude Codeのセッション側をトレースで見る方法は、Langfuseプラグインでセッションをトレースする手順にあります。

Claude Code側のスパンは、既定ではプロンプトの本文、ツールの入力の詳細、ツールの内容を隠します。含めるにはOTEL_LOG_USER_PROMPTS=1、OTEL_LOG_TOOL_DETAILS=1、OTEL_LOG_TOOL_CONTENT=1を設定します。送信先に機微な内容が流れるため、必要になるまで有効にしない設定です。

図を作る段階でのつまずき

複数のホストをまたぐと、時刻の順序が崩れる

スパンの開始時刻は、スパンを作ったマシンの時計で記録されます。サーバー側のスパンの開始時刻も、そのサーバーの時計です。ホスト間で時計がずれていると、時刻で並べ替えたときに、呼び出しの前に戻りが来るような図になります。

テストをすべて1台の開発機のコンテナで動かしているなら、この問題は起きません。複数のホストにまたがるテスト環境では、並び替えをやめて、親子関係の木を深さ優先で辿る方式に変えるほうが安全です。

非同期のメッセージは、親子では表せない

PRODUCERとCONSUMERのスパンは、メッセージブローカーを挟む呼び出しです。ブローカーに送った側の処理が終わっても、受け側の処理は続きます。呼び出しと戻りを対にした図に押し込むと、戻りの無い呼び出しが活性化したまま残ります。

Mermaidには、非同期を表す開いた矢印-)があります。kindが4のスパンは-)で描き、戻りの矢印を出さない分岐をスクリプトに足すと、実態に近づきます。

リトライは別のスパンとは限らない

サンプルのトレースでは、決済の2回の呼び出しが別々のスパンでした。計装によっては、リトライが1つのスパンの属性やイベントに畳まれ、呼び出し回数が図に出ません。図に出ないリトライを疑うなら、スパンの属性に試行回数を持つものがないかを、元のJSONで探します。

図が大きくなりすぎる

1回のテストが数百の呼び出しを含むと、図は読めなくなります。対象を絞る手は2つあります。ルートのスパン名で1つのリクエストに限定する方法と、テスト1本ずつ別のトレースファイルに出す方法です。Claude Codeに見せる図は、1枚あたり数十本の矢印に収まるくらいが読みやすい大きさです。

Mermaidの記法で壊れる文字

メッセージ文にセミコロンがあると、Mermaidは改行の代わりとして扱います。含めたいときは#59;と書きます。endという単語もキーワードと衝突して、図が壊れる原因になります。スパン名がendだけの場合は、括弧や引用符で囲みます。HTTPのパスや関数名をそのまま矢印のラベルにするなら、変換時にこれらの文字を置き換える処理を入れておくと安全です。

図の構文を書き出し前に確かめる自動化は、PlantUMLの構文検証をhookで回す手順と同じ考え方で組めます。データベース側の図は、DDLからER図を起こす手順で扱っています。

まとめ

トレースから起こした図は、設計書の図が語らない「実際に起きたこと」を示します。ただし、見えているのはそのテストが通した経路だけです。食い違いを探すときは、トレースにあってコードで説明が付かないものと、順序が違うものに絞ります。トレースに無いものは、まずテストが分岐を通したかを疑います。

図を作る部分はスクリプトで決定的にし、読んで判断する部分をClaude Codeに任せる分担にすると、同じトレースから毎回同じ図が得られます。テストが終わるたびにhookで図を作っておけば、失敗したテストの直後に、Claude Codeがその図を読んで原因の絞り込みに入れます。テストの失敗をPlaywrightのトレースから切り分ける進め方は、Playwright traceでテスト失敗の原因を切り分ける手順に別の入口があります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks
  2. code.claude.com/docs/en/monitoring-usage
  3. mermaid.js.org/syntax/sequenceDiagram.html
  4. opentelemetry.io/docs/specs/otlp
  5. github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry…
この記事を共有:XはてブLinkedIn