Claude API の OpenTelemetry:トークンを二重に数えずに SDK と API をトレースする
スパンの入力トークン数にはキャッシュの読み込みと書き込みがすでに含まれています。そのため、Messages API を知っている人が自然に書くクエリでは、キャッシュされたプレフィックスが二重に数えられます。ローカルのモックで試した SDK 側のセットアップ、ドキュメントに沿った Console のデスティネーション設定、そしてキャッシュされていない入力に戻すための引き算をまとめます。
目次
Anthropic は10月14日、Claude API の OpenTelemetry エクスポートをベータで公開しました。リリースノートは1段落だけです。Claude SDK が7つの言語でスパンを出力し、Console でデスティネーションを追加すると API も独自のスパンを出力する、という内容です。1つの機能に見えますが、実際には2つあります。担当者も壊れ方も違い、両者の接点は1つの HTTP ヘッダーで、そのヘッダーを制御するのはアプリケーション側のサンプラーです。
これが一番効いてくるのは、多くの人が最初に作ろうとするトークンのダッシュボードです。SDK 側と API 側は同じリクエストを同じスパン名で二重に報告し、両者が共有するトークン属性の意味は、Messages API のレスポンスにある input_tokens の意味とは違います。
実際に動かしたのは Python SDK 側です。キャッシュ使用量を含む固定レスポンスを返す /v1/messages のローカルモックに対して実行したので、トークンは一切消費していません。以下のトークン数はその固定レスポンスそのままで、毎回同じになります。トレース ID とスパン ID は毎回ランダムです。動かしていないのは API 側です。組織の管理者と公開 HTTPS エンドポイントが必要なため、その手順はドキュメントに基づいており、本文でもそう明記します。
Claude API の OpenTelemetry で必要なのは SDK トレースか API トレースか
まずは SDK トレースから始めてください。必要なのは自分のコードだけです。リクエストの内側まで見る必要が出てきたら、API トレースを追加します。SDK がトレースするのはプロセスの動きです。送信する各リクエスト、ツールランナーが実行する各クライアントツール、ファイルのアップロードやその他の呼び出しが対象です。API がトレースするのは Anthropic 側で起きることです。リクエスト内の各モデル実行、Web 検索のようなサーバーツール、MCP コネクタの呼び出し、Claude Managed Agents セッションの各ターンが対象です。
必要な条件も同じくらい違います。SDK トレースにはロールが不要で、Bedrock、Google Cloud、Foundry を含め、SDK が対応するどのプラットフォームでも動きます。API トレースには Console 組織の管理者ロールが必要で、使えるのは Claude Platform と Claude Platform on AWS だけです。Managed Agents を使っている場合、呼び出しの合間にエージェントが何をしたかを見られるのは API 側だけです。SDK に見えるのは、イベントの送信、ストリームの読み取り、カスタムツールの実行だけです。
Claude Python SDK で OpenTelemetry を有効にする方法
SDK と OpenTelemetry SDK をインストールし、最初の呼び出しの前にトレーサープロバイダーを登録します。Claude の呼び出し自体は変更不要です。SDK はグローバルに登録されたプロバイダーを自動で使います。テストしたバージョンは固定しました。
pip install "anthropic==1.15.0" "opentelemetry-sdk==1.45.1"SDK ガイドの最小例は、スパンをコンソールに出力します。
from anthropic import Anthropic
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
client = Anthropic()
client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)オフラインで動かすため、Anthropic(api_key="sk-test", base_url="http://127.0.0.1:8765") でクライアントをモックに向けました。モックは数行の http.server で、どの POST にも同じ固定メッセージを返します。キャッシュされていない入力 120 トークン、1時間キャッシュへの書き込み 2,000 トークン、キャッシュからの読み込み 5,000 トークン、出力 40 トークンです。1回の呼び出しでスパンが1つ出ました。必要な行だけに絞るとこうなります。
name anthropic.messages.create
kind SpanKind.CLIENT
gen_ai.request.model claude-opus-5-5
anthropic.request.id req_mock01
anthropic.message.stop_reason end_turn
gen_ai.usage.input_tokens 7120
gen_ai.usage.cache_write.input_tokens 2000
gen_ai.usage.cache_read.input_tokens 5000
anthropic.usage.cache_creation.ephemeral_1h_input_tokens 2000
gen_ai.usage.output_tokens 40実際のバックエンドに送るには、コンソールエクスポーターを OTLP エクスポーターに差し替えます。残りは設定だけで、コードでクライアントに open_telemetry= を渡すか、環境変数を使います。ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content でプロンプトとレスポンスを記録し、ANTHROPIC_OPEN_TELEMETRY=false で SDK のスパンを無効にします。ドキュメントにある注意点が1つあります。コードで open_telemetry= を渡すと、これらの環境変数はまとめて上書きされます。省略したネストされたキーは、対応する環境変数が設定されていてもデフォルト値に戻ります。
プロジェクトでも SDK のバージョンを固定してください。ベータ期間中は新しい SDK リリースでスパン、属性、オプション、環境変数の名前が変わることがある、とドキュメントにははっきり書かれています。古い名前で作ったダッシュボードは、エラーを一切出さずに壊れます。
Claude API の OpenTelemetry デスティネーションを追加する方法
API トレースのガイドによると(ここでは未検証)、組織の管理者が Console の OpenTelemetry 設定でデスティネーションを追加するか、ベータヘッダー telemetry-destinations-2026-08-11 を付けて Admin API で作成します。スパンは1分ほどで届き始めます。デスティネーションは、OTLP のベース URL、認証用のヘッダー、コンテンツモード、対象のワークスペースで構成されます。URL はベースで、API が /v1/traces を自動で付け足します。つまり https://otel.example.com/otlp には https://otel.example.com/otlp/v1/traces で届きます。トレースのパスが違うバックエンドは、同じホスト上で、別のトレースエンドポイント欄に指定します。
セルフホストのコレクターがそもそも使えるかどうかは、エンドポイントの条件で決まります。HTTPS の 443 番ポートで protobuf の OTLP/HTTP を話し、IP アドレスではなくホスト名で到達できる必要があります。標準の Collector は 4318 番で待ち受けるので、リバースプロキシか受信設定の変更が必要です。エンドポイントが 3xx リダイレクトを返した場合、ホスト名が公開されていないアドレスに解決された場合、24時間すべての配信が失敗した場合、API はデスティネーションを無効にします。VPN や tailnet の内側からしか届かないコレクターは選択肢になりません。前段に認証を設けたうえで、公開インターネット上に置く必要があります。
保存後、Console のテスト操作で anthropic.test スパンが送られます。API のスパンには service.name = claude-platform が付くので、これで絞り込めば見つかります。1つの組織で使えるデスティネーションは10個で、無効になったものも数に入ります。
Claude API と SDK のスパンが1つの OpenTelemetry トレースにつながる仕組み
W3C の traceparent ヘッダーを通じてつながります。SDK はアプリケーションが登録したプロパゲーターでこのヘッダーを書き、API がそれを読み、API のスパンが SDK のスパンの子になります。Python のデフォルトのプロパゲーターは W3C なので、そのまま動きます。ドキュメントによると、Go と、コードで設定した場合の Java や PHP は、デフォルトでは何も登録しないので追加が必要です。ヘッダーがない場合、そのリクエストの API スパンはルートとして独自のトレースを始めます。
モックには SDK が送ったヘッダーを記録させました。parent-id フィールドは CLIENT スパン自身の ID で、この実行では16進数で 023c7f20402aa439 でした。つまりサーバー側のスパンはクライアントのスパンの直下にぶら下がるはずです。トレースフラグは 03 で、sampled ビットが立ち、さらに Trace Context Level 2 のランダムトレース ID ビットも立っていました。
次に、サンプラーを ParentBased(TraceIdRatioBased(0.0)) に替えました。新しいルートトレースをすべて捨てる設定です。呼び出しに親はないので、捨てられました。予想どおりスパンは1つもエクスポートされませんでしたが、SDK はヘッダー自体は送り続け、フラグは 02 になりました。ランダムビットはそのままで、sampled ビットだけが落ちています。API がこれをどう扱うかは、ドキュメントに書かれています。
If the sampled flag in the header is off, the API doesn't emit the request's span or the spans under it.つまり、Anthropic 側があるリクエストについてスパンを出すかどうかは、アプリケーションのサンプラーが決めます。サンプリングされていないコンテキストでプロセスを出たリクエストには API スパンが付かず、その中のサーバーツール、モデル実行、Managed Agents のターンもトレースされません。一律 1% のヘッドサンプラーなら、およそ 99% のリクエストは API 側に何も残しません。サンプリングは普通、ローカルなコストの判断です。ここではそれが別のシステムにまで及びます。API スパンをすべて取りたいなら、リクエストをプロセス内でサンプリングするか、traceparent ヘッダーなしで API に届けて独自のトレースを始めさせる必要があります。
逆のスイッチにも副作用があります。ANTHROPIC_OPEN_TELEMETRY=false にすると SDK はスパンを出さず、今回の実行では traceparent も送りませんでした。ほかの計装がヘッダーを付けない限り、そのクライアントからの API スパンは独立したルートトレースとして届き、原因になったコードとひも付けられません。HTTP クライアントの計装がすでに traceparent を送っていて、ヘッダーの重複を止めたいだけなら、SDK ガイドにあるスイッチは Anthropic(open_telemetry={"propagation": False}) です。ヘッダーは止まり、SDK のスパンは残ります。
OpenTelemetry のスパンで Claude API のトークンを一度だけ数える方法
リクエストごとにソースを1つ選び、そのリクエストスパンだけを合計します。1つのリクエストに SDK のスパンと API のスパンの両方がある場合、ドキュメントは API のスパンを合計するよう指示しています。どちらも名前は anthropic.messages.create で、SDK 側は種類が CLIENT、API 側は SERVER です。SDK ガイドのトラブルシューティング表には、この症状が "Each request appears twice"(各リクエストが2回現れる)として載っています。
API のスパンの中では、使用量が1つ下の階層でも繰り返されます。anthropic.messages.create のサーバースパンにあり、その下の各 anthropic.model_request にもあります。合計するのはどちらか一方の階層だけで、両方を足してはいけません。コンパクションは繰り返しではなく追加分です。gen_ai.usage.* にはコンパクションのトークンが含まれないので、SDK のスパンでは anthropic.usage.compaction.input_tokens と anthropic.usage.compaction.output_tokens を足し、API のスパンでは anthropic.compaction スパンを足します。Managed Agents のセッションでは、anthropic.model_request と anthropic.session.outcome_evaluation を合計します。配信は at-least-once なので、バックエンドが重複排除しない場合はトレース ID とスパン ID の組で重複を取り除いてください。
次は、入力の数値が何を意味するかです。モックで試した価値があったのはここでした。モックのレスポンスは input_tokens: 120 で、その横にキャッシュ書き込み 2,000 とキャッシュ読み込み 5,000 がありました。スパンは gen_ai.usage.input_tokens: 7120 でした。Messages API のレスポンスでは、input_tokens はキャッシュされていない部分だけを数え、入力全体は3つのフィールドの合計です。だから input_tokens だけを残すロガーはキャッシュされたプレフィックスを丸ごと失います。スパンはこの足し算を代わりにやってくれます。スパンのリファレンスによると、スパンの入力にはキャッシュの読み書きがすでに含まれていて、ドキュメントはその上にキャッシュ属性を足さないよう警告しています。
そのため、Messages API を知っている人が自然に書く gen_ai.usage.input_tokens と gen_ai.usage.cache_read.input_tokens を足すクエリは、キャッシュされたプレフィックスを二重に数えます。コストを出すには、入力の数値が1つだけでは形が合いません。各部分の料金が違うからです。読み込みは基本入力料金のごく一部、5分キャッシュの書き込みは 1.25 倍、1時間キャッシュの書き込みは2倍です。Messages API のリクエストスパンでは、次のように分解し直します。
uncached input = gen_ai.usage.input_tokens
- gen_ai.usage.cache_read.input_tokens
- gen_ai.usage.cache_write.input_tokens
cache writes by lifetime = anthropic.usage.cache_creation.ephemeral_5m_input_tokens
+ anthropic.usage.cache_creation.ephemeral_1h_input_tokensキャッシュ属性がない場合は、null ではなく 0 として扱ってください。そうしないと、この計算を SQL に書き換えると行が黙って落ちます。有効期間別の内訳は Messages API のスパンにしかなく、結果評価のスパンにはキャッシュ属性がまったくありません。コンパクションは請求書でも別の行のままです。ストリーミングレスポンスの SDK スパンでは、5分と1時間の内訳に含まれるのは最初の出力トークンより前の書き込みだけです。ストリームでキャッシュ書き込みのコストを正確に出すなら、API のスパンを合計してください。
Claude API の OpenTelemetry スパンが課金の記録にならない理由
スパンは設計上なくなることがあり、ドキュメントも集計には別の手段を案内しているからです。配信はベストエフォートで、失われたスパンは再送されません。前述のとおり、呼び出し側は sampled フラグでリクエストのスパンを捨てられます。API キーごとの使用量を追うとき(たとえば漏えいしたキーを見つけたいとき)、ドキュメントが勧めているのは Usage API です。こちらはスパンを出さなかったリクエストも数えます。
2つのツールは違う問いに答えます。Usage API には Admin API キーなどの Admin API 認証情報が必要で、ワークスペースのキーでは使えません。データは通常5分以内に反映されます。スパンは Usage API には答えられないことに答えます。トークンがどの機能やどのエージェントのターンに属するか、ある実行がなぜ遅かったか、なぜ失敗したか、です。現実的な構成は、スパンでトークンを振り分け、Usage API を合計値として使うやり方です。両者の比較に意味があるのは、同じリクエスト、同じ時間帯、同じトークンの定義を対象にし、サンプリング率と反映の遅れを考慮した場合だけです。サンプリング率が一定のままトラフィックに比例して開く差は想定どおりです。調べるべきなのは比率の変化で、その原因は実際の使用量だけでなく、配信の欠落、サンプラーの変更、トレースを切ったクライアントのこともあります。
Claude API トレースの Full content モードが送るもの
プロンプトとモデルのレスポンスをバックエンドに送ります。保存期間とアクセス権はそのバックエンドのルールに従い、デフォルトではスパンあたり最大 0.75 MB のコンテンツが送られます。デフォルトの Metadata only では、ID、タイミング、ステータス、トークン数、エージェントやツールや MCP サーバーに付けた名前が残ります。エクスポーターのバグに見えて実はそうではない欠落が2つあります。Managed Agents のスレッドではシステム指示が最初のターンのスパンにしか出ません。ストリーミングの SDK スパンにレスポンスが入るのは、messages.stream() で読んだ場合だけです。
設定の名前からは分かりにくい点もいくつかあります。ゼロデータ保持でも、Messages API では Full content を使えます。API は配信までコンテンツをメモリに保持するだけです。Managed Agents はテレメトリの有無にかかわらず、ゼロデータ保持の対象外です。HIPAA、FERPA、K-12 の制限がある組織では、設定にかかわらずデスティネーションにはメタデータしか送られません。ただし SDK のスパンはこの制限の対象外なので、そうした組織でも Full content の SDK クライアントはプロンプトをエクスポートします。また、呼び出しを会話単位にまとめる SDK オプション conversation_id はどちらのモードでもエクスポートされるので、メールアドレスや顧客名を入れてはいけません。
Claude Code 自身の OpenTelemetry は Claude API のエクスポートとどう関係するのか
別のシステムです。ドキュメントも Claude Code、Agent SDK、Cowork についてはそれぞれのモニタリングのページに案内しています。Claude Code は独自の属性名で独自のイベントとメトリクスを出します。同じ日にリリースされた Claude Code 2.1.300 は共通の語彙に近づきました。リリースノートによると、トレーススパンに gen_ai.provider.name、gen_ai.conversation.id、error.type などの GenAI セマンティック規約の属性が加わり、api_request イベントに cache_creation_5m_tokens と cache_creation_1h_tokens が加わりました。
キャッシュの内訳は両者で表記が違い、Claude Code では cache_creation_1h_tokens、API のスパンでは anthropic.usage.cache_creation.ephemeral_1h_input_tokens です。両方を1つのダッシュボードで見るには、対応付けの層が必要です。Claude Code 側については、アイドル時コンパクションと1時間のプロンプトキャッシュの記事で扱っています。そこでは1時間キャッシュの書き込み数が、コンパクション後のキャッシュの書き直しを測っており、それはコンパクションのコストの一部です。
Claude API の OpenTelemetry は今オンにする価値があるのか
筆者の見立てでは、SDK 側は今すぐ、Metadata only モードでオンにする価値があります。必要なのはトレーサープロバイダー1つで、すでにあるトレースの中でリクエストごとのトークンとレイテンシが見えるようになります。バージョンを固定し、デスティネーションを追加するまではトークンのクエリを CLIENT スパンだけに対して書き、最初のダッシュボードからキャッシュされていない入力の分解を使ってください。合計の間違いが最初に表に出るのは、請求書と見比べたときだからです。
API 側のセットアップが報われるのは、サーバーツールや分岐するワークフローを持つ Managed Agents を使っている場合です。そこでは作業がプロセスの目の届かないところで起きるからです。追加する前に2つ決めておいてください。1つは、トークンのクエリがどちらのソースを合計するかです。追加した時点から、すべてのリクエストが二重に報告されます。もう1つは、サンプラーが何をするかです。今後は手元に残すものだけでなく、Anthropic が何をエクスポートするかもサンプラーが決めます。
01 / ディスカッション
コメント欄はありません。議論は X で行っています。
02 / ほかの記事
Claude Code のアイドル時コンパクション:今の安い要約か、後でキャッシュを丸ごと書き直すか
アイドル時コンパクションが動くかどうかは5つの条件で決まり、デフォルトではそのうち2つが、API キーのセッションと、200K ウィンドウのセッションの大半を対象外にします。実際に対象になるのは誰か、このトレードオフがキャッシュトークンでいくらになるか、調整や無効化の方法をまとめます。
Gemini 4 Argon が一般公開:Google が初日に報告した利用例と、12月1日に2倍になる価格
Google が Argon について報告している利用例を7つ、出どころの記事とあわせて並べます。レビューの記録まで公開しているのは1つだけで、提案6件のうち採用は2件でした。残りは、発表が触れずドキュメントに任せた請求の話です。まず、long decode が入力をどう数えるかから始めます。
Claude Managed Agents の dynamic workflows:ファンアウトはエージェントが書く、だから予算を先に決める
新しい multiagent タイプではスイッチが最初からオンで、ラン開始を承認する仕組みはなく、唯一の金額上限はセッション作成時にしか付けられません。ドキュメントをもとにした設定ガイドです。最初のメッセージを送る前に決めておく 3 つの設定を扱います。