像 Claude Code、OpenAI Codex、Google Antigravity 和 Cursor 这样的智能体编程工具,如今已成为日常软件开发的标配。
随着智能体系统日趋成熟,开发者交给它们的大部分工作都是层层委派完成的,一次交给一个 subagent。许多团队也在探索并采用共享式、多租户的智能体基础设施,这类基础设施的成本并不由某个单一负责人承担。正因如此,可观测性成为监控基础设施成本的关键。
在本指南中,你将先了解可观测性的工作原理,随后开启 Claude Code 内置的遥测功能,运行一个后端来收集这些数据,并解读它输出的指标、日志和追踪信息。这能帮你更有效地追踪团队成本;而且随着遥测数据日渐成熟、与会话的关联愈发清晰,效果只会越来越好。
注意:就目前而言,Claude Code 输出的遥测数据不包含任何属性,无法可靠地映射到具体的命名会话。虽然可以借助 session_id 追踪用量,但在一个混杂了多个提示/技能的长会话中,这种方式仍显得相当笨拙。
本指南的范围仅限于 Claude Code 在指标、日志与追踪方面的遥测功能。请注意,其内容仅适用于 Linux 和 macOS。
目录
使用 OpenTelemetry 实现可观测性
可观测性指的是这样一种能力:仅凭系统输出的数据,就能回答有关其运行时行为的问题。要做到这一点,你无需窥探系统内部,不必附加调试器、阅读源代码,也不用动手复现相关行为。
这里的“运行时行为”,指的是从外部可以看到的表现。你可以提出类似这样的问题:
95% 的请求耗时是多少。
所有收到的请求中,失败率是多少。
服务所用内存缓存的命中率是多少。
服务的配置副本数与实际部署副本数之间的差异。
对 Claude Code 而言,外界无从得知的内部机制包括:它如何管理上下文、如何把工作拆分到多次 LLM 调用中,以及如何编排各个子代理(subagent)。不过,你可以通过读取 Claude Code 发出的遥测数据来回答这类问题:
某位开发者或某个团队在一天、一周或一个月内花了多少钱。
这些用量在受支持的各个模型和 effort 级别之间如何分布。
每花一美元能换来多少 token,以及按类型(输入、输出、cacheRead、cacheCreation)划分时,这个数字有多大差异。
压缩(compaction)事件何时触发,以及它把上下文的 token 占用降低了多少。
只有经过插桩的系统才能回答这些问题。所谓插桩(instrumentation),是指由开发者手动添加或内置于工具中的一段代码,用来记录程序的运行时行为,并将其以遥测数据的形式输出。比如像 this request spent 100 tokens 这样的一条度量记录。
遥测数据能提供一条结构清晰的数据轨迹,完整记录系统随时间变化的行为,从而帮助避免静默故障。例如,下面这张图出自 GitHub 2026 年 8 月 17 日故障的复盘文章,展示了 GitHub Actions 的运行量如何随时间从约 3000 万增长到约 1.1 亿:
遥测数据
发出的遥测数据分为三类:
指标(Metrics):在时间窗口内聚合的数值度量,例如每秒查询数(QPS)。
日志(Logs):带时间戳的单个事件的详细记录。例如 Claude Code 中的一次压缩事件。
追踪(Traces):一个请求在系统中流转的完整路径,按时间切分为一层层嵌套的子请求。例如电商网站上的下单请求,能显示出它为完成请求调用了哪些内部服务。
OpenTelemetry(https://opentelemetry.io/)是一个可观测性框架,帮助你生成并采集这些信号,再导出到后端,由后端负责存储、查询和可视化。这种职责分离让方案不绑定任何特定工具或厂商——后端既可以是开源方案,也可以是闭源产品。它为多种编程语言提供了插桩 SDK。
为 Claude Code 插桩
插桩代码通常随应用一起运行,部署在最适合测量的位置:比如请求进入或响应发出的中间件,或是统计 token 数量的环节。
插桩接入应用的方式有两种:
共享插桩库:使用开源框架的应用可以把插桩库作为依赖引入,并将其与应用的生命周期方法关联起来。OpenTelemetry 为众多框架发布了现成的插桩库(例如 Spring Framework)。
由应用开发者定制实现:借助 OpenTelemetry SDK,直接在代码库中添加遥测数据的发射点。闭源产品虽然代码私有,但同样能发出符合 OpenTelemetry 标准的遥测数据。
示例:HTTP 插桩
HTTP 处理中的中间件是所有请求的必经之路,认证这类全局配置之所以放在这里,正是这个原因。同样的道理,中间件也是放置插桩代码的理想位置:只需包装一次处理器,就能测量每一个请求。
上图展示了插桩相对于请求路径所处的位置。
客户端请求先经过 HTTP 中间件,随后才到达应用的请求处理器和下游调用。中间件借助 SDK 提供的结构为处理器计时,并记录耗时、状态和计数。
之后,SDK 会将这些测量数据缓存起来,在后台线程中把 OTLP 数据推送给 Collector,完全不占用请求路径。
Claude Code 属于第二种情况:核心应用及其插桩模块都由 Anthropic 提供。统计 token 用量、成本和工具调用的代码已经内置其中,并通过 OTLP 发送 OpenTelemetry 遥测数据。作为 Claude Code 用户,你只需启用遥测功能,再准备好一个后端来接收并分析这些数据即可。
注意:OTLP 是 OpenTelemetry 项目设计的一种遥测数据传输协议。本指南假定整条链路均与 OTLP 兼容。如果接入不兼容的遥测数据,或使用不兼容的后端组件,可能出现不可预知的结果,不在本文讨论范围之内。
要采集、存储并查看遥测数据,你需要以下组件:
OpenTelemetry Collector:一套与厂商无关的实现,负责接收、处理并导出遥测数据。个人环境下可选,但强烈推荐;生产环境则必不可少。
Jaeger:分布式链路追踪后端,由 Uber 开源。
Prometheus:以时间序列的形式采集并存储指标数据。
Loki:Grafana 出品的可扩展日志聚合系统。
Grafana:用于对日志、指标和链路进行可视化展示。
拉取与推送:遥测数据如何离开应用
遥测数据离开应用的方式不外乎两种:
拉取(抓取):应用把当前指标暴露在一个 HTTP 端点上,抓取器(Prometheus)定期读取该端点。每个运行中的实例都需要独占一个端口,而且抓取器必须事先知道所有这些地址。应用处于被动地位:数据流动由抓取器驱动。这种方式适合地址固定、长期运行的进程。
在 OpenTelemetry 中,基于拉取的抓取方式通过 OTEL_METRICS_EXPORTER=prometheus 来配置(导出器的可选取值参见 SDK 环境变量文档)。
按照惯例,应用会在 http://localhost:9464/metrics 上提供指标数据。该导出器仅处理指标。
推送(OTLP):应用按固定间隔把遥测数据发送到接收端点。这种方式更灵活:任意数量的进程都可以向同一个端点推送数据,无需事先注册,因此应用可以随意启停,地址变了也不受影响。
在 OpenTelemetry 中,推送通过 OTEL_METRICS_EXPORTER=otlp 来配置,数据经由 OTLP 导出器发出。
推送模式可以承载指标、日志和链路三类数据。
什么时候需要部署 Collector
当现有技术栈已无法应对系统日益增长的规模和复杂度时,就需要部署 Collector 了。它能带来以下帮助:
统一的导出配置:所有生产者都只需指向 Collector,不必各自维护一套针对不同后端的导出器配置。
只需出站连接:企业网络常常会封锁拉取式抓取器所需的入站连接。有了 Collector,应用只需向它推送数据,再由它继续向外推送,任何环节都无需接受入站流量。
扇出与格式转换:Collector 可以把遥测数据转换成厂商的存储格式,并将同一份信号发送到多个后端。
缓冲:一旦某个后端宕机,Collector 会暂存数据并自动重试,从而扛住临时性的故障。
数据处理:在数据发往存储之前,它可以应用处理器,例如对属性进行脱敏。
注意:尽管本文面向的是单用户场景,本指南仍采用带 Collector 的推送式配置。这套技术栈包含三个后端,它们存储和查询数据的方式各不相同;让 Collector 一次性接收 Claude Code 的 OTLP 数据,再把每类信号路由到合适的位置,比让应用逐一对接三个后端要简单得多。这正是工具清单把 Collector 标注为“个人使用可选、生产环境必备”的原因:生产者和后端越多,它的价值就越大。
前置条件
本指南的每一节都附有相关文档链接,不过如果你已经熟悉下面列出的工具和查询语言,上手会快得多:
你需要准备
最新版 Claude Code
Docker Engine(29.4.1+)以及 Docker Compose。
- 可运行 5 个容器的资源(4 核 CPU、8GB 内存、15GB 磁盘)
包含 Enhanced Telemetry 测试版的 Claude 套餐(Pro+ / Max)
确保 localhost 上以下端口未被占用:
3000(Grafana)3100(Loki)4317/4318(OTel Collector 的 OTLP gRPC/HTTP)9090(Prometheus)16686(Jaeger UI)
有帮助的背景知识
Docker Compose:会用
docker compose ...命令启动 compose 文件中定义的容器,并查看容器状态和日志。PromQL(Prometheus):如何使用计数器/仪表(counter/gauge)、范围选择器,以及
sum/increase/rate/by(按标签)分组。Grafana:通过 Explore 探索数据源,用 stat 和 timeseries 面板构建仪表盘。面板转换与全局变量。
LogQL(Loki):流选择器、logfmt 与 label_format。
Jaeger 与链路追踪:trace/span 模型(父子 span、span 数量、持续时间),以及 Jaeger UI 的标签搜索。
Claude Code 的执行模型:会话、子代理、技能、工具以及上下文压缩。
Claude 计费基础:token 与提示词缓存层级。
环境搭建
这套用于测试的可观测性技术栈通过 Docker Compose 部署。要让遥测数据正常导出,Claude Code 必须能访问 Collector 的 OTLP 端点——在同一台机器上运行时,该端点为 localhost:4317(gRPC)或 localhost:4318(HTTP)。所有后端服务都运行在容器中,并各自使用独立的 Docker 卷来持久化数据。
上图展示了我们所用的各个遥测组件之间是如何连接的。
除 Claude Code 外,其余组件都以容器的形式由 Docker Compose 统一管理。
运行在任意机器(本地主机或云端虚拟机)上的多个 Claude Code 实例,只要与 Collector 网络连通(即 Collector 容器所在主机的 4317/4318 端口可访问),就能向其导出遥测数据。
自上而下来看:
Claude Code 通过 OTLP 将全部三种信号导出到 Collector。
Jaeger、Prometheus 和 Loki 各自把数据持久化到专属的 Docker 卷中。
Grafana 作为统一的仪表盘层,查询这三个后端。
Collector 会按类型拆分这些数据:追踪推送给 Jaeger,日志推送给 Loki,指标则暴露在 8889 端口上,供 Prometheus 抓取。
我们的目标是让 Claude Code 的遥测数据以实时流的形式存入后端,无论会话进行中还是结束很久之后,都能按需查询。为此需要做好两件事:
在 Claude Code 中启用遥测。埋点已经内置,但只有开启遥测并把 OTLP 导出器指向 Collector 之后,数据才会真正发出。
运行可观测性后端。Collector 处理每一类信号,并将其转发给 Prometheus、Loki 和 Jaeger 存储,同时对外提供查询服务。
我们会先启动后端,这样遥测数据才有地方可去。
启动可观测性后端
在启用 Claude Code 的遥测之前,先确保整套技术栈已经就绪,能够收集、处理并读取数据。测试用可观测性后端的代码位于这个 Github 仓库。
该仓库的结构如下:
.
├── README.md
└── compose
├── docker-compose.yml # Docker config for 5 containers in the observability stack. Applies pinned image versions, port mappings and named volumes for each service.
├── grafana
│ └── provisioning
│ ├── alerting
│ ├── dashboards
│ ├── datasources # datasources(Prometheus, Loki, Jaeger) and dashboards. Empty initially.
│ └── plugins
├── jaeger-config.yaml # Jaeger v2, badger (local-file) storage for traces. Ties to the user: root TIP below.
├── loki-config.yaml # single-binary Loki, filesystem storage. Near default settings.
├── otel-collector-config.yaml # receive/process/export pipeline: OTLP in on 4317/4318, traces out to Jaeger, logs to Loki, metrics exposed on :8889 for Prometheus.
└── prometheus.yml # a single scrape job against the Collector's :8889, 30s interval.
启动这些容器只需一条 docker compose 命令。它会读取 docker-compose.yml,启动各个容器,并把它们与各自的配置文件关联起来。
容器之间的连通性:
所有容器都在同一个 Docker 网络内启动,因此可以直接用容器名称相互通信。例如,collector 的 exporter 配置里就使用了容器名称:
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:8889
otlphttp/loki:
endpoint: http://loki:3100/otlp
请注意,其中并没有 Prometheus 的配置,因为 Prometheus 最终会从采集器抓取数据,具体抓取配置写在 prometheus.yml 中:
global:
scrape_interval: 30s
scrape_configs:
- job_name: otel-collector
static_configs:
- targets: ["otel-collector:8889"]
使用 Docker compose 启动容器:
git clone https://github.com/ps-mir/otel-dev-stack.git
cd otel-dev-stack/compose
docker compose up -d
# Output
✔ Volume compose_loki_data Created 0.0s
✔ Volume compose_grafana_data Created 0.0s
✔ Volume compose_prometheus_data Created 0.0s
✔ Volume compose_jaeger_data Created 0.0s
✔ Network compose_default Created 0.1s
✔ Container compose-prometheus-1 Started 4.1s
✔ Container compose-loki-1 Started 4.2s
✔ Container compose-jaeger-1 Started 4.3s
✔ Container compose-otel-collector-1 Started 3.3s
✔ Container compose-grafana-1 Started 2.7s
查看容器状态:
# all five services should show "Up"
docker compose ps
# Output
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
compose-grafana-1 grafana/grafana:13.2.0 "/run.sh" grafana 3 minutes ago Up 3 minutes 0.0.0.0:3000->3000/tcp, [::]:3000->3000/tcp
compose-jaeger-1 cr.jaegertracing.io/jaegertracing/jaeger:2.20.0 "/go/bin/jaeger --co…" jaeger 3 minutes ago Up 3 minutes 0.0.0.0:16686->16686/tcp, [::]:16686->16686/tcp
compose-loki-1 grafana/loki:3.7.6 "/usr/bin/loki -conf…" loki 3 minutes ago Up 3 minutes 0.0.0.0:3100->3100/tcp, [::]:3100->3100/tcp
compose-otel-collector-1 otel/opentelemetry-collector-contrib:0.159.0 "/otelcol-contrib --…" otel-collector 3 minutes ago Up 3 minutes 0.0.0.0:4317-4318->4317-4318/tcp, [::]:4317-4318->4317-4318/tcp, 55679/tcp
compose-prometheus-1 prom/prometheus:v3.11.2 "/bin/prometheus --c…" prometheus 3 minutes ago Up 3 minutes 0.0.0.0:9090->9090/tcp, [::]:9090->9090/tcp
提示:Jaeger 需要以 user: root 身份运行(在 Compose 文件中指定),才能创建 badger 目录,否则会报错:mkdir /badger/key: permission denied。Jaeger 本身并不需要 root 权限,问题在于 Docker 卷首次挂载时的属主是 root:root。
启用遥测
OpenTelemetry 埋点添加到应用后,默认处于禁用状态,需要通过特定配置才能启用。
在 Claude Code 中启用遥测有两种方式:
1. 环境变量
设置特定的环境变量即可开始生成遥测数据。除了标准的 OTEL_* 变量之外,Claude Code 还定义了一套自己的 CLAUDE_CODE_* 变量。
本指南采用以下配置:
# master switch: when unset or 0, Claude Code produces no telemetry at all
export CLAUDE_CODE_ENABLE_TELEMETRY=1
# opt into the beta enhanced-telemetry attributes and events (extra session and tool detail)
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1
# per-signal exporter selection; "otlp" ships the signal over OTLP.
# other accepted values are "console" (print locally), "prometheus" (metrics only), and "none" (drop the signal)
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_TRACES_EXPORTER=otlp
# OTLP transport: "grpc" talks to the collector's 4317 port; "http/protobuf" would use 4318
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
# one endpoint for all three signals: the collector's OTLP listener on the local machine
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# how often metrics are flushed, in milliseconds; the default is 60000 (60s),
# shortened here so a manual check sees fresh data without a long wait
export OTEL_METRIC_EXPORT_INTERVAL=5000
# emit cumulative counters instead of delta (see "Aggregation Temporality" below)
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative
不过,环境变量是进程级的,影响范围不止 Claude Code。例如:
不小心在其他应用程序里也启用了埋点。
如果你正在开发自己的埋点,或参与任何 OpenTelemetry SDK 的开发,这些变量还可能干扰 OpenTelemetry 相关的代码和测试。
2. Claude Code settings.json
OpenTelemetry 定义了声明式配置——一种基于 YAML 的遥测启用方式,但 Claude Code 并不支持它。不过,你可以在 ~/.claude/settings.json 中设置同样的环境变量。这虽然算不上声明式配置,却比 shell 环境变量更稳妥,因为它只对 Claude Code 生效。示例如下:
{
"effortLevel": "medium",
"tui": "fullscreen",
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
"OTEL_METRIC_EXPORT_INTERVAL": "5000",
"OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "cumulative"
}
}
遥测相关只需要关注 env 块。effortLevel 和 tui 是另外的设置项,与遥测无关,你可能已经配置过了。这些变量与上文的注释清单一致。
聚合时间性
Prometheus 的 counter(计数器)类型指标只会随时间递增。原始数值本身没什么参考意义,通常要通过每秒增量(rate())或某个时间窗口内的总增量(increase())来读取。
聚合时间性决定了计数器每次导出遥测数据时上报什么数值:是自上次导出以来的变化量,还是自进程启动以来的累计总量。
举个简单的例子。假设 Claude Code 在四个 5 秒的导出区间内消耗 token:
| 导出时间 | 距上次导出消耗的 token 数 | 上报的增量值 | 上报的累积值 |
|---|---|---|---|
| 0 秒(启动) | -- | -- | 0 |
| 5 秒 | 100 | 100 | 100 |
| 10 秒 | 0 | 0 | 100 |
| 15 秒 | 250 | 250 | 350 |
| 20 秒 | 50 | 50 | 400 |
默认情况下,Claude Code 以 AggregationTemporality: Delta 的方式输出指标。可以通过下面的命令查看 Collector 容器的日志来验证这一点:
# Command only works from directory containing docker-compose.yml
docker compose logs otel-collector
注意:要在 Collector 中启用详细日志,需要将 debug 导出器添加到 Collector 配置中。
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/jaeger, debug]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus, debug]
然后重启容器:
# Command only works from directory containing docker-compose.yml
docker compose up -d --force-recreate otel-collector
以下是 AggregationTemporality: Delta 的日志输出:
otel-collector-1 | Descriptor:
otel-collector-1 | -> Name: claude_code.active_time.total
otel-collector-1 | -> Description: Total active time in seconds
otel-collector-1 | -> Unit: s
otel-collector-1 | -> DataType: Sum
otel-collector-1 | -> IsMonotonic: true
otel-collector-1 | -> AggregationTemporality: Delta <---
otel-collector-1 | NumberDataPoints #0
Delta 与 Prometheus 的 rate()/increase() 这类函数配合得并不好,因为这些函数期望的是累积值。
设置 OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative 后,导出指标的时序就会从 delta 切换为 cumulative。本指南所用的 env 和 JSON 配置里都已经加上了这一项。
和之前一样,配置改动之后需要重启 Collector 容器,改动才能生效。
探索遥测数据
遥测数据跑起来之后,就可以开始查询了。指标、日志和追踪各自回答的是关于 Claude Code 使用情况的不同问题,因此下面三节的内容基本相互独立。
这些内容背后的数据来自两个地方。
指标和日志两节查询的是后端中积累下来的 Claude Code 使用数据,因此面板上显示的是你自己的会话,数字自然与截图对不上。先实际跑上几个会话,面板才会有像样的内容。
追踪一节则不同,它完整演示了一次专门设计的运行——用一个自定义技能汇总一批会议,每一步都描述得足够详细,可以照着做。你不必亲手复现它。
指标
指标是从聚合、分时间窗口的视角来呈现 Claude Code 的使用情况。例如:总花费、token 用量,以及两者随时间的变化趋势,还可以按 model、effort、token type 等属性进行拆分。借助指标,你可以监控开销,并及时发现消耗模式的变化。
每个指标都是持续记录下来的数值度量,本质上是一条 时间序列——由一系列带时间戳的值组成,既可以绘图,也可以聚合。Claude Code 的指标是累计值(计数器),因此查询返回的是所选时间窗口内的变化量,而不是原始值。其原理可参见前文的 聚合时序 一节。
这里使用的指标后端是 Prometheus:它从 Collector 抓取数据、存储时间序列,并响应以 PromQL 编写的查询。Grafana 读取同一份数据,在 localhost:3000 上呈现仪表盘。完整的指标列表及其属性详见 Claude Code 监控文档。
在浏览器中打开 Prometheus(localhost:9090),在查询框里输入 claude,就能看到所有受支持的指标:

