← 返回
IT技术

使用 OpenTelemetry 实现 Claude Code 可观测性

✍️ zhirenhun 📅 2026/9/6 👁 132 阅读 ⏱ 76 分钟
使用 OpenTelemetry 实现 Claude Code 可观测性

Claude CodeOpenAI CodexGoogle AntigravityCursor 这样的智能体编程工具,如今已成为日常软件开发的标配。

随着智能体系统日趋成熟,开发者交给它们的大部分工作都是层层委派完成的,一次交给一个 subagent。许多团队也在探索并采用共享式、多租户的智能体基础设施,这类基础设施的成本并不由某个单一负责人承担。正因如此,可观测性成为监控基础设施成本的关键。

在本指南中,你将先了解可观测性的工作原理,随后开启 Claude Code 内置的遥测功能,运行一个后端来收集这些数据,并解读它输出的指标、日志和追踪信息。这能帮你更有效地追踪团队成本;而且随着遥测数据日渐成熟、与会话的关联愈发清晰,效果只会越来越好。

注意:就目前而言,Claude Code 输出的遥测数据不包含任何属性,无法可靠地映射到具体的命名会话。虽然可以借助 session_id 追踪用量,但在一个混杂了多个提示/技能的长会话中,这种方式仍显得相当笨拙。

本指南的范围仅限于 Claude Code 在指标、日志与追踪方面的遥测功能。请注意,其内容仅适用于 Linux 和 macOS。

目录

使用 OpenTelemetry 实现可观测性

可观测性指的是这样一种能力:仅凭系统输出的数据,就能回答有关其运行时行为的问题。要做到这一点,你无需窥探系统内部,不必附加调试器、阅读源代码,也不用动手复现相关行为。

这里的“运行时行为”,指的是从外部可以看到的表现。你可以提出类似这样的问题:

对 Claude Code 而言,外界无从得知的内部机制包括:它如何管理上下文、如何把工作拆分到多次 LLM 调用中,以及如何编排各个子代理(subagent)。不过,你可以通过读取 Claude Code 发出的遥测数据来回答这类问题:

只有经过插桩的系统才能回答这些问题。所谓插桩(instrumentation),是指由开发者手动添加或内置于工具中的一段代码,用来记录程序的运行时行为,并将其以遥测数据的形式输出。比如像 this request spent 100 tokens 这样的一条度量记录。

遥测数据能提供一条结构清晰的数据轨迹,完整记录系统随时间变化的行为,从而帮助避免静默故障。例如,下面这张图出自 GitHub 2026 年 8 月 17 日故障的复盘文章,展示了 GitHub Actions 的运行量如何随时间从约 3000 万增长到约 1.1 亿:

GitHub Actions 增长曲线

遥测数据

发出的遥测数据分为三类:

