Claude API OpenTelemetry: SDK und API tracen, ohne Tokens doppelt zu zählen
Die Input-Token-Zahl eines Spans enthält Cache-Reads und Cache-Writes bereits. Die Abfrage, die sich natürlich anfühlt, wenn du die Messages API kennst, zählt den gecachten Prefix deshalb doppelt. Setup für die SDK-Hälfte, getestet gegen einen lokalen Mock, die Console-Destination laut Doku und die Subtraktion, die dich zum ungecachten Input zurückbringt.
Inhalt
Anthropic hat am 14. Oktober OpenTelemetry-Export für die Claude API als Beta veröffentlicht. Die Release Note ist ein Absatz: Das Claude SDK erzeugt Spans in sieben Sprachen, und die API erzeugt eigene, sobald du in der Console eine Destination anlegst. Klingt nach einem Feature. Es sind zwei, mit unterschiedlichen Zuständigen und unterschiedlichen Fehlerbildern, und sie treffen sich in einem einzigen HTTP-Header, über den der Sampler deiner Anwendung entscheidet.
Am meisten zählt das beim ersten Einsatz, zu dem man greift: einem Token-Dashboard. Die beiden Hälften melden dieselbe Anfrage zweimal unter demselben Span-Namen, und das Token-Attribut, das sie teilen, bedeutet nicht das, was input_tokens in einer Antwort der Messages API bedeutet.
Was ich ausgeführt habe: die Python-SDK-Hälfte, gegen einen lokalen Mock von /v1/messages, der eine feste Antwort mit Cache-Usage zurückgibt. Es wurde also kein Token verbraucht. Die Token-Zahlen unten stammen direkt aus dieser festen Antwort und wiederholen sich bei jedem Lauf, Trace- und Span-IDs sind jedes Mal zufällig. Was ich nicht ausgeführt habe: die API-Hälfte. Dafür braucht es einen Organisations-Admin und einen öffentlichen HTTPS-Endpoint. Diese Schritte stammen deshalb aus der Doku, und ich kennzeichne sie so.
Welche Hälfte von Claude API OpenTelemetry brauchst du, SDK-Tracing oder API-Tracing?
Fang mit SDK-Tracing an, denn dafür brauchst du nur deinen Code. API-Tracing kommt dazu, wenn du in eine Anfrage hineinsehen musst. Das SDK tracet, was dein Prozess tut: jede Anfrage, die es sendet, jedes Client-Tool, das der Tool Runner ausführt, Datei-Uploads und andere Aufrufe. Die API tracet, was auf Anthropics Seite passiert: jeden Modelllauf innerhalb einer Anfrage, Server-Tools wie die Websuche, Aufrufe über den MCP Connector und die Turns einer Claude-Managed-Agents-Session.
Die Voraussetzungen unterscheiden sich genauso stark. SDK-Tracing braucht keine Rolle und läuft auf jeder Plattform, die das SDK unterstützt, inklusive Bedrock, Google Cloud und Foundry. API-Tracing braucht die Admin-Rolle in einer Console-Organisation und funktioniert nur auf Claude Platform und Claude Platform on AWS. Wenn du Managed Agents betreibst, ist die API-Hälfte die einzige, die sieht, was der Agent zwischen deinen Aufrufen getan hat. Das SDK sieht nur, wie du Events sendest, den Stream liest und deine eigenen Tools ausführst.
Wie aktivierst du OpenTelemetry im Claude Python SDK?
Installiere das SDK und ein OpenTelemetry SDK, registriere vor dem ersten Aufruf einen Tracer Provider und lass deine Claude-Aufrufe, wie sie sind. Das SDK findet den global registrierten Provider von selbst. Ich habe die getesteten Versionen gepinnt:
pip install "anthropic==1.15.0" "opentelemetry-sdk==1.45.1"Das minimale Beispiel aus dem SDK-Guide gibt Spans auf der Konsole aus:
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"}],
)Um es offline laufen zu lassen, habe ich den Client mit Anthropic(api_key="sk-test", base_url="http://127.0.0.1:8765") auf den Mock gerichtet. Der Mock besteht aus ein paar Zeilen http.server und beantwortet jeden POST mit derselben vorgefertigten Nachricht: 120 ungecachte Input-Tokens, 2.000 Tokens in den 1-Stunden-Cache geschrieben, 5.000 aus dem Cache gelesen und 40 Output-Tokens. Ein Aufruf ergab einen Span. Gekürzt auf die relevanten Zeilen:
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 40Für ein echtes Backend tauschst du den Konsolen-Exporter gegen einen OTLP-Exporter. Der Rest ist Konfiguration, entweder im Code über open_telemetry= am Client oder über Umgebungsvariablen: ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content zeichnet Prompts und Antworten auf, ANTHROPIC_OPEN_TELEMETRY=false schaltet die Spans des SDK ab. Ein Haken laut Doku: open_telemetry= im Code überschreibt all diese Variablen als Ganzes, und jeder verschachtelte Schlüssel, den du weglässt, fällt auf seinen Default zurück, auch wenn die zugehörige Variable gesetzt ist.
Pinne die SDK-Version auch in deinem Projekt. Die Doku sagt klar, dass ein neues SDK-Release während der Beta einen Span, ein Attribut, eine Option oder eine Umgebungsvariable umbenennen kann. Ein Dashboard, das auf den alten Namen aufbaut, würde dann ohne jede Fehlermeldung kaputtgehen.
Wie legst du eine Claude-API-OpenTelemetry-Destination an?
Laut API-Tracing-Guide, hier nicht getestet: Ein Organisations-Admin legt in den OpenTelemetry-Einstellungen der Console eine Destination an oder erstellt sie über die Admin API mit dem Beta-Header telemetry-destinations-2026-08-11, und nach etwa einer Minute kommen Spans an. Eine Destination besteht aus einer OTLP-Basis-URL, den Headern für die Authentifizierung, einem Content-Modus und den Workspaces, die sie abdeckt. Die URL ist eine Basis: Die API hängt /v1/traces selbst an, https://otel.example.com/otlp bekommt die Lieferungen also unter https://otel.example.com/otlp/v1/traces. Ein Backend mit anderem Traces-Pfad kommt in das separate Feld für den Traces-Endpoint, auf demselben Host.
Die Endpoint-Regeln entscheiden, ob ein selbst gehosteter Collector überhaupt funktioniert. Er muss OTLP/HTTP mit Protobuf auf HTTPS-Port 443 sprechen, unter einem Hostnamen statt einer IP-Adresse. Ein Standard-Collector lauscht auf 4318, braucht also einen Reverse Proxy oder einen umkonfigurierten Receiver. Die API deaktiviert eine Destination, wenn der Endpoint mit einem 3xx-Redirect antwortet, wenn sein Hostname auf eine nicht öffentliche Adresse auflöst oder wenn 24 Stunden lang jede Lieferung fehlschlägt. Ein Collector, der nur in einem VPN oder Tailnet erreichbar ist, kommt also nicht in Frage. Er muss im öffentlichen Internet stehen, mit Authentifizierung davor.
Nach dem Speichern schickt die Test-Aktion der Console einen anthropic.test-Span, und API-Spans tragen service.name = claude-platform. Nach diesem Attribut filterst du, um sie zu finden. Eine Organisation bekommt zehn Destinations, deaktivierte mitgezählt.
Wie verbinden sich Claude-API- und SDK-Spans zu einem OpenTelemetry-Trace?
Über den W3C-Header traceparent. Das SDK schreibt ihn mit dem Propagator, den deine Anwendung registriert hat, die API liest ihn, und der Span der API wird zum Kind des SDK-Spans. In Python ist der Default-Propagator W3C, es funktioniert also einfach. Laut Doku registrieren Go und, bei Konfiguration im Code, auch Java oder PHP standardmäßig keinen Propagator, dort musst du einen hinzufügen. Ohne Header startet der API-Span einer Anfrage einen eigenen Trace als Root.
Der Mock hat den Header protokolliert, den das SDK gesendet hat. Sein Parent-ID-Feld ist die eigene ID des CLIENT-Spans, in diesem Lauf 023c7f20402aa439 in Hex. Der Server-Span wird also direkt unter dem Client-Span hängen. Die Trace-Flags waren 03: das Sampled-Bit gesetzt, dazu das Random-Trace-ID-Bit aus Trace Context Level 2.
Dann habe ich den Sampler gegen ParentBased(TraceIdRatioBased(0.0)) getauscht, der jeden neuen Root-Trace verwirft. Der Aufruf hatte keinen Parent, wurde also verworfen. Wie erwartet wurde kein Span exportiert, aber das SDK hat den Header trotzdem gesendet, jetzt mit Flags 02: dasselbe Random-Bit, Sampled-Bit aus. Die Doku sagt, was die API damit macht:
If the sampled flag in the header is off, the API doesn't emit the request's span or the spans under it.Der Sampler deiner Anwendung entscheidet also, ob Anthropics Seite für eine Anfrage überhaupt Spans erzeugt. Eine Anfrage, die deinen Prozess in einem nicht gesampelten Kontext verlässt, bekommt keinen API-Span, und auch ihre Server-Tools, Modellläufe und Managed-Agents-Turns werden nicht getracet. Bei einem einheitlichen 1-%-Head-Sampler hinterlassen grob 99 % der Anfragen auf der API-Seite nichts. Sampling ist normalerweise eine lokale Kostenentscheidung, hier greift es in ein anderes System hinein. Wenn du jeden API-Span willst, muss die Anfrage in deinem Prozess gesampelt sein oder die API ohne traceparent-Header erreichen und einen eigenen Trace starten.
Der umgekehrte Schalter hat eine eigene Nebenwirkung. Mit ANTHROPIC_OPEN_TELEMETRY=false hat das SDK keinen Span erzeugt und in meinem Lauf auch kein traceparent gesendet. Wenn keine andere Instrumentierung den Header setzt, kommen API-Spans von so einem Client als separate Root-Traces an, die du nicht dem Code zuordnen kannst, der sie ausgelöst hat. Wenn es nur darum geht, doppelte Header zu vermeiden, weil deine HTTP-Client-Instrumentierung traceparent schon sendet, ist der Schalter laut SDK-Guide Anthropic(open_telemetry={"propagation": False}). Der stoppt die Header und behält die SDK-Spans.
Wie zählst du Claude-API-Tokens über OpenTelemetry-Spans nur einmal?
Wähle pro Anfrage eine Quelle und summiere nur deren Request-Spans. Hat eine Anfrage sowohl einen SDK-Span als auch einen API-Span, sagt die Doku: die API-Spans summieren. Beide heißen anthropic.messages.create, der SDK-Span ist vom Typ CLIENT, der API-Span vom Typ SERVER. Die Troubleshooting-Tabelle im SDK-Guide führt das Symptom als „Each request appears twice“ (jede Anfrage erscheint zweimal).
Innerhalb der API-Spans wiederholt sich die Usage eine Ebene tiefer. Sie steht am Server-Span anthropic.messages.create und noch einmal an jedem anthropic.model_request darunter. Du summierst also die eine Ebene oder die andere, nie beide. Compaction kommt dazu, statt sich zu wiederholen: gen_ai.usage.* lässt Compaction-Tokens aus. Auf SDK-Spans addierst du deshalb anthropic.usage.compaction.input_tokens und anthropic.usage.compaction.output_tokens, auf API-Spans die anthropic.compaction-Spans. Für eine Managed-Agents-Session summierst du anthropic.model_request und anthropic.session.outcome_evaluation. Die Lieferung erfolgt at least once, dedupliziere also nach Trace-ID plus Span-ID, falls dein Backend das nicht tut.
Dann bleibt die Frage, was die Input-Zahl bedeutet. Genau hier hat sich der Mock-Lauf gelohnt. Die Antwort des Mocks enthielt input_tokens: 120, daneben 2.000 Cache-Writes und 5.000 Cache-Reads. Der Span meldete gen_ai.usage.input_tokens: 7120. In einer Antwort der Messages API zählt input_tokens nur den ungecachten Teil, und der gesamte Input ist die Summe der drei Felder. Deshalb verliert ein Logger, der nur input_tokens behält, den gecachten Prefix komplett. Der Span erledigt die Addition für dich: Laut Span-Referenz enthält sein Input Cache-Reads und -Writes bereits, und die Doku warnt davor, die Cache-Attribute noch obendrauf zu addieren.
Eine Abfrage, die gen_ai.usage.input_tokens und gen_ai.usage.cache_read.input_tokens addiert, und genau die schreibt man, wenn man die Messages API kennt, zählt den gecachten Prefix also doppelt. Für die Kosten hat die einzelne Input-Zahl die falsche Form, weil die Teile unterschiedlich bepreist sind: Reads zu einem Bruchteil des Basis-Input-Preises, 5-Minuten-Writes zum 1,25-Fachen, 1-Stunden-Writes zum Doppelten. Auf Request-Spans der Messages API teilst du sie wieder auf:
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_tokensBehandle ein fehlendes Cache-Attribut als 0, nicht als NULL, sonst lässt eine SQL-Umsetzung dieser Formel die Zeile stillschweigend fallen. Die Aufteilung nach Lebensdauer gibt es nur auf Spans der Messages API, Outcome-Evaluation-Spans tragen gar keine Cache-Attribute, und Compaction bleibt eine eigene Zeile auf der Rechnung. Beim SDK-Span einer Streaming-Antwort enthält die Aufteilung in 5 Minuten und 1 Stunde nur die Writes vor dem ersten Output-Token. Für exakte Cache-Write-Kosten bei Streams summierst du deshalb die API-Spans.
Warum sind Claude-API-OpenTelemetry-Spans keine Abrechnungsgrundlage?
Weil Spans planmäßig verloren gehen können und die Doku dich für die Abrechnung woandershin schickt. Die Lieferung ist Best Effort, und ein verlorener Span wird nicht neu gesendet. Ein Aufrufer kann die Spans einer Anfrage über das Sampled-Flag verwerfen, wie oben gezeigt. Für die Usage pro API-Key, etwa um einen geleakten Key zu entdecken, empfiehlt die Doku die Usage API. Sie zählt auch Anfragen, die keine Spans erzeugt haben.
Die beiden Werkzeuge beantworten unterschiedliche Fragen. Die Usage API braucht Admin-API-Credentials, etwa einen Admin-API-Key, ein Workspace-Key reicht nicht. Ihre Daten kommen typischerweise innerhalb von fünf Minuten an. Spans beantworten, was die Usage API nicht kann: zu welchem Feature oder Agent-Turn die Tokens gehören und warum ein Lauf langsam war oder fehlgeschlagen ist. Ein brauchbares Setup: Tokens über Spans zuordnen und die Usage API als Gesamtsumme behalten. Ein Vergleich der beiden bedeutet nur dann etwas, wenn beide dieselben Anfragen, dasselbe Zeitfenster und dieselbe Token-Definition abdecken, mit Sampling-Rate und Meldeverzug eingerechnet. Eine Lücke, die bei konstanter Sampling-Rate mit dem Traffic wächst, ist zu erwarten. Untersuchen solltest du eine Änderung des Verhältnisses, und die kann genauso gut eine verlorene Lieferung, ein geänderter Sampler oder ein Client mit abgeschaltetem Tracing sein wie echter Verbrauch.
Was sendet der Full-content-Modus aus Claude-API-Traces?
Er schickt deine Prompts und die Antworten des Modells an das Backend, unter dessen Aufbewahrungs- und Zugriffsregeln, standardmäßig bis zu 0,75 MB Inhalt pro Span. Metadata only, der Default, behält IDs, Timing, Status, Token-Zahlen und die Namen, die du Agents, Tools und MCP-Servern gegeben hast. Zwei Lücken, die wie Exporter-Bugs aussehen, sind keine: In einem Managed-Agents-Thread erscheinen die Systemanweisungen nur am Span des ersten Turns, und ein gestreamter SDK-Span enthält die Antwort nur, wenn du sie mit messages.stream() liest.
Ein paar Fallstricke verrät der Name der Einstellung nicht. Mit Zero Data Retention kannst du Full content für die Messages API trotzdem nutzen, die API hält den Inhalt nur bis zur Lieferung im Speicher. Managed Agents kommt für Zero Data Retention nicht in Frage, mit oder ohne Telemetrie. Organisationen mit HIPAA-, FERPA- oder K-12-Einschränkung bekommen von Destinations unabhängig von der Einstellung nur Metadaten. SDK-Spans fallen aber nicht unter diese Einschränkung, ein SDK-Client mit Full content in so einer Organisation exportiert also weiterhin Prompts. Und conversation_id, die SDK-Option, die Aufrufe zu einer Unterhaltung gruppiert, wird in beiden Modi exportiert. Sie darf also keine E-Mail-Adresse und keinen Kundennamen enthalten.
Wo steht das eigene OpenTelemetry von Claude Code neben dem Claude-API-Export?
Es ist ein separates System, und die Doku verweist für Claude Code, das Agent SDK und Cowork auf eigene Monitoring-Seiten. Claude Code erzeugt eigene Events und Metriken mit eigenen Attributnamen. Am selben Tag hat sich Claude Code 2.1.300 dem gemeinsamen Vokabular angenähert: Laut Release Notes haben Trace-Spans GenAI-Semantic-Convention-Attribute wie gen_ai.provider.name, gen_ai.conversation.id und error.type bekommen, und das Event api_request hat cache_creation_5m_tokens und cache_creation_1h_tokens bekommen.
Die Cache-Aufteilung heißt auf beiden Seiten anders, cache_creation_1h_tokens in Claude Code und anthropic.usage.cache_creation.ephemeral_1h_input_tokens in API-Spans. Ein Dashboard über beide braucht also eine Mapping-Schicht. Der Beitrag über Idle-Compaction und den 1-Stunden-Prompt-Cache behandelt die Seite von Claude Code. Dort misst die Zahl der 1-Stunden-Writes den Cache-Rewrite nach einer Compaction, also einen Teil dessen, was sie kostet.
Lohnt es sich, Claude API OpenTelemetry jetzt einzuschalten?
Meine Einschätzung: die SDK-Hälfte ja, heute, im Modus Metadata only. Sie kostet einen Tracer Provider und liefert dir Tokens und Latenz pro Anfrage in Traces, die du ohnehin schon hast. Pinne die Version, schreib die Token-Abfragen nur gegen CLIENT-Spans, bis du eine Destination hinzufügst, und nutz die Aufteilung in ungecachten Input ab dem ersten Dashboard, denn eine falsche Summe fällt zuerst beim Abgleich mit der Rechnung auf.
Die API-Hälfte rechtfertigt ihr Setup, wenn du Server-Tools oder Managed Agents mit verzweigenden Workflows betreibst, weil die Arbeit dort außerhalb der Sicht deines Prozesses passiert. Bevor du sie hinzufügst, entscheide zwei Dinge: welche Quelle deine Token-Abfragen summieren, denn ab diesem Moment meldet jede Anfrage doppelt, und was dein Sampler tut, denn er bestimmt jetzt auch, was Anthropic exportiert, nicht nur, was du behältst.
01 / Diskussion
Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.
02 / Weitere Artikel
Claude Code Idle-Compaction: jetzt eine billige Zusammenfassung oder später ein kompletter Cache-Rewrite
Fünf Bedingungen entscheiden, ob die Idle-Compaction überhaupt greift, und zwei davon schließen per Default Sessions mit API-Key und die meisten mit 200K-Fenster aus. Wer sie wirklich bekommt, was der Tausch an Cache-Tokens kostet und wie du sie einstellst oder abschaltest.
Gemini 4 Argon ist öffentlich: die Einsätze, die Google am ersten Tag meldet, und der Preis, der sich am 1. Dezember verdoppelt
Sieben Einsätze, die Google für Argon meldet, jeweils mit dem Beitrag, aus dem sie stammen, und der einzige, der sein Review-Protokoll zeigt: sechs Vorschläge, zwei ausgeliefert. Der Rest ist die Rechnung, die nicht in der Ankündigung steht, sondern in der Doku, angefangen damit, wie Long Decode den Input zählt.
Claude Managed Agents Dynamic Workflows: Der Agent schreibt den Fan-out, das Budget muss vorher stehen
Mit dem neuen multiagent-Typ ist der Schalter standardmäßig an, den Start eines Runs genehmigt niemand, und die einzige Kostengrenze lässt sich nur beim Anlegen der Session setzen. Eine Einrichtungsanleitung auf Basis der Dokumentation, mit den drei Einstellungen, die vor der ersten Nachricht entschieden sein müssen.