对于下面的每个指标,我们先用 PromQL 探究其含义,再用同一个查询把它作为面板添加到 Grafana 仪表盘中。
总花费(美元)
claude_code_cost_usage_USD_total 表示累计使用成本(以美元计),并按会话分别统计。这个指标可用于控制预算,也能帮你及时发现使用量突然飙升的情况。
这是客户端的估算结果:按 token 用量乘以 Anthropic 针对各模型、各类型设定的单价,再逐次累加而成。这个数值超过你的 Claude Code 套餐订阅费用完全正常。
注意:如果你按原始 API 调用付费,这个指标就尤为关键。订阅制则提供一定的用量额度,速率上限虽有所提高,但仍然有限。
先在 Prometheus 中测试以下查询(localhost:9090/query):
sum(increase(claude_code_cost_usage_USD_total[10m]))
increase(...[10m]) 返回该计数器在过去 10 分钟内的增长量。不带 by 子句的 sum(...) 会把按属性拆分出的各条序列(model、effort 等)汇总成一个数字。
在 Grafana 面板中,可以把固定的 [10m] 时间窗口换成内置变量 $__range,让数值跟随面板的时间选择器动态变化:
sum(increase(claude_code_cost_usage_USD_total[$__range]))
要把它添加为面板,请打开 Explore,选择 Prometheus 作为数据源,然后运行查询。查询结果取决于你在该时间窗口内使用了多少 Claude Code。
把它添加到仪表盘后,你会看到更多面板选项。选择 Stat 面板:
这个面板还需要添加到 Grafana 仪表盘中。
Token 总用量
claude_code_token_usage_tokens_total 记录的是累计 token 数,与美元成本指标一样,同属计数器(counter)类型。先在 Prometheus 中查看一条原始序列,弄清楚可以按哪些标签做聚合。单条序列如下:
claude_code_token_usage_tokens_total{effort="high", exported_job="claude-code", instance="otel-collector:8889", job="otel-collector", model="claude-sonnet-5", otel_scope_name="com.anthropic.claude_code", otel_scope_version="2.1.252", query_source="auxiliary", session_id="e0b9795b-4da3-4171-8fa6-a2866bf44d86", terminal_type="ssh-session", type="cacheCreation"} 213730
末尾的数字就是计数值。你主要会打交道的关键属性是 type、model 和 effort。下方的按类型划分的令牌用量图表就是按 type 分组的。
窗口总量与累计美元支出采用相同的查询结构,只是把计数器换成了令牌:
sum(increase(claude_code_token_usage_tokens_total[$__range]))
每美元可获得的 Token 数
与前两个指标不同,这项指标并非直接测量,而是一个推导值:在指定时间窗口内,用"总 Token 数 ÷ 总成本"计算得出。
sum(increase(claude_code_token_usage_tokens_total[$__range])) / sum(increase(claude_code_cost_usage_USD_total[$__range]))
这个数字将所有属性组合归并为一个值。每一种不同的组合——比如中等力度下的模型 A 与高力度下的模型 B——都是一条独立的时间序列,而查询会把所有这些序列加总在一起。
若要分析某个特定组合,可以按某个属性拆分同一比率,再进行对比。
sum by (model) (increase(claude_code_token_usage_tokens_total[$__range]))
/ sum by (model) (increase(claude_code_cost_usage_USD_total[$__range]))
把 model 换成 effort 或 type 即可;上文那张原始序列面板里列出了其余的标签。
整体结果:
统计面板(6 小时窗口):总花费($4.28)、Token 总消耗量(3.02M)、每美元 Token 数(707k)。
按类型统计 Token 用量
接下来看 claude_code_token_usage_tokens_total 随时间的变化,并按 type 拆分。单个数值指标看不出消耗形态,所以要改用时间序列面板。
type 属性有四个取值,成本差异很大:
cacheRead:命中已有缓存条目而返回的 token。长会话中,token 消耗的大头都来自这里,而且价格低于基准费率。cacheCreation:首次加载前缀时写入提示缓存的 token。价格不菲。input:未命中缓存的新提示 token。output:模型生成的 token。
如果还想同时看到总量,可以并用两条查询:一条按类型拆分,另一条不拆分、只作总量参考。
# per-type breakdown
sum by (type) (increase(claude_code_token_usage_tokens_total[$__rate_interval]))
# total
sum(increase(claude_code_token_usage_tokens_total[$__rate_interval]))
__rate_interval 是 Grafana 中时序面板按步长计算时使用的窗口,相当于上文统计面板所用的 __range。
保存为面板之前,这两个查询在 Explore 中的运行效果。
添加到仪表板之后:
每个尖峰都是一次 Claude Code 的密集使用,平缓的部分则是空闲时段。悬停在任意一个点上,就能把总量拆成四种类型:图中总量约为 100 万 token,其中 cacheRead 约 925k(约 92%),其余是 cacheCreation、output 和 input。
提示:token 消耗主要来自 cacheRead,而这种类型恰恰也是最便宜的。
按模型和推理力度统计 Token 用量
这是 “Tokens Per USD” 一节中提到的细分思路的具体落地:看看究竟哪些 model 与 effort 组合在真正消耗 token。
sum by (model, effort) (
increase(claude_code_token_usage_tokens_total[$__rate_interval])
)
这里的 token 计数器按 model 和 effort 两个属性分组。在这个时间窗口内,每条序列都是 claude-sonnet-5,推理力度为 medium 或 high;18:13 附近有一波 medium 力度的用量突发,峰值约达 260 万 token。分组组合的多少,取决于所选属性的基数(cardinality)。
日志
日志记录是一条带时间戳的事件,并附带完整的字段集。当你想还原某个具体事件及其上下文——发生了什么、何时发生、各字段取值是什么——就应该去查日志。
指标则是同一活动的预聚合形态。凡是涉及跨大量记录做计数、求和或取百分位的数据,都应归入指标。如果你在下游还要对日志输出做聚合,那这份数据从一开始就该是指标。
日志适合用在以下场景:
单次事件的上下文:某一次发生的完整细节,而不是汇总后的数字。
离散或不规则的事件:上下文压缩被触发、会话启动,或某个 API 报错。
事后取证分析:出了问题再排查调试时,回读原始记录。
trace 关联:一行带有 trace ID 和 span ID 的日志,能让你直接定位到它所属的那次请求。
这里使用的日志后端是 Loki,并通过 LogQL 进行查询。把日志接入这样的后端,能带来:
结构化字段:基于命名字段做过滤和计算,而不必对纯文本跑正则。
字段索引:按标签查询可以直接命中返回,无需逐行扫描。
trace 与 span 关联:从一条日志跳转到对应的 trace,或拉取某条 trace 下的全部日志。
限定时间范围:每次查询都只覆盖一个时间窗口,扫描成本因此保持在低位。
Grafana 像读取 Prometheus 一样,从 Loki 读取数据来构建仪表盘。
压缩事件
压缩(compaction)是指 Claude Code 在自身上下文变得过大时对其进行裁剪。每次压缩都会发出一条日志事件(event_name="compaction"),其中携带压缩前后的 token 计数(pre_tokens、post_tokens),以及事件发生时所在的 span_id。对这类事件做查询,就能看出压缩触发的频率,以及每次能回收多少 token。
在 Grafana Explore 中,选择 Loki 作为数据源,然后粘贴下面的 LogQL。这段查询会筛选出压缩事件,用 logfmt 解析其中的字段,再通过 label_format 为每次事件计算出缩减百分比。这些字段只有在压缩真正发生后才会存在,所以请先手动触发几次压缩。
{service_name="claude-code"} | event_name="compaction"
| logfmt
| label_format reduction_pct=`{{ printf "%.1f" (mulf (divf (subf .pre_tokens .post_tokens) .pre_tokens) 100) }}`
在这里为每条记录计算 reduction_pct 是可以的,因为它始终停留在单次事件层面。而跨多次压缩的滑动平均,就应该放进指标里了。
label_format 这一行会添加一个 reduction_pct 标签。要以表格形式展示,需将面板切换为 Table 视图,并添加三个 Grafana 转换:
从 labels 对象中提取字段。
按名称筛选字段,只保留 Time、pre_tokens、post_tokens、reduction_pct 和 span_id。
转换字段类型,把 pre_tokens、post_tokens 和 reduction_pct 转为数字。
链路追踪
链路追踪能详细呈现一个请求在应用中从开始到结束所经过的完整路径。先了解几个链路追踪的基本概念:
Span:带时间信息的一次操作,代表一个工作单元,是构建链路的基本构件。所有追踪数据都以一串 span 的形式记录,每个 span 都有一个类型,也就是它的操作名称:
claude_code.interaction:一次提示词,以及 Claude Code 为回答它所做的全部工作。通常作为根 span,因此一次交互实际上就对应一条链路。claude_code.llm_request:一次交互中的单次模型调用。claude_code.tool:一次交互中的单次工具调用(Bash、Write、Agent等)。
Trace:由 span 组成的树,表示一个请求从开始到完成的路径。
Session:一次 Claude Code 运行,由
session.id标识。一次会话可能产生多个交互和多条链路。Subagent:由
Agent工具启动的嵌套 Claude Code 实例,会运行自己的交互。
这里使用的链路追踪后端是 Jaeger。Collector 通过 OTLP 把 span 转发给它,由 Jaeger 负责存储,并支持按服务和 span 标签搜索链路,还能以 span 树的形式查看每条链路的细节。下文的所有操作都基于它位于 localhost:16686 的 UI。
生成链路
为了生成追踪数据,本指南将使用一个测试提示词来启动代理并准备一些文本。该提示词已在 Sonnet 5 上以中等推理力度完成测试。
你可以直接将这段提示词粘贴到 Claude Code 中:
Spawn 4 subagents in parallel, one per topic below. Each subagent researches its topic from your own knowledge and returns a ~150-word summary with 3 key points. Do not have them read files or run commands.
Topics:
1. How TCP congestion control works
2. The CAP theorem
3. How DNS resolution works
4. What a Bloom filter is
Once all 4 return, combine the summaries into one markdown document and write it to summary.md
注意:提示词执行完毕后,需要在同一个会话中向 Claude Code 询问 session_id,后续要用它在 Jaeger 中查找相关的追踪记录。
按 Session ID 查找追踪
搜索条件是 service = claude-code 加上标签 session.id=。结果返回了 6 条追踪,根节点全部是 claude_code.interaction,span 数量从 1 到 20 不等,耗时则从约 1 秒到 33 秒不等。
单看这份列表,分不清每条追踪各自做了什么。手动逐一查看,或者针对真实会话调用 trace API 写脚本分析,结果如下:
| # | 追踪名称 | Span 数 | 耗时 | llm_calls | tools |
|---|---|---|---|---|---|
| 1 | claude_code.interaction | 1 | 1.4s | 0 | – |
| 2 | claude_code.interaction | 3 | 4.6s | 2 | – |
| 3 | claude_code.interaction | 1 | 5.4s | 0 | – |
| 4 | claude_code.interaction | 1 | 2.5s | 0 | – |
| 5 | claude_code.interaction | 20 | 15.7s | 7 | Agent(x4) |
| 6 | claude_code.interaction | 15 | 32.5s | 5 | ScheduleWakeup(x2), Write(x1) |
几点观察:
一半的追踪都是噪音。追踪 1、3、4 是只有一个 span 的交互,既没有调用模型,也没有使用工具,属于空闲会话收到的心跳请求。追踪 2 是一次简短对话。只有追踪 5 和 6 才是真正的运行记录。
并行派发体现在同一次交互里。追踪 5 在一个
claude_code.interaction中同时发起了全部四个Agent调用。它们的嵌套模型调用(各耗时 7 到 10 秒)彼此重叠,因此整个交互只用了大约 16 秒就完成了——尽管四个子代理的 LLM 时间加起来约有 35 秒。每个子代理的模型调用都嵌套在各自的
Agentspan 之下,并带有agent_id,据此可以区分这四个调用。agent_id是不透明的,上面没有agent.name或skill.name之类的信息。追踪只能告诉你四个子代理运行了、各自花了多久,却看不出每个子代理分到的是什么主题。Span 只记录 token 数量,不含美元成本。每个
claude_code.llm_request都带有input_tokens、output_tokens、cache_read_tokens和cache_creation_tokens,但没有任何美元金额。写入是另一次独立的后续交互。Trace 6 中没有
Agentspan:一次耗时约 23 秒的claude_code.llm_request生成合并后的 markdown,随后是一次简短的Write。两个ScheduleWakeupspan 属于后台协调。
提示:从 Trace 中可以看到四次子代理调用,每次都带有 agent_id、token 数量和耗时,但没有任何 span 说明子代理被分配了哪个主题。相比之下,Metrics 可以按 model、effort 和技能进行归因。
注意:交互 span 中的 user_prompt 默认会被脱敏。设置 OTEL_LOG_USER_PROMPTS=1 可以关闭脱敏,记录原始提示词文本。在多用户/多租户环境中应避免开启,因为它会把提示词内容暴露给所有能访问遥测后端的人。
结语
本指南对 Claude Code 的可观测性做了一次端到端的完整演练:开启遥测,把三种信号逐一采集到本地后端进行分析。
Metrics 让你能够按属性拆解选定时间段内的累计成本和用量数据。这在共享或多租户环境中最为关键——成本并不归属于单一负责人,但总得有人把它算清楚。
Logs 是单个事件的记录,适合深入查看某次事件中究竟发生了什么变化,比如压缩。
Traces 展示了一条提示词如何展开成子代理调用和模型调用,每次调用都带有耗时和 token 数量。这是调试或精简复杂提示词、多代理提示词的起点,不过 span 目前尚未记录每次调用究竟由哪条提示词或哪个技能触发。
其中部分遥测功能仍处于 Enhanced Telemetry 测试阶段,因此 span 名称和属性还可能变动,逐调用归因之类的空白也可能随着功能成熟而补齐。建议随着这套功能逐步定型,回头再查阅监控文档。