ClaudeClaude API系列第 25 / 25 篇

给 Claude API 接上 OpenTelemetry:每次请求、每次工具调用都有 span,能嵌进你自己的追踪

Claude 的 SDK 和 API 新加了 OpenTelemetry 追踪(beta)。本站用官方 key 实测了 Python SDK 一侧:每个请求、每次工具调用、托管智能体的会话请求都有 span,调用代码不用改,走中转站也能用。API 一侧要在 Console 配接收地址,按文档整理。

发布 · 约 14 分钟读完 · 经人工审核()· 本站的内容怎么来的

本站已实测,测试日期:2026-10-15。

本页目录(11 节)
  1. 先说结论
  2. 一、SDK 和 API 两边各出什么
  3. 二、最小的例子:先打印到控制台
  4. 三、默认记什么,content 模式多记什么
  5. 四、tool runner:每次工具调用一个 span
  6. 五、流式和报错
  7. 六、把几次调用串成一段对话,或者关掉追踪
  8. 七、托管智能体的请求也有 span
  9. 八、API 这一侧:在 Console 里配接收地址
  10. 九、走中转站
  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.create span,带模型、token 数、stop_reason、请求 ID、HTTP 状态码和耗时。默认不记提示和回答,打开 content 模式才记。
  • tool runner 会多出一层:一个 anthropic.messages.tool_runner span 下面,挂着每次请求和每次工具调用;你在工具代码里自己开的 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。

然后:

  1. 在 Console 的 OpenTelemetry 设置页(Settings → telemetry destinations)点 Add destination。
  2. Provider 选你的观测平台,不在列表里就选 Custom。官方列出的合作平台有 Arize AI、Braintrust、Databricks、Datadog、Grafana Cloud、Honeycomb、Langfuse、LangSmith、New Relic、PostHog、Sentry 和 Splunk;选了它们,Console 会直接要那个平台的 key。
  3. 选 Custom 的话,填 OTLP endpoint URL(基础地址,不要带 /v1/traces,API 会自己加)和 Request headers(凭据只放在头的值里,保存后 Console 不再显示)。
  4. Content mode 选只记元数据还是带内容,意思和第三节一样。
  5. Workspaces 选这个地址接收哪些工作区:全部、选中的几个,或者除了某几个之外的全部。
  6. 保存后在 More actions 里点 Test,平台上应该收到一个 anthropic.test span。
  7. 在对应的工作区里发一个请求,等一分钟左右,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 参考。

来源

通过 API 中转站使用这些模型?到 Veridrop 免费检测中转站给你的是不是它声称的模型,或者看看中转站排行榜前 100 名。

相关文章