给 Claude API 接上 OpenTelemetry:每次请求、每次工具调用都有 span,能嵌进你自己的追踪
Claude 的 SDK 和 API 新加了 OpenTelemetry 追踪(beta)。本站用官方 key 实测了 Python SDK 一侧:每个请求、每次工具调用、托管智能体的会话请求都有 span,调用代码不用改,走中转站也能用。API 一侧要在 Console 配接收地址,按文档整理。
本站已实测,测试日期:2026-10-15。
本页目录(11 节)
这篇教程解决一个问题:自己的程序调 Claude,哪次请求慢、哪次报错、一次对话花了多少 token、工具调用卡在哪一步,怎么在已有的监控里看到?
Claude 的开发平台文档在 2026 年 10 月 15 日新加了 OpenTelemetry 追踪的四页,目前是 beta。追踪分两边:
- SDK 这一侧:Claude 的 SDK 在你的程序里,为每个请求、tool runner 的每次工具调用生成 span,交给你程序里的 OpenTelemetry 发出去。不用任何权限,改几行初始化代码就行。
- API 这一侧:Anthropic 的服务器为每次请求里的模型调用、服务端工具、托管智能体的每一轮生成 span,直接推到你在 Console 里配的接收地址。要组织管理员来配。
本站实测了 SDK 这一侧。API 那一侧要一个公网 HTTPS 的接收地址,本站没有配,第八节按官方文档整理。
Claude Code 自己的 OpenTelemetry 是另一套,看的是 Claude Code 的用量和事件,见《用 OpenTelemetry 看自己的 Claude Code 用量》。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-15 |
| 接口 | 直连官方接口(Max 套餐每月赠送的额度);第九节另外通过酷站AI 中转站跑了其中三项 |
| SDK | Python 的 anthropic 1.14.0,opentelemetry-sdk 1.45.1 |
| 模型 | Haiku 5.5;中转站的模型列表里没有 Haiku 5.5,那三项用 Sonnet 5.5 |
| 方式 | 一台 Linux 服务器上,把 span 收在内存里(InMemorySpanExporter)逐个打印,所以下面的输出都是整理过的;第二节的控制台输出是原样 |
先说结论
- SDK 升级到 1.14.0,注册一个 tracer provider,就有 span。 调用
messages.create的代码一行不用改。1.13.0 还没有这个功能。 - 每个请求一个
anthropic.messages.createspan,带模型、token 数、stop_reason、请求 ID、HTTP 状态码和耗时。默认不记提示和回答,打开 content 模式才记。 - tool runner 会多出一层:一个
anthropic.messages.tool_runnerspan 下面,挂着每次请求和每次工具调用;你在工具代码里自己开的 span,挂在那次工具调用下面。 - 失败的请求状态是
ERROR,带error.type。流式请求要读完或者关掉,span 才结束。 - SDK 会自动在请求里带上
traceparent,所以配了 API 一侧以后,服务器那边的 span 能接到你自己的追踪下面。 - 走中转站,SDK 这一侧照样能用,只是少了请求 ID 和组织、工作区三个属性。API 那一侧的追踪中转站给不了。
- 记得设
service.name,不然默认是unknown_service:python。
一、SDK 和 API 两边各出什么
按官方的对照表整理:
| 能看到的 | SDK 这一侧 | API 这一侧 |
|---|---|---|
| Messages API 的请求 | 有 | 有 |
| 托管智能体的 Sessions API 请求 | 有 | 有 |
| token 用量 | 有 | 有 |
| 一次请求里的每次模型调用 | 没有 | 有 |
| 托管智能体的每一轮 | 没有 | 有 |
| 服务端工具(联网搜索等,API 来执行) | 没有 | 有 |
| 你自己的工具(tool runner 来执行) | 有 | 没有 |
| 自托管沙箱里执行的工具 | 有 | 没有 |
| 其他请求,比如上传文件 | 有 | 没有 |
| 怎么配 | 代码 | Console |
| 要什么权限 | 不用 | 组织管理员 |
| 覆盖哪些调用方 | 用 SDK 的程序 | 所有调用方,包括 curl |
两边都配上,同一个请求的 SDK span 和 API span 会出现在同一条 trace 里:你的 span 里面是 SDK 的 span,SDK 的 span 里面是 API 的 span。
二、最小的例子:先打印到控制台
装 SDK 和 OpenTelemetry:
pip install --upgrade anthropic
pip install opentelemetry-sdk
官方的示例(注释译成了中文),原样能跑:
from anthropic import Anthropic
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
# 注册一个把每个 span 打印到控制台的 tracer provider,要在第一次调用之前
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
client = Anthropic()
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(message.content)
控制台打印出一个 span(节选,ID 截短了):
{
"name": "anthropic.messages.create",
"kind": "SpanKind.CLIENT",
"status": { "status_code": "OK" },
"attributes": {
"server.address": "api.anthropic.com",
"http.route": "/v1/messages",
"gen_ai.provider.name": "anthropic",
"gen_ai.operation.name": "chat",
"gen_ai.request.model": "claude-opus-5-5",
"gen_ai.request.max_tokens": 1024,
"anthropic.request.id": "req_011C…",
"anthropic.organization.id": "…",
"anthropic.workspace.id": "…",
"gen_ai.response.id": "msg_011C…",
"gen_ai.response.model": "claude-opus-5-5",
"anthropic.message.stop_reason": "end_turn",
"gen_ai.usage.input_tokens": 15,
"gen_ai.usage.cache_write.input_tokens": 0,
"gen_ai.usage.cache_read.input_tokens": 0,
"anthropic.response.service_tier": "standard",
"gen_ai.usage.output_tokens": 52,
"gen_ai.usage.reasoning.output_tokens": 0,
"http.response.status_code": 200
},
"resource": {
"attributes": {
"service.name": "unknown_service:python"
}
}
}
几点说明:
- 属性名大多来自 OpenTelemetry 的 GenAI 语义约定(
gen_ai.*),支持这套约定的观测平台不用额外配置就认得。Anthropic 自己的属性以anthropic.开头。 anthropic.request.id就是响应头里的request-id,找官方查问题时要用它。service.name是 OpenTelemetry 的默认值。正式用的时候在 tracer provider 上设好,不然观测平台里分不清是哪个程序:
from opentelemetry.sdk.resources import Resource
provider = TracerProvider(resource=Resource.create({"service.name": "my-app"}))
接到观测平台时,把 ConsoleSpanExporter 换成 OTLP 的 exporter,指向平台给的地址。这一步和 Claude 无关,照 OpenTelemetry 官方的 Python 文档配。
三、默认记什么,content 模式多记什么
默认只记元数据
默认的模式叫 metadata_only:标识、耗时、状态、token 数,以及你自己起的名字(工具名、工具说明等)。提示和回答不进 span。
token 的几个属性要这样看:
| 属性 | 意思 |
|---|---|
gen_ai.usage.input_tokens |
输入 token,已经包含读缓存和写缓存的部分,不要再把下面两项加上去 |
gen_ai.usage.cache_read.input_tokens |
从缓存里读的 |
gen_ai.usage.cache_write.input_tokens |
写进缓存的;另有两个属性把它拆成 5 分钟和 1 小时两种 |
gen_ai.usage.output_tokens |
输出 token |
gen_ai.usage.reasoning.output_tokens |
思考用掉的 token,算在输出里面 |
实测一次请求,响应里的 usage.input_tokens 是 27,span 上的 gen_ai.usage.input_tokens 也是 27。
content 模式:提示和回答也进 span
client = Anthropic(open_telemetry={"traces": {"content_mode": "content"}})
client.messages.create(
model="claude-haiku-5-5",
max_tokens=200,
system="你是一个简洁的助手。",
messages=[{"role": "user", "content": "用一句话说 OpenTelemetry 是什么。"}],
)
span 上多了三个属性,都是 JSON 字符串:
gen_ai.system_instructions = [{"type":"text","content":"你是一个简洁的助手。"}]
gen_ai.input.messages = [{"role":"user","parts":[{"type":"text","content":"用一句话说 OpenTelemetry 是什么。"}]}]
gen_ai.output.messages = [{"role":"assistant","parts":[{"type":"text","content":"OpenTelemetry 是一个开源的可观测性框架……"}]}]
也可以不改代码,用环境变量打开:
export ANTHROPIC_OPEN_TELEMETRY_TRACES_CONTENT_MODE=content
打开之前想清楚:
- 提示和回答会存在你的观测平台里,按那个平台的权限和保留期管理。里面有用户数据的,先确认合规。
- 每个 span 默认最多带 0.75 MB 的内容。 超出时 SDK 会截短,并把
anthropic.content.truncated设成true:请求的 span 先丢系统提示和最早的消息,工具的 span 从结果末尾丢。图片和文档只记类型。上限用traces.max_content_bytes改。 - 给
open_telemetry传了参数,就整个覆盖相关的环境变量,没写的键用默认值,即使对应的环境变量设了也不管用。
四、tool runner:每次工具调用一个 span
tool runner 是 SDK 里替你跑工具循环的帮手。它的 span 最能看出追踪的用处。下面在自己的 span answer_question 里跑 tool runner,工具代码里再开一个 span:
import json
from anthropic import Anthropic, beta_tool
from opentelemetry import trace
tracer = trace.get_tracer("my-app")
client = Anthropic()
@beta_tool
def get_weather(city: str) -> str:
"""查一个城市现在的天气。
Args:
city: 城市名,比如杭州
"""
with tracer.start_as_current_span("lookup_weather_db"): # 工具自己的代码开的 span
return json.dumps({"city": city, "weather": "晴", "temp_c": 22}, ensure_ascii=False)
with tracer.start_as_current_span("answer_question"):
runner = client.beta.messages.tool_runner(
model="claude-haiku-5-5",
max_tokens=400,
tools=[get_weather],
messages=[{"role": "user", "content": "杭州现在天气怎么样?"}],
)
final = runner.until_done()
实测得到 6 个 span,层级是这样(括号里是这次的耗时和关键属性):
answer_question(2.36 秒)
└─ anthropic.messages.tool_runner(2.36 秒,gen_ai.operation.name = invoke_agent)
├─ anthropic.messages.create(1.39 秒,stop_reason = tool_use,输入 409,输出 50)
├─ anthropic.custom_tool_use get_weather(gen_ai.tool.name = get_weather)
│ └─ lookup_weather_db
└─ anthropic.messages.create(0.88 秒,stop_reason = end_turn,输入 490,输出 25)
- 每次请求、每次工具调用一目了然:哪一步慢,哪一步报错,一共几轮。
- 工具的 span 带工具名、工具说明(取自 docstring)和这次调用的 ID。调用的参数和工具返回的结果只在 content 模式下才记。
- 工具代码里开的 span 自动挂在工具 span 下面,你查数据库、调别的服务的耗时都能接上。
anthropic.messages.tool_runner这个 span 本身没有 token 数,要把下面每个anthropic.messages.create的加起来。
五、流式和报错
用 messages.stream() 读完一个流,span 在读完时结束,多一个 gen_ai.response.time_to_first_chunk,是从发出请求到第一段文字的秒数(这次是 0.91 秒)。在 content 模式下,span 里也有完整的回答。
用 create(stream=True),读了两个事件就停下,实测:
- 没关流之前,结束的 span 一个也没有:span 要等流读完或关掉才结束。
- 调了
stream.close()之后 span 才结束,状态是UNSET(没设),不是OK。 - 即使开了 content 模式,span 里也没有回答。官方说只有用
messages.stream()读,span 里才有回答。
所以读到一半不读的流,一定要关掉,不然 span 一直挂着。另外,读流的循环里你自己开的 span,和 SDK 的 span 是兄弟关系,不是父子。想把它们归到一起,在调用和读流外面再包一个自己的 span。
请求失败:故意写一个不存在的模型名,SDK 抛 NotFoundError,span 的状态是 ERROR:
anthropic.messages.create 状态 ERROR
gen_ai.request.model = claude-no-such-model
http.response.status_code = 404
error.type = not_found_error
error.type 就是官方报错里的类型,比如限流时是 rate_limit_error。在观测平台里按它分组,就能看出报错主要是哪一类。
六、把几次调用串成一段对话,或者关掉追踪
串成一段对话:给几次调用同一个 conversation_id,SDK 把它写进 gen_ai.conversation.id,观测平台里按它筛:
client = Anthropic()
chat = client.with_options(
open_telemetry={**client.open_telemetry, "conversation_id": "conv_42"}
)
chat.messages.create(model="claude-haiku-5-5", max_tokens=200, messages=[...])
with_options() 会整个替换 open_telemetry,所以要从 client.open_telemetry 展开再加键。这个 ID 在两种模式下都会发出去,不要放个人信息和密钥。
关掉追踪,两种办法实测都是 0 个 span:
quiet_client = Anthropic(open_telemetry=False) # 只关这一个客户端
export ANTHROPIC_OPEN_TELEMETRY=false # 用环境变量,整个进程
什么时候要关:别的库已经在追踪 SDK 的请求了,不关的话每个请求会出现两次。
七、托管智能体的请求也有 span
托管智能体的每个 API 请求,SDK 同样出 span。实测建一个会话、打开事件流、发一条消息,等它回答完:
with tracer.start_as_current_span("chat_request"):
session = client.beta.sessions.create(agent=agent.id, environment_id=env.id)
stream = client.beta.sessions.events.stream(session.id)
client.beta.sessions.events.send(session.id, events=[
{"type": "user.message", "content": [{"type": "text", "text": "你好,介绍一下你自己。"}]}])
for event in stream:
if event.type == "session.status_idle":
break
stream.close()
chat_request
├─ anthropic.sessions.create http.route = /v1/sessions
├─ anthropic.sessions.events.stream http.route = /v1/sessions/{session_id}/events/stream,状态 UNSET
└─ anthropic.sessions.events.send http.route = /v1/sessions/{session_id}/events
- span 的名字是
anthropic.加接口路径,去掉了beta:client.beta.sessions.events.send()对应anthropic.sessions.events.send。 - 跟会话有关的三个 span 都带
anthropic.session.id,按它就能把一个会话的请求归到一起。 - 事件流是我们提前关掉的,所以状态是
UNSET。一直开着的流,span 要到关的时候才结束。
智能体在云端每一轮做了什么(模型调用、bash 等工具),SDK 这一侧看不到,要靠 API 一侧的追踪。
八、API 这一侧:在 Console 里配接收地址
这一节按官方文档整理,本站没有实测。
实测过的部分:SDK 会带上 traceparent
API 一侧的 span 要接到你的追踪下面,靠的是请求头里的 W3C traceparent。这一点在 SDK 这边可以验证。我们在 HTTP 客户端上挂了一个钩子,只检查这个头,不打印任何请求头:
import httpx2 # SDK 的 http_client 要的是 httpx2.Client
seen = []
hook = {"request": [lambda request: seen.append(request.headers.get("traceparent"))]}
client = Anthropic(http_client=httpx2.Client(event_hooks=hook))
结果:
- 请求带了
traceparent;里面的 trace id 和 SDK span 的一致,父 span 就是 SDK 的那个 span。 - 最后一段标志是
03,最低位是 1,表示已采样。API 只为已采样的请求出 span。 - 传
open_telemetry={"propagation": False}之后,请求不再带这个头,SDK 照样出 span。你的 HTTP 客户端已经有别的插桩在写这个头时,用它避免重复。
怎么配
要准备三样东西:
- Console 组织的管理员角色。开发者角色只能看。
- 一个 OTLP/HTTP 的接收地址:接受 protobuf 编码,HTTPS,443 端口,要用域名,不能用 IP。自己跑 OpenTelemetry Collector 的话,它默认在 4318 端口,要改成 443 的 HTTPS,或者前面放一个反向代理。
- 这个地址的凭据,比如观测平台的 API key。
然后:
- 在 Console 的 OpenTelemetry 设置页(Settings → telemetry destinations)点 Add destination。
- Provider 选你的观测平台,不在列表里就选 Custom。官方列出的合作平台有 Arize AI、Braintrust、Databricks、Datadog、Grafana Cloud、Honeycomb、Langfuse、LangSmith、New Relic、PostHog、Sentry 和 Splunk;选了它们,Console 会直接要那个平台的 key。
- 选 Custom 的话,填 OTLP endpoint URL(基础地址,不要带
/v1/traces,API 会自己加)和 Request headers(凭据只放在头的值里,保存后 Console 不再显示)。 - Content mode 选只记元数据还是带内容,意思和第三节一样。
- Workspaces 选这个地址接收哪些工作区:全部、选中的几个,或者除了某几个之外的全部。
- 保存后在 More actions 里点 Test,平台上应该收到一个
anthropic.testspan。 - 在对应的工作区里发一个请求,等一分钟左右,Delivery 一栏显示 Healthy。平台里按
service.name=claude-platform筛,就是 API 发来的 span。
也可以用 Admin API 建,要管理员的 Admin key:
client.beta.organization.telemetry_destinations.create(
endpoint_url="https://gateway.example.com/otlp",
headers={"authorization": "Bearer 观测平台的 key"},
traces={"content_mode": "metadata_only"},
workspaces={"type": "all"},
)
投递的规矩
- 至少一次:同一个 span 可能收到不止一次,按 trace id 和 span id 去重。
- 尽力而为:span 可能丢,丢了不会补发。所以官方说,导出的 span 不能当完整的账单用;要按 key 统计用量,比如发现 key 泄露,用 Usage API(见《团队用 Claude API 之前要管好的事》)。
- 每次投递先用 gzip 压缩;你的地址回 400 或 415,就马上不压缩再发一次。
- 超时、连不上、DNS 或 TLS 出错,以及 408、429、5xx,会过一会儿重试几次;别的错误不重试。
- 你的地址要在 5 秒内完成 TLS 握手、5 秒内回响应头,一次投递总共不超过 10 秒。
- 以下情况 API 会停用这个地址:回了 3xx 跳转;域名解析到不是公网的 IP;连续 24 小时全部投递失败。修好后要手动点 Enable。
- 一个组织最多 10 个接收地址(停用的也算)。
两边都配时,token 别算两遍
同一个请求,SDK 和 API 各有一个 anthropic.messages.create:SDK 的 kind 是 CLIENT,API 的是它下面的 SERVER。官方的算法:
- 两边都有时,只加 API 的 span。
- API 一侧,加
anthropic.messages.create或者它下面的anthropic.model_request,二选一,不要都加。 - 托管智能体的会话,加模型调用的 span(
anthropic.model_request)和结果评估的 span(名字以outcome_evaluation结尾)。 - 用了上下文压缩的请求,要另外加压缩用掉的 token,
gen_ai.usage.*里不含这部分。
数据保留
- 开了零数据保留(ZDR)的组织也能用 Full content:Messages API 的内容只在内存里待到送出为止。托管智能体本来就不在零数据保留的范围内,开不开追踪都一样。
- 组织和 Anthropic 签了 HIPAA、FERPA 或 K-12 数据限制的,API 一侧不管选哪种模式都只发元数据。SDK 一侧不受影响。
- span 送到你的平台以后,就按那个平台的保留期和权限管了。
九、走中转站
SDK 的 span 是在你自己的程序里生成的,所以走中转站照样有。我们把第二节、第四节和第八节的 traceparent 检查,通过酷站AI 中转站又跑了一遍(模型换成 Sonnet 5.5):
- span 的数量和层级和直连官方完全一样:tool runner 那次同样是 6 个 span,工具 span、工具代码里的 span 都在。
- token 数、
stop_reason、HTTP 状态码、耗时都有。 traceparent照样带上,trace id 和父 span 都对得上。- 少了三个属性:
anthropic.request.id、anthropic.organization.id、anthropic.workspace.id。这三个值是从官方的响应头里读的,中转站没有传过来。 server.address和url.full是中转站的地址。
API 那一侧就用不上了:API 的 span 只发给请求所在工作区对应的接收地址。走中转站时,请求不在你自己的 Console 组织里,你配的接收地址收不到。
十、出问题时查什么
| 现象 | 查什么 |
|---|---|
| 一个 span 也没有 | tracer provider 要在第一次调用之前注册;SDK 要升级到支持追踪的版本(本站测的是 1.14.0);ANTHROPIC_OPEN_TELEMETRY 不能是 false |
| SDK 的 span 没有嵌在你的 span 下面 | 要在你自己的 span 里面调用 SDK |
| 每个请求出现两次 | 同名的 SDK span 和 API span,按 kind 或 service.name 区分;或者别的库也在追踪 SDK,关掉一边 |
| 流式请求的 span 一直不结束,或者没有回答 | 读到一半的流要关掉;要回答内容用 messages.stream() |
请求里没有 traceparent |
给 OpenTelemetry 注册 W3C trace context 的 propagator,SDK 用你程序的 propagator 写这个头 |
| API 一侧收不到 span | 请求所在的工作区要被某个启用的接收地址覆盖;刚改完要等一分钟左右 |
还有一条:beta 期间,SDK 的选项、span 和属性的名字都可能改,而且可能不兼容。官方建议在依赖里锁定 SDK 的版本,自己决定什么时候升级;你的查询和看板里写死了属性名的,升级前对一下官方的 span 参考。