2026-09-07 · 主题:OpenTelemetry 遥测(metrics / events / traces beta) 前置:第 8 课(上下文管理)、第 15 课(成本控制)、第 19 课(Skills)、第 25 课(
/cost的缓存命中原因)。 与前面课程的分界:第 15 课教的是在会话里看成本(/cost、状态栏),这一课教的是把数据吐到会话外面——落到 Prometheus / Grafana / Datadog / 任意 OTLP 后端,变成能跨天、跨项目、跨人查询的时间序列。
为什么值得学这一课
/cost 和状态栏只回答「这一次会话花了多少」。但真正想知道的那些问题都是跨会话的:
- 我这个月在哪个项目上烧的 token 最多?
- 那个 skill 我到底用没用上,它每次进来占多少 context?
- 权限提示我拒绝了多少次、拒的都是哪类工具?
- 上周三下午那次 agent 空转两小时,是卡在 API 重试还是卡在某个工具?
这些数据 Claude Code 本来就在产,只是默认不往外送。打开遥测,是一次性成本大概二十分钟的事。
Claude Code 的遥测走标准 OpenTelemetry:metrics(时间序列)、events/logs(离散事件)、可选的 traces(分布式追踪,beta)。任何接受 OTLP 的后端都能收。
技巧一(入门):四个环境变量,先在终端里看见数据
不要一上来就接后端。先用 console exporter 把指标打到自己终端,确认数据真的在产出。
export CLAUDE_CODE_ENABLE_TELEMETRY=1 # 必须,总开关
export OTEL_METRICS_EXPORTER=console # 先打到终端
export OTEL_METRIC_EXPORT_INTERVAL=1000 # 1 秒一次(默认 60000ms,调试时太慢)
claude
看见输出后,再切到真正的后端。最常见的两种:
# A. OTLP over gRPC(发给本地 collector)
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer your-token" # 后端要鉴权时
# B. Prometheus(自己起 exporter,让 Prometheus 来抓 http://localhost:9464/metrics)
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=prometheus
三个必须知道的坑:
OTEL_EXPORTER_OTLP_PROTOCOL没有默认值。 只要你启用了otlpexporter,就必须显式设grpc/http/json/http/protobuf之一,否则什么都不会发出去。- 验证方式是找一个具体指标:配好后去后端查
claude_code.session.count——它在会话启动时就会发。只开了 logs 的话,提交一次 prompt 然后查claude_code.user_prompt事件。 - 什么都没到怎么办:跑
claude --debug。exporter 的失败会以[3P telemetry]前缀报出来(3P = third-party)。看到[Anthropic telemetry]前缀的行不用管,那是 Anthropic 自己的运营遥测,和你的配置无关。
生产环境记得把 OTEL_METRIC_EXPORT_INTERVAL 调回默认的 60000ms,1 秒一次纯属浪费。
技巧二:八个指标怎么读,以及怎么按 skill / plugin / agent 拆开看
Claude Code 一共导出 8 个指标:
| 指标名 | 含义 | 单位 |
|---|---|---|
claude_code.session.count |
启动的会话数 | — |
claude_code.lines_of_code.count |
改动的代码行数 | — |
claude_code.pull_request.count |
创建的 PR 数 | — |
claude_code.commit.count |
创建的 git commit 数 | — |
claude_code.cost.usage |
会话成本 | USD |
claude_code.token.usage |
token 用量 | tokens |
claude_code.code_edit_tool.decision |
代码编辑类工具的权限决策次数 | — |
claude_code.active_time.total |
活跃时长(不含空闲) | s |
真正有用的是拆维度。官方文档明确说明 claude_code.token.usage 可以按 type(input/output)、user、team、model、skill.name、plugin.name、agent.name 拆开。这直接回答了第 19 课留下的问题——哪个 skill 在真的干活,哪个只是在占 context:
# 各 skill 的 token 消耗排行(Prometheus 语法示意)
topk(10, sum by (skill_name) (rate(claude_code_token_usage[24h])))
# subagent 分工的实际成本占比
sum by (agent_name) (claude_code_cost_usage)
claude_code.active_time.total 值得单独说:它只在用户交互中(打字、读回复)和 CLI 处理中(工具执行、模型生成)累加,空闲不计。所以「今天用了 8 小时 Claude Code」和「今天有 2.3 小时是真的在推进」是两个数,后者才是你排期时该用的那个。
两个默认值需要主动改:
export OTEL_METRICS_INCLUDE_VERSION=true # 默认 false,但排查「某版本引入的回归」时必开
export OTEL_METRICS_INCLUDE_ENTRYPOINT=true # 默认 false,区分 cli / sdk-cli / sdk-ts / sdk-py / claude-vscode
反过来,OTEL_METRICS_INCLUDE_SESSION_ID 默认是 true——如果你的会话数很多,这条会把指标基数(cardinality)拉爆,存储成本上升。团队级看板可以关掉它:OTEL_METRICS_INCLUDE_SESSION_ID=false。
技巧三:多项目 / 多团队归因,用 OTEL_RESOURCE_ATTRIBUTES 打标
自己一个人也有意义——把游戏项目、日报流水线、实验仓库分开算账:
# 在每个项目的 .envrc / direnv 里设,或写进项目启动脚本
export OTEL_RESOURCE_ATTRIBUTES="project=ios_game_a,workstream=gamedev,cost_center=personal"
这些自定义 key 会同时出现在 OTLP resource block 和每一个 metric datapoint 的属性上,所以多数后端能直接把它们当 label 来 group by / filter。
格式规则很严,踩过就知道(官方文档的 Warning 原文列举):
- 不能有空格。
org.name=My Company是非法的。 - 必须是逗号分隔的
key=value。 - 只允许 US-ASCII,且排除控制字符、空白、双引号、逗号、分号、反斜杠。
- 加引号不能转义空格——
org.name="My Company"得到的字面值是"My Company"(带引号),不是My Company。
要空格只能这么写:
export OTEL_RESOURCE_ATTRIBUTES="org.name=Johns_Organization" # 下划线
export OTEL_RESOURCE_ATTRIBUTES="org.name=JohnsOrganization" # camelCase
export OTEL_RESOURCE_ATTRIBUTES="org.name=John%27s%20Organization" # 百分号编码
基数警告:每个自定义 key 会变成每条指标序列上的一个 label。如果你打的是高基数的值(比如分支名、时间戳),存储成本会直线上升。想只放进 resource block、不进 datapoint label:
export OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES=false
另外:自定义 key 永远不会覆盖内建标准属性(user.id、session.id 等),撞名时 Claude Code 保留内建值。
技巧四(进阶):打开 beta 分布式追踪,把一次提问还原成一条 trace
指标告诉你「花了多少」,trace 告诉你「时间去哪了」。这是排查「agent 空转」「某次任务慢得离谱」唯一靠谱的手段。
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 # 追踪总开关,必须
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
span 层级结构(每次用户 prompt 起一个根 span):
claude_code.interaction ← 一次提问 = 一条 trace
├── claude_code.llm_request ← 每次 API 调用
├── claude_code.hook ← 需要 detailed beta tracing
└── claude_code.tool
├── claude_code.tool.blocked_on_user ← 等你点「同意/拒绝」的时间
├── claude_code.tool.execution ← 工具真正跑的时间
└── (Agent 工具)subagent 的 llm_request / tool span 嵌在这里
这个结构本身就解答了一个常见困惑:「慢」到底是慢在模型、慢在工具、还是慢在等我批权限——blocked_on_user 和 execution 是分开的两个 span,一眼就能分辨。
值得关注的 span 属性:
claude_code.llm_request上有ttft_ms(首 token 时间)、input_tokens/output_tokens/cache_read_tokens/cache_creation_tokens、attempt(总尝试次数,含重试)、stop_reason、request_id。cache_read_tokens配合第 25 课的缓存命中原因一起看,能定位到具体是哪一轮把缓存打掉的。claude_code.tool上有result_tokens(工具结果的近似 token 大小)——这是找出「哪个工具在往 context 里灌垃圾」的直接证据。agent_id/parent_agent_id/workflow.run_id:多 agent 场景下,把 subagent 的开销归因到具体是谁派的活。
两个跨进程的实用细节:
- 追踪开启时,Bash 和 PowerShell 子进程会自动继承
TRACEPARENT环境变量(W3C trace context)。任何读TRACEPARENT的脚本都能把自己的 span 挂到同一条 trace 下——你的构建脚本、测试脚本可以和 Claude 的动作出现在同一张时间线上。 - 在 Agent SDK 和
claude -p无头会话里,Claude Code 会反过来读环境里的TRACEPARENT/TRACESTATE,把claude_code.interaction挂成调用方 span 的子节点。这意味着第 12 课的 CI 流水线可以做到:一条 trace 从 CI job 开始,穿过 Claude Code,一直到它跑的每个工具。交互式会话则故意忽略入站TRACEPARENT,避免误继承 CI / 容器环境里的游离值。
隐私默认值(重要,别乱开):span 默认脱敏 prompt 文本、工具入参和工具内容。要放开需要显式开关:
| 变量 | 放开的内容 | 默认 |
|---|---|---|
OTEL_LOG_USER_PROMPTS |
prompt 原文 | 关 |
OTEL_LOG_ASSISTANT_RESPONSES |
助手回复原文 | 关(未设时跟随上一条) |
OTEL_LOG_TOOL_DETAILS |
Bash 命令、MCP server/tool 名、skill 名、文件路径、工具入参 | 关 |
OTEL_LOG_TOOL_CONTENT |
工具输入输出正文(需追踪开启,60KB 截断) | 关 |
OTEL_LOG_RAW_API_BODIES |
完整 Messages API 请求/响应 JSON(含整段对话历史) | 关 |
个人排查时,OTEL_LOG_TOOL_DETAILS=1 通常是性价比最高的一个——它能让 skill_name、full_command、file_path 出现在 span 上,而不至于像 OTEL_LOG_RAW_API_BODIES 那样把整段对话灌进日志后端。注意 OTEL_LOG_RAW_API_BODIES=1 在文档里被明确写为「等同于同时同意 OTEL_LOG_USER_PROMPTS / TOOL_DETAILS / TOOL_CONTENT 会暴露的一切」,共享的机器上不要开。
一个坑:OTEL_* 不会传给子进程
Claude Code 不会把 OTEL_* 环境变量传给它 spawn 的子进程——包括 Bash 工具、hooks、MCP server、language server。
所以:如果你通过 Bash 工具跑的某个应用自己也想上报 OTel,得在那条命令里直接设这些变量,它不会从 Claude Code 继承 endpoint 和 headers。
(唯一例外是上面说的 TRACEPARENT:追踪开启时它会注入子进程,但那只是 trace context,不是 exporter 配置。)
本课要点
- 先用
consoleexporter 验证数据在产,再接后端;OTEL_EXPORTER_OTLP_PROTOCOL必须显式设。 - 8 个指标里最有信息量的是
token.usage按skill.name/agent.name拆开,以及active_time.total(不含空闲的真实工作时长)。 OTEL_RESOURCE_ATTRIBUTES做多项目归因,格式极严(无空格、无引号转义),高基数值记得用OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES=false挡在 label 外。- beta trace 把
blocked_on_user和execution分成两个 span,这是分辨「模型慢 / 工具慢 / 我批权限慢」的唯一直接证据;CI 里通过TRACEPARENT可以做到端到端一条 trace。
参考来源
- Monitoring — Claude Code Docs(本课全部配置、指标、span 属性的一手来源)
- Observability with OpenTelemetry — Agent SDK
- Track team usage with analytics — Claude Code Docs
- Manage costs effectively — Claude Code Docs
- Claude Code changelog(各变量的最低版本要求,如
OTEL_LOG_ASSISTANT_RESPONSES需 v2.1.193+、CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH需 v2.1.214+)
主页