Agent SDKをOpenTelemetryで可観測化する実装ガイド
Agent SDKのテレメトリはCLIが計装する仕組みで、SDKは環境変数経由で設定を橋渡しするだけです。有効化からトレース連結、機微データ制御までを実装コードで説明します。
Agent SDKのテレメトリはどこから来るか
Agent SDKはClaude Code CLIを子プロセスとして起動し、ローカルのパイプで通信します。テレメトリを作っているのはSDK自身ではなく、この子プロセスのCLIです。CLIはモデルリクエストとツール実行の周りにスパンを記録し、トークンとコストのカウンターをメトリクスとして出し、プロンプトとツール結果を構造化ログイベントとして出します。SDKはこの設定を子プロセスへ橋渡しするだけで、独自にテレメトリを生成しません。
橋渡しの経路は環境変数です。子プロセスは既定でアプリケーションの環境変数を継承するため、設定できる場所は2つに分かれます。
環境変数を置く場所は2つ
プロセス環境
シェル・コンテナ・オーケストレーターで、アプリの起動前に変数を設定します。query()のどの呼び出しにもコード変更なしで反映されます。
呼び出しごとのoptions
同一プロセス内で複数のエージェントに別々のテレメトリ設定を持たせたいときに使います。Pythonのenvは継承環境にマージされますが、TypeScriptのoptions.envは継承環境を丸ごと置き換えます。
CLIは3種類のシグナルを独立に出力します。メトリクスはトークン・コスト・セッション数などのカウンターです。ログイベントはプロンプトごとのAPIリクエストやツール結果の構造化レコード、トレースは各インタラクション・モデルリクエスト・ツール呼び出し・フックのスパンです。トレースだけはベータ扱いで、有効化に専用のフラグが要ります。Messages APIを直接呼ぶアプリ側のトレースは別の仕組みで、Claude APIのOpenTelemetryで扱っています。
前提条件と有効化の最小構成
テレメトリは既定で無効です。CLAUDE_CODE_ENABLE_TELEMETRY=1を設定し、シグナルごとに少なくとも1つのエクスポーターを選ぶまで何も送信されません。もっとも一般的な構成は、3シグナルすべてをOTLP HTTPでコレクターへ送る形です。
import { query } from "@anthropic-ai/claude-agent-sdk";
const otelEnv = {
CLAUDE_CODE_ENABLE_TELEMETRY: "1",
// トレースはベータのため必須。メトリクスとログイベントには不要
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA: "1",
OTEL_TRACES_EXPORTER: "otlp",
OTEL_METRICS_EXPORTER: "otlp",
OTEL_LOGS_EXPORTER: "otlp",
OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf",
OTEL_EXPORTER_OTLP_ENDPOINT: "http://collector.example.com:4318",
OTEL_EXPORTER_OTLP_HEADERS: "Authorization=Bearer your-token",
};
for await (const message of query({
prompt: "List the files in this directory",
// TypeScriptのenvは継承環境を置き換えるため、先にprocess.envを展開する
options: { env: { ...process.env, ...otelEnv } },
})) {
console.log(message);
}送信を確認するには、コレクターのログでスパン・メトリクス・ログイベントの着信を見るのが確実です。メトリクスなら、セッション開始時に出るカウンターclaude_code.session.countの着信を見ます。ログイベントのみの構成なら、claude_code.user_promptイベントです。
CLIはエクスポートエラーを既定で握りつぶします。エンドポイントに到達できなくても、コレクターがデータを拒否しても、エージェントは正常に動き続け、テレメトリだけが黙って失われます。エラーを表に出すには、エクスポーター変数と一緒にCLAUDE_CODE_OTEL_DIAG_STDERR=1(Claude Code v2.1.179以降)を設定します。診断ログはSDKのstderrコールバック(Python)またはstderrオプション(TypeScript)で読めます。
短命な呼び出しでテレメトリを取りこぼさないための調整
CLIはテレメトリをバッチにまとめ、一定間隔でエクスポートします。プロセスが正常終了するときはCLIが保留データのフラッシュを試みますが、そのフラッシュ自体が短いタイムアウトで打ち切られるため、コレクターの応答が遅いとスパンが欠落することがあります。プロセスがCLIのシャットダウン前にkillされた場合は、バッファに残っていたデータがそのまま失われます。
既定のエクスポート間隔
メトリクス
60秒
OTEL_METRIC_EXPORT_INTERVAL
トレース
5秒
OTEL_TRACES_EXPORT_INTERVAL
ログ
5秒
OTEL_LOGS_EXPORT_INTERVAL
単発タスクでは3つとも縮めて、実行中にデータを送り切ります。
OTEL_ENV = {
# ... 前節のエクスポーター設定 ...
"OTEL_METRIC_EXPORT_INTERVAL": "1000",
"OTEL_LOGS_EXPORT_INTERVAL": "1000",
"OTEL_TRACES_EXPORT_INTERVAL": "1000",
}本番の長時間セッションでは間隔を既定値に戻し、短命な単発呼び出しでだけこの短縮設定を使い分けます。
トレースでエージェントループの内側を読む
CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1を設定すると、エージェントループの各ステップがスパンとして記録されます。claude_code.interactionが1ターン全体を包みます。その子として並ぶのが、モデル名・レイテンシ・トークン数を属性に持つclaude_code.llm_requestと、claude_code.toolです。claude_code.toolは、権限待ちのclaude_code.tool.blocked_on_userと実行本体のclaude_code.tool.executionを子に持ちます。
claude_code.hookスパンだけは条件が違います。ENABLE_BETA_TRACING_DETAILED=1とBETA_TRACING_ENDPOINTの組み合わせが必要で、この2つはログとトレースの送り先も変えます。CLAUDE_CODE_ENHANCED_TELEMETRY_BETAだけではフックのスパンは出ません。両変数はプロジェクト設定・ローカル設定では無視されるため、シェルかユーザー設定、管理設定に置きます。
エージェントがAgentツールでサブエージェントを起動すると、そのサブエージェントのスパンも親のclaude_code.toolスパンの下にネストし、委譲の連鎖が1本のトレースにまとまります。
スパンには既定でsession.id属性が付きます。同じセッションに対して複数回query()を呼ぶ構成では、この属性でフィルタすれば1つのタイムラインとして追えます。OTEL_METRICS_INCLUDE_SESSION_IDに偽の値を設定すると、この属性は付きません。
トレースはベータ機能で、スパン名や属性はリリース間で変わる可能性があります。ダッシュボードやアラートをスパン名に依存させるなら、CLIのバージョンを上げるたびに見直す前提で組みます。Claude SDKとAPI側のスパンはOpenTelemetryスパン属性リファレンスにあります。
アプリケーション自身のトレースとつなげる
アプリケーション側でOpenTelemetryのスパンがアクティブな状態でquery()を呼ぶと、あとはSDKとCLIが自動でつなぎます。
トレース連結の流れ
- 1
アプリのスパンがアクティブな状態でquery()を呼ぶ
SDKがW3Cのトレースコンテキストを読み取ります。
- 2
SDKが子プロセスの環境変数に注入する
TRACEPARENTとTRACESTATEが子プロセスに渡ります。 - 3
CLIがinteractionスパンの親として使う
claude_code.interactionがアプリ側スパンの子になり、エージェントの実行が独立したルートスパンではなくアプリのトレースの一部として現れます。
ログも同じトレースに結べます。TRACEPARENTが設定されていれば、実行中に出るOTLPイベントのログレコードに、アプリのトレースと一致するtrace_idとspan_idが付きます。トレースのエクスポーターを設定していなくても同様で、ログ基盤側でイベントとスパンを突き合わせられます。付与されるのはClaude Code v2.1.212からで、それより前はアクティブなスパンの外で出たイベントレコードにtrace_id/span_idが付きません。さらにv2.1.214より前は、スパンの非同期コンテキストの外で出たレコードに、スパンのIDではなく受け取ったTRACEPARENTのIDが入っていました。ログとトレースを突き合わせる構成では、CLIのバージョンを先に確認してください。
TRACEPARENTをoptions.envに明示的に設定すると、自動注入はスキップされます。任意の親コンテキストを固定したいときに使えます。対話的なCLIセッションは受け取ったTRACEPARENTを無視しますが、Agent SDKとclaude -pの実行だけはこれを親として読み込みます。
Bashツールを介して実行したコマンドが自前でOpenTelemetryスパンを出す場合、そのスパンはclaude_code.tool.executionスパンの下にネストします。CLIがBashとPowerShellのコマンドへTRACEPARENTを転送しているためです。
この転送とモデルリクエストのtraceparentヘッダーには、条件が1つあります。ANTHROPIC_BASE_URLが未設定かAnthropic APIを指しているときだけ送られます。独自のプロキシ経由で運用していて、トレースコンテキストを伝播させたい場合はCLAUDE_CODE_PROPAGATE_TRACEPARENT=1を設定します。
エージェントごとにサービス名を分ける
CLIはservice.nameを既定でclaude-codeとして報告します。複数のエージェントを動かすときや、他のサービスと同じコレクターに送るときは、サービス名を上書きし、リソース属性を足しておくとバックエンドで絞り込めます。
options = ClaudeAgentOptions(
env={
# ... 有効化例のエクスポーター設定 ...
"OTEL_SERVICE_NAME": "support-triage-agent",
"OTEL_RESOURCE_ATTRIBUTES": "service.version=1.4.0,deployment.environment=production",
},
)ここで指定した値は、そのエージェントが出すすべてのスパン・メトリクス・イベントにリソース属性として付きます。
マルチテナントアプリでエンドユーザーを特定する
CLIは呼び出しに使った認証情報に基づいて、身元属性をすべてのイベントに付けます。1つのデプロイで多数のエンドユーザーにサービスを提供するアプリケーションでは、この属性はアプリ側の認証情報を指すだけで、エージェントが誰の代理で動いたかを表しません。
ツール呼び出しやMCPアクティビティをエンドユーザー単位に紐づけたい場合は、各query()呼び出しでエンドユーザーの識別子をリソース属性として注入します。OTEL_RESOURCE_ATTRIBUTESはカンマ・スペース・等号を予約文字として扱うため、値は事前にパーセントエンコードします。
from urllib.parse import quote
options = ClaudeAgentOptions(
env={
# ... 有効化例のエクスポーター設定 ...
"OTEL_RESOURCE_ATTRIBUTES": f"enduser.id={quote(request.user_id)},tenant.id={quote(request.tenant_id)}",
},
)エンドユーザーの識別子を付けた状態では、tool_decision・tool_result・mcp_server_connection・permission_mode_changedの各イベント(claude_code.接頭辞のログレコードとしてエクスポートされる)が、ユーザー単位の監査証跡になります。
これをSIEMへ転送する運用が現実的な使い道です。Grafana Lokiに集約したログをLogQLを書かずに検索する方法はGrafana LokiのMCPログ検索で扱っています。
機微データの記録範囲を制御する
テレメトリは既定では構造情報だけです。所要時間・モデル名・ツール名は毎回記録されますが、エージェントが読み書きした中身は既定では記録されません。トークン数もAPIリクエストが使用量を返したときに記録されるため、失敗または中断したリクエストのスパンには付かないことがあります。以下の変数を立てると、記録範囲が段階的に広がります。
| 変数 | 追加される内容 |
|---|---|
OTEL_LOG_USER_PROMPTS=1 | 追加される内容claude_code.user_promptイベントとclaude_code.interactionスパンにプロンプト本文 |
OTEL_LOG_TOOL_DETAILS=1 | 追加される内容claude_code.tool_resultイベントにツール入力引数(ファイルパス・シェルコマンド・検索パターン)。コスト・トークンのメトリクスには実際のエージェント・スキル・プラグイン・MCPサーバー名も入る |
OTEL_LOG_TOOL_CONTENT=1 | 追加される内容claude_code.toolスパンのtool.outputスパンイベントにファイル内容・Bashの出力・MCPツールやWebFetchなどの返り値(既定60KBで切り詰め。トレース有効化が前提) |
OTEL_LOG_RAW_API_BODIES | 追加される内容Messages APIのリクエスト・レスポンス全文(ログイベント) |
CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTHは切り詰め上限を調整する変数で、Claude Code v2.1.214以降で使えます。OTEL_LOG_TOOL_CONTENTの対象のうち、MCPツール・WebFetch・WebSearchの返り値はv2.1.283以降です。
tool.outputにはさらに細かい条件があります。OTEL_LOG_TOOL_CONTENT=1だけで記録されるのはReadとBashの呼び出しで、v2.1.283以降はMCPツール・WebFetch・WebSearchも加わります。ただしReadのうち、画像・PDF・内容が変わっていないファイルの再読込は記録されません。EditとWriteはOTEL_LOG_TOOL_DETAILS=1も立てたときだけです。エラーで終わったツール呼び出しと、実行中にバックグラウンドへ移されたWebFetch・WebSearchの呼び出しも、このイベントを記録しません。
アシスタントの応答本文(assistant_responseイベントのresponse)は、既定で<REDACTED>に置き換わります。記録するにはOTEL_LOG_ASSISTANT_RESPONSES=1を立てます。
この変数が未設定のときはOTEL_LOG_USER_PROMPTSの値に従います。つまりプロンプト本文のためにOTEL_LOG_USER_PROMPTS=1を立てると、応答本文も一緒に記録されます。プロンプトだけ残して応答を伏せたままにするなら、OTEL_LOG_ASSISTANT_RESPONSES=0を明示します。この変数はClaude Code v2.1.193以降で使えます。
OTEL_LOG_RAW_API_BODIESだけは値の指定方法が2通りあります。1なら60KBで切り詰めたインライン記録、file:<dir>なら切り詰めなしでディスクに本文を書き出し、イベント側にはbody_refのパスだけが残ります。この変数を有効にすることは、上の3変数が明かす内容すべてに同意したことを意味します。会話履歴全体を含み、拡張思考の内容だけは編集済みで記録される点も踏まえ、観測パイプラインが機微データを保持できる承認を得ているときだけ有効にします。
Claude Code CLIの直接設定とどこが違うか
Claude Code CLIを直接使う場合の設定手順はClaude CodeのOpenTelemetryで利用量とコストを可視化にまとめています。Agent SDK経由では次の2点が変わります。
| 観点 | Claude Code CLI | Agent SDK |
|---|---|---|
| 設定の書き方 | Claude Code CLIシェルの環境変数・設定ファイル | Agent SDKoptions.env(呼び出し単位)またはプロセス環境 |
| 親スパンの受け取り | Claude Code CLI対話セッションはTRACEPARENTを無視 | Agent SDKアプリのスパンが親になる(claude -pも同様) |
実行経路はapp.entrypoint属性の値で区別できます。値はcli、sdk-cli、sdk-ts、sdk-py、claude-vscodeなどで、TypeScript SDK経由ならsdk-ts、Python SDK経由ならsdk-pyが入ります。ただしメトリクスにこの属性が付くのはOTEL_METRICS_INCLUDE_ENTRYPOINT=trueを設定したときだけで、既定では付きません。CLIとSDKのテレメトリを同じコレクターに集約している組織では、この変数を有効にしてから、ダッシュボードのフィルタをこの属性で分けます。
CLIのバージョンで使える機能が変わる
この記事の機能の多くは、起動するClaude Code CLIのバージョンに左右されます。Agent SDKを更新しても、手元のCLIが古ければ効きません。
| バージョン | 加わる・直る内容 | 関わる節 |
|---|---|---|
| v2.1.179以降 | 加わる・直る内容CLAUDE_CODE_OTEL_DIAG_STDERRでエクスポートエラーを表に出せる | 関わる節有効化 |
| v2.1.193以降 | 加わる・直る内容assistant_responseイベントとOTEL_LOG_ASSISTANT_RESPONSES | 関わる節機微データ |
| v2.1.212以降 | 加わる・直る内容イベントのログレコードにtrace_idとspan_idが付く。HTTPエクスポートがContent-Lengthを付けて送られる | 関わる節トレース連結 |
| v2.1.214以降 | 加わる・直る内容非同期コンテキスト外のレコードにもスパンのIDが入る。CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTHが使える。ストリーム使用量の二重計上が直る | 関わる節トレース連結・機微データ |
| v2.1.283以降 | 加わる・直る内容MCPツール・WebFetch・WebSearchの返り値もtool.outputに記録できる | 関わる節機微データ |
よくあるつまずき
症状から原因を引く早見表です。表の右列は、直す側がどこを触るかを示します。
| 症状 | 原因 | 対処 |
|---|---|---|
スパンは届くが、ログにtrace_idが付かない | 原因CLIがv2.1.212より前 | 対処CLIを上げる |
ログのtrace_idが、アプリのスパンと違うIDになる | 原因CLIがv2.1.214より前で、受け取ったTRACEPARENTのIDが入っている | 対処CLIを上げる |
| プロキシ経由にしたらトレースがつながらない | 原因ANTHROPIC_BASE_URLが独自プロキシだとtraceparentが送られない | 対処CLAUDE_CODE_PROPAGATE_TRACEPARENT=1 |
| コストとトークンのカウンターが実際より大きい | 原因プロキシが使用量を複数フレームに分けて流し、v2.1.214より前は余分に数えていた | 対処CLIを上げる |
Azure Monitorなどが411 Length Requiredや400で拒否する | 原因v2.1.191以降v2.1.212より前が、HTTPエクスポートをチャンク転送で送っていた | 対処CLIを上げる |
| プロンプトを伏せたいのに応答本文が出る | 原因OTEL_LOG_ASSISTANT_RESPONSESが未設定で、OTEL_LOG_USER_PROMPTSに従っている | 対処OTEL_LOG_ASSISTANT_RESPONSES=0 |
まとめ
Agent SDKのテレメトリは、SDK自身ではなく子プロセスのCLIが計装しています。CLAUDE_CODE_ENABLE_TELEMETRY=1とシグナルごとのエクスポーター指定が揃えば、コストの集計を応答ストリームから読む方法とは別に、外部の観測基盤へトレース・メトリクス・ログを流し込めます。本番に出す前の判断は、観測基盤が機微データを預かってよいかと、手元のCLIが必要なバージョンに届いているかの2つに絞れます。