Mingyu's Library主页
2026年9月7日

第 27 课:可观测性 —— 把 Claude Code 的用量、成本与失败接进你的看板

2026-09-07 · 主题:OpenTelemetry 遥测(metrics / events / traces beta) 前置:第 8 课(上下文管理)、第 15 课(成本控制)、第 19 课(Skills)、第 25 课(/cost 的缓存命中原因)。 与前面课程的分界:第 15 课教的是在会话里看成本(/cost、状态栏),这一课教的是把数据吐到会话外面——落到 Prometheus / Grafana / Datadog / 任意 OTLP 后端,变成能跨天、跨项目、跨人查询的时间序列。


为什么值得学这一课

/cost 和状态栏只回答「这一次会话花了多少」。但真正想知道的那些问题都是跨会话的:

这些数据 Claude Code 本来就在产,只是默认不往外送。打开遥测,是一次性成本大概二十分钟的事。

Claude Code 的遥测走标准 OpenTelemetry:metrics(时间序列)、events/logs(离散事件)、可选的 traces(分布式追踪,beta)。任何接受 OTLP 的后端都能收。

官方文档:Monitoring — Claude Code Docs


技巧一(入门):四个环境变量,先在终端里看见数据

不要一上来就接后端。先用 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

三个必须知道的坑:

  1. OTEL_EXPORTER_OTLP_PROTOCOL 没有默认值。 只要你启用了 otlp exporter,就必须显式设 grpc / http/json / http/protobuf 之一,否则什么都不会发出去。
  2. 验证方式是找一个具体指标:配好后去后端查 claude_code.session.count——它在会话启动时就会发。只开了 logs 的话,提交一次 prompt 然后查 claude_code.user_prompt 事件。
  3. 什么都没到怎么办:跑 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 原文列举):

要空格只能这么写:

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 属性:

两个跨进程的实用细节:

  1. 追踪开启时,Bash 和 PowerShell 子进程会自动继承 TRACEPARENT 环境变量(W3C trace context)。任何读 TRACEPARENT 的脚本都能把自己的 span 挂到同一条 trace 下——你的构建脚本、测试脚本可以和 Claude 的动作出现在同一张时间线上。
  2. 在 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 配置。)


本课要点

  1. 先用 console exporter 验证数据在产,再接后端;OTEL_EXPORTER_OTLP_PROTOCOL 必须显式设。
  2. 8 个指标里最有信息量的是 token.usage 按 skill.name / agent.name 拆开,以及 active_time.total(不含空闲的真实工作时长)。
  3. OTEL_RESOURCE_ATTRIBUTES 做多项目归因,格式极严(无空格、无引号转义),高基数值记得用 OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES=false 挡在 label 外。
  4. beta trace 把 blocked_on_user 和 execution 分成两个 span,这是分辨「模型慢 / 工具慢 / 我批权限慢」的唯一直接证据;CI 里通过 TRACEPARENT 可以做到端到端一条 trace。

参考来源