OpenTelemetry(https://opentelemetry.io/)是一个可观测性框架,帮助你生成并采集这些信号,再导出到后端,由后端负责存储、查询和可视化。这种职责分离让方案不绑定任何特定工具或厂商——后端既可以是开源方案,也可以是闭源产品。它为多种编程语言提供了插桩 SDK。

为 Claude Code 插桩

插桩代码通常随应用一起运行,部署在最适合测量的位置:比如请求进入或响应发出的中间件,或是统计 token 数量的环节。

插桩接入应用的方式有两种:

  1. 共享插桩库:使用开源框架的应用可以把插桩库作为依赖引入,并将其与应用的生命周期方法关联起来。OpenTelemetry 为众多框架发布了现成的插桩库(例如 Spring Framework)。

  2. 由应用开发者定制实现:借助 OpenTelemetry SDK,直接在代码库中添加遥测数据的发射点。闭源产品虽然代码私有,但同样能发出符合 OpenTelemetry 标准的遥测数据。

示例:HTTP 插桩

HTTP 处理中的中间件是所有请求的必经之路,认证这类全局配置之所以放在这里,正是这个原因。同样的道理,中间件也是放置插桩代码的理想位置:只需包装一次处理器,就能测量每一个请求。

HTTP 插桩示例

上图展示了插桩相对于请求路径所处的位置。

Claude Code 属于第二种情况:核心应用及其插桩模块都由 Anthropic 提供。统计 token 用量、成本和工具调用的代码已经内置其中,并通过 OTLP 发送 OpenTelemetry 遥测数据。作为 Claude Code 用户,你只需启用遥测功能,再准备好一个后端来接收并分析这些数据即可。

注意OTLP 是 OpenTelemetry 项目设计的一种遥测数据传输协议。本指南假定整条链路均与 OTLP 兼容。如果接入不兼容的遥测数据,或使用不兼容的后端组件,可能出现不可预知的结果,不在本文讨论范围之内。

要采集、存储并查看遥测数据,你需要以下组件:

拉取与推送:遥测数据如何离开应用

遥测数据离开应用的方式不外乎两种:

拉取(抓取):应用把当前指标暴露在一个 HTTP 端点上,抓取器(Prometheus)定期读取该端点。每个运行中的实例都需要独占一个端口,而且抓取器必须事先知道所有这些地址。应用处于被动地位:数据流动由抓取器驱动。这种方式适合地址固定、长期运行的进程。

在 OpenTelemetry 中,基于拉取的抓取方式通过 OTEL_METRICS_EXPORTER=prometheus 来配置(导出器的可选取值参见 SDK 环境变量文档)。

按照惯例,应用会在 http://localhost:9464/metrics 上提供指标数据。该导出器仅处理指标。

推送(OTLP):应用按固定间隔把遥测数据发送到接收端点。这种方式更灵活:任意数量的进程都可以向同一个端点推送数据,无需事先注册,因此应用可以随意启停,地址变了也不受影响。

在 OpenTelemetry 中,推送通过 OTEL_METRICS_EXPORTER=otlp 来配置,数据经由 OTLP 导出器发出。

推送模式可以承载指标、日志和链路三类数据。

什么时候需要部署 Collector

当现有技术栈已无法应对系统日益增长的规模和复杂度时,就需要部署 Collector 了。它能带来以下帮助:

注意:尽管本文面向的是单用户场景,本指南仍采用带 Collector 的推送式配置。这套技术栈包含三个后端,它们存储和查询数据的方式各不相同;让 Collector 一次性接收 Claude Code 的 OTLP 数据,再把每类信号路由到合适的位置,比让应用逐一对接三个后端要简单得多。这正是工具清单把 Collector 标注为“个人使用可选、生产环境必备”的原因:生产者和后端越多,它的价值就越大。

前置条件

本指南的每一节都附有相关文档链接,不过如果你已经熟悉下面列出的工具和查询语言,上手会快得多:

你需要准备

有帮助的背景知识

环境搭建

这套用于测试的可观测性技术栈通过 Docker Compose 部署。要让遥测数据正常导出,Claude Code 必须能访问 Collector 的 OTLP 端点——在同一台机器上运行时,该端点为 localhost:4317(gRPC)或 localhost:4318(HTTP)。所有后端服务都运行在容器中,并各自使用独立的 Docker 卷来持久化数据。

插桩环境搭建

上图展示了我们所用的各个遥测组件之间是如何连接的。

自上而下来看:

我们的目标是让 Claude Code 的遥测数据以实时流的形式存入后端,无论会话进行中还是结束很久之后,都能按需查询。为此需要做好两件事:

我们会先启动后端,这样遥测数据才有地方可去。

启动可观测性后端

在启用 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。例如:

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 块。effortLeveltui 是另外的设置项,与遥测无关,你可能已经配置过了。这些变量与上文的注释清单一致。

聚合时间性

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 的使用情况。例如:总花费、token 用量,以及两者随时间的变化趋势,还可以按 modeleffort、token type 等属性进行拆分。借助指标,你可以监控开销,并及时发现消耗模式的变化。

每个指标都是持续记录下来的数值度量,本质上是一条 时间序列——由一系列带时间戳的值组成,既可以绘图,也可以聚合。Claude Code 的指标是累计值(计数器),因此查询返回的是所选时间窗口内的变化量,而不是原始值。其原理可参见前文的 聚合时序 一节。

这里使用的指标后端是 Prometheus:它从 Collector 抓取数据、存储时间序列,并响应以 PromQL 编写的查询。Grafana 读取同一份数据,在 localhost:3000 上呈现仪表盘。完整的指标列表及其属性详见 Claude Code 监控文档

在浏览器中打开 Prometheus(localhost:9090),在查询框里输入 claude,就能看到所有受支持的指标:

Prometheus 中可用的 Claude Code 计数器类型指标。

对于下面的每个指标,我们先用 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(...) 会把按属性拆分出的各条序列(modeleffort 等)汇总成一个数字。

在 Grafana 面板中,可以把固定的 [10m] 时间窗口换成内置变量 $__range,让数值跟随面板的时间选择器动态变化:

sum(increase(claude_code_cost_usage_USD_total[$__range]))

要把它添加为面板,请打开 Explore,选择 Prometheus 作为数据源,然后运行查询。查询结果取决于你在该时间窗口内使用了多少 Claude Code。

Grafana USD 总额 Stat——查询在 Metrics explorer 中的视图。

把它添加到仪表盘后,你会看到更多面板选项。选择 Stat 面板:

USD 总额 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

末尾的数字就是计数值。你主要会打交道的关键属性是 typemodeleffort。下方的按类型划分的令牌用量图表就是按 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 换成 efforttype 即可;上文那张原始序列面板里列出了其余的标签。

整体结果:

统计面板的聚合指标

统计面板(6 小时窗口):总花费($4.28)、Token 总消耗量(3.02M)、每美元 Token 数(707k)。

按类型统计 Token 用量

接下来看 claude_code_token_usage_tokens_total 随时间的变化,并按 type 拆分。单个数值指标看不出消耗形态,所以要改用时间序列面板。

type 属性有四个取值,成本差异很大:

如果还想同时看到总量,可以并用两条查询:一条按类型拆分,另一条不拆分、只作总量参考。

# 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

Grafana Explore 中的时序查询

保存为面板之前,这两个查询在 Explore 中的运行效果。

添加到仪表板之后:

按类型统计的 token 消耗走势

每个尖峰都是一次 Claude Code 的密集使用,平缓的部分则是空闲时段。悬停在任意一个点上,就能把总量拆成四种类型:图中总量约为 100 万 token,其中 cacheRead 约 925k(约 92%),其余是 cacheCreation、output 和 input。

提示:token 消耗主要来自 cacheRead,而这种类型恰恰也是最便宜的。

按模型和推理力度统计 Token 用量

这是 “Tokens Per USD” 一节中提到的细分思路的具体落地:看看究竟哪些 modeleffort 组合在真正消耗 token。

sum by (model, effort) (
  increase(claude_code_token_usage_tokens_total[$__rate_interval])
)
按模型与推理力度分组的实时 token 用量

这里的 token 计数器按 modeleffort 两个属性分组。在这个时间窗口内,每条序列都是 claude-sonnet-5,推理力度为 mediumhigh;18:13 附近有一波 medium 力度的用量突发,峰值约达 260 万 token。分组组合的多少,取决于所选属性的基数(cardinality)。

日志

日志记录是一条带时间戳的事件,并附带完整的字段集。当你想还原某个具体事件及其上下文——发生了什么、何时发生、各字段取值是什么——就应该去查日志。

指标则是同一活动的预聚合形态。凡是涉及跨大量记录做计数、求和或取百分位的数据,都应归入指标。如果你在下游还要对日志输出做聚合,那这份数据从一开始就该是指标。

日志适合用在以下场景:

  1. 单次事件的上下文:某一次发生的完整细节,而不是汇总后的数字。

  2. 离散或不规则的事件:上下文压缩被触发、会话启动,或某个 API 报错。

  3. 事后取证分析:出了问题再排查调试时,回读原始记录。

  4. trace 关联:一行带有 trace ID 和 span ID 的日志,能让你直接定位到它所属的那次请求。

这里使用的日志后端是 Loki,并通过 LogQL 进行查询。把日志接入这样的后端,能带来:

  1. 结构化字段:基于命名字段做过滤和计算,而不必对纯文本跑正则。

  2. 字段索引:按标签查询可以直接命中返回,无需逐行扫描。

  3. trace 与 span 关联:从一条日志跳转到对应的 trace,或拉取某条 trace 下的全部日志。

  4. 限定时间范围:每次查询都只覆盖一个时间窗口,扫描成本因此保持在低位。

Grafana 像读取 Prometheus 一样,从 Loki 读取数据来构建仪表盘。

压缩事件

压缩(compaction)是指 Claude Code 在自身上下文变得过大时对其进行裁剪。每次压缩都会发出一条日志事件(event_name="compaction"),其中携带压缩前后的 token 计数(pre_tokenspost_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 是可以的,因为它始终停留在单次事件层面。而跨多次压缩的滑动平均,就应该放进指标里了。

针对 Loki 数据源的压缩事件 LogQL 查询。

label_format 这一行会添加一个 reduction_pct 标签。要以表格形式展示,需将面板切换为 Table 视图,并添加三个 Grafana 转换:

  1. 从 labels 对象中提取字段。

  2. 按名称筛选字段,只保留 Time、pre_tokens、post_tokens、reduction_pct 和 span_id。

  3. 转换字段类型,把 pre_tokens、post_tokens 和 reduction_pct 转为数字。

压缩事件及其压缩前后的 token 数量与计算出的压缩百分比。

链路追踪

链路追踪能详细呈现一个请求在应用中从开始到结束所经过的完整路径。先了解几个链路追踪的基本概念:

这里使用的链路追踪后端是 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 查找追踪

按 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)

几点观察:

提示:从 Trace 中可以看到四次子代理调用,每次都带有 agent_id、token 数量和耗时,但没有任何 span 说明子代理被分配了哪个主题。相比之下,Metrics 可以按 modeleffort 和技能进行归因。

注意:交互 span 中的 user_prompt 默认会被脱敏。设置 OTEL_LOG_USER_PROMPTS=1 可以关闭脱敏,记录原始提示词文本。在多用户/多租户环境中应避免开启,因为它会把提示词内容暴露给所有能访问遥测后端的人。

结语

本指南对 Claude Code 的可观测性做了一次端到端的完整演练:开启遥测,把三种信号逐一采集到本地后端进行分析。

Metrics 让你能够按属性拆解选定时间段内的累计成本和用量数据。这在共享或多租户环境中最为关键——成本并不归属于单一负责人,但总得有人把它算清楚。

Logs 是单个事件的记录,适合深入查看某次事件中究竟发生了什么变化,比如压缩。

Traces 展示了一条提示词如何展开成子代理调用和模型调用,每次调用都带有耗时和 token 数量。这是调试或精简复杂提示词、多代理提示词的起点,不过 span 目前尚未记录每次调用究竟由哪条提示词或哪个技能触发。

其中部分遥测功能仍处于 Enhanced Telemetry 测试阶段,因此 span 名称和属性还可能变动,逐调用归因之类的空白也可能随着功能成熟而补齐。建议随着这套功能逐步定型,回头再查阅监控文档

参考资料

——

🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。