Headroom Proxy 指标技术指南:Prometheus 与 OpenTelemetry 双端点的指标体系、PromQL 实践与 Dashboard 避坑
本文围绕 Headroom 代理的官方指标指南 docs/metrics-technical-guide.md 展开,系统讲解 GET /metrics(Prometheus)与 OTLP/HTTP(OpenTelemetry)两套监控端点的定位差异,覆盖节省量、延迟、缓存、流量健康度、订阅窗口、节省归因和压缩内部六大面板的完整指标表与可直接复制的 PromQL;并结合 headroom/proxy/prometheus_metrics.py 与 headroom/observability/metrics.py 的源码,说明每个指标在代码中的产生位置与口径差异。读完后你能独立完成:为 Headroom 代理搭建 Prometheus/Grafana 面板、配置多租户 OTel 导出,并规避五类最常见的 Dashboard 失真问题。
两套端点:先分清 Prometheus 与 OTel 的分工
Headroom 代理(默认监听 :8787)提供两个观测面:
| 端点 | 获取方式 | 适用场景 |
|---|---|---|
Prometheus — GET /metrics |
始终开启,无需任何配置 | 下面所有指标都从这里开始 |
| OpenTelemetry — OTLP/HTTP | HEADROOM_OTEL_METRICS_ENABLED=1 + pip install "headroom-ai[proxy,otel]" |
同样的数据,但指标名为点分风格,并额外支持按租户维度的标签 |
两套端点的关键差异:
- 命名不同:Prometheus 使用
headroom_tokens_saved_total,且时间类指标以毫秒为单位;OTel 使用headroom.proxy.tokens.saved,时间以秒为单位。下文各面板均同时列出两套名称。 - 覆盖口径不同:以"节省 token"这一核心指标为例,Prometheus 侧的
headroom_tokens_saved_total只统计压缩,不含工具 schema 延迟加载;而 OTel 的headroom.proxy.tokens.saved两者都包含。源码可以直接印证这一点:在 headroom/observability/metrics.py 的record_proxy_request()中,self._proxy_saved_tokens.add(compression_saved + tool_schema_saved, attrs)把压缩节省与工具 schema 节省合并记入headroom.proxy.tokens.saved;而 Prometheus 侧在 headroom/proxy/prometheus_metrics.py 中明确注释tokens_saved_total是 message compression only,工具 schema 节省单独存放在tool_search_saved_total,以避免"工具字节从不改变 tok_before/tok_after"造成的口径混淆。
/metrics 路由本身实现得非常薄,见 headroom/proxy/server.py:直接调用 proxy.metrics.export() 并返回 text/plain; version=0.0.4,导出格式由 headroom/proxy/prometheus_metrics.py 的 export() 方法拼装。OTel 的运行时状态则挂在 /stats 端点上,"otel": get_otel_metrics_status()(headroom/proxy/server.py),这也解释了文末"用 curl -s localhost:8787/stats | jq .otel 验证"的做法。
节省量面板:从 hero 数字开始
headroom.proxy.tokens.saved(OTel)是头条数字。它已经合并了压缩 + 工具 schema 延迟加载两部分——不需要再叠加任何其他项。
| 指标 | 含义 |
|---|---|
headroom.proxy.tokens.saved(OTel) |
Headroom 拦在请求之外的输入 token 总量。压缩 + 工具节省,合并后的 hero 数字 |
headroom.proxy.savings.usd{source}(OTel) |
节省金额(美元),按层拆分:compression、tool_schema、output_shaping、provider_cache。求和得总额 |
headroom_persistent_savings_tokens_saved_total |
与上面相同的 tokens-saved 口径,但在代理重启后仍然保留。用于"累计节省"类磁贴 |
headroom_persistent_savings_compression_savings_usd_total |
累计节省金额,跨重启持久化 |
headroom_tokens_input_total |
实际发往上游的输入 token(压缩后)。是计算压缩率的分子/分母之一 |
headroom_tokens_output_total |
Provider 返回的输出 token |
配套 PromQL:
# Hero 磁贴:每秒节省的 token 数
rate(headroom_tokens_saved_total[5m])
+ sum(rate(headroom_savings_attributed_tokens_total{source="tool_search",realized="true"}[5m]))
# 上下文缩减百分比
100 * rate(headroom_tokens_saved_total[5m])
/ clamp_min(rate(headroom_tokens_input_total[5m]) + rate(headroom_tokens_saved_total[5m]), 1)
# 累计磁贴(跨重启保留)
headroom_persistent_savings_tokens_saved_total
headroom_persistent_savings_compression_savings_usd_total
Prometheus 侧的一个陷阱:
headroom_tokens_saved_total只包含压缩,漏掉了工具 schema 延迟加载。OTel 的headroom.proxy.tokens.saved两者都算。所以上面的查询把tool_search那一项加回来了。在工具密集的负载上,这个缺口会相当大。
源码层面,headroom_persistent_savings_* 一组磁贴并非内存累加,而是从持久化的 SavingsTracker 快照中取 lifetime 段读出(headroom/proxy/prometheus_metrics.py),这正是它们能跨重启存活的原因;同一机制也决定了它们依赖持久卷——若 HEADROOM_WORKSPACE_DIR 未落在持久卷上,部署即清零(见"五个会搞坏 Dashboard 的问题"第 1 条)。
延迟面板:均值可用,分位数缺失
Prometheus 侧所有耗时指标均以毫秒为单位,并暴露为 _sum / _count / _min / _max 四件套,均值用 rate(sum)/rate(count) 计算。这与源码结构一一对应:headroom/proxy/prometheus_metrics.py 中 latency_*、overhead_*、ttfb_* 各自维护 sum_ms / min_ms / max_ms / count 四个字段。
| 指标 | 含义 |
|---|---|
headroom_overhead_ms_* |
Headroom 自身增加的耗时:handler 入口 → 压缩结束,不含 LLM 调用。这就是"这个东西本身花了我们多少时间"的数字 |
headroom_latency_ms_* |
请求总时长,包含 provider 往返 |
headroom_ttfb_ms_* |
上游首字节时间(TTFB)。仅流式请求 |
headroom_stage_timing_ms_*{path,stage} |
handler 内部时间去向——compression_first_stage、upstream_connect、memory_context 等 |
headroom_transform_timing_ms_*{transform} |
单个压缩 transform 的耗时。用于定位慢 transform |
# Headroom 附加开销(均值,ms)
rate(headroom_overhead_ms_sum[5m]) / rate(headroom_overhead_ms_count[5m])
# 端到端耗时(均值,ms)
rate(headroom_latency_ms_sum[5m]) / rate(headroom_latency_ms_count[5m])
# 最慢的阶段 top5
topk(5, rate(headroom_stage_timing_ms_sum[5m]) / rate(headroom_stage_timing_ms_count[5m]))
没有分位数可用。
/metrics上没有直方图桶;OTel 侧的直方图虽然存在(如 headroom/observability/metrics.py 中headroom.proxy.request.duration、headroom.proxy.overhead.duration、headroom.proxy.ttfb.duration三个 histogram),但使用的是默认桶,所有请求都落进同一个桶,histogram_quantile()会返回无意义值。均值是可靠的。如果今天就要真实的 p95/p99,用headroom perfCLI。另外:每个
_sum必须除以它自己的_count。overhead 和 TTFB 只在大于 0 时才采样记录——源码中是if overhead_ms > 0/if ttfb_ms > 0才record(headroom/observability/metrics.py),因此它们的 count 天然小于 latency 的 count。
stage 级指标的双标签设计也有源码依据:stage_timing_sum 以 (path, stage) 元组为键,目的是让同一个指标名区分例如 openai_responses_ws 的 upstream_connect 与 anthropic_messages 的 upstream_connect(headroom/proxy/prometheus_metrics.py)。
缓存面板:分清命中、写放大与 cache bust
| 指标 | 含义 |
|---|---|
headroom_provider_cache_hit_requests_total{provider} |
读取了 provider prompt cache 的请求数 |
headroom_provider_cache_requests_total{provider} |
存在任意缓存活动的请求数。这才是命中率的分母 |
headroom_cache_read_tokens_total{provider} |
从缓存读出的 token(享折扣的那部分) |
headroom_cache_write_tokens_total{provider} |
写入缓存的 token(这部分要付溢价) |
headroom_cache_write_ttl_tokens_total{provider,ttl} |
按 TTL 拆分的缓存写入——5m 对比 1h |
headroom_uncached_input_tokens_total{provider} |
完全未命中缓存的输入 token |
headroom_cache_bust_total |
因压缩破坏已缓存前缀的请求数。应当长期接近零 |
headroom_cache_miss_attribution_total{provider,reason} |
缓存前缀未命中的原因——ttl_expiry、prefix_change、unknown |
# 按 provider 的缓存命中率
sum by (provider) (rate(headroom_provider_cache_hit_requests_total[5m]))
/ sum by (provider) (rate(headroom_provider_cache_requests_total[5m]))
# 压缩正在破坏缓存——这个数字升高就该告警
rate(headroom_cache_bust_total[5m])
不要用
headroom_requests_cached_total当命中率。它把 provider 的 prompt cache 和 Headroom 自己的响应 cache 混进同一个布尔值,两边都不代表。
源码中缓存统计按 provider 分桶维护,每个 provider 默认一个含 cache_read_tokens、cache_write_tokens、cache_write_5m/1h_tokens、hit_requests(即 cache_read > 0 的请求)、bust_count 等键的结构(headroom/proxy/prometheus_metrics.py);注释里同时给出了各 provider 的缓存经济学差异——Anthropic cache_read=0.1x / cache_write=1.25x,OpenAI cache_read=0.5x 且无写入惩罚,Google 约 0.1x 且有存储费,Bedrock 无缓存指标。miss 归因的 reason 值来自 prefix_tracker 的 MISS_* 常量,用于区分"空闲超过缓存生命周期(考虑换更长 TTL)"和"可缓存前缀内容变了"(headroom/proxy/prometheus_metrics.py)。
流量与健康面板
| 指标 | 含义 |
|---|---|
headroom_requests_total |
处理的请求数。无标签 |
headroom_requests_by_provider{provider} |
按 provider 的流量分布——anthropic、openai、gemini、bedrock… |
headroom_requests_by_model{model} |
按模型的流量分布。不同 model 上限 1024 个,超出部分归入 model="other" |
headroom_requests_failed_total |
上游 5xx 错误 |
headroom_requests_rate_limited_total |
被 Headroom 自身限流器拒绝的请求(不是上游 429) |
headroom_compression_failed_total{reason} |
压缩失败——timeout 或 error。因失败开放(fail-open),流量继续走但节省悄悄停了。值得配告警 |
headroom_compression_quarantine_total{event} |
连续超时后压缩被隔离禁用——activated、skipped、released |
headroom_inbound_requests_active |
在途请求数,gauge。统计所有 HTTP 请求,包括 /metrics 本身 |
headroom_active_ws_sessions |
存活的 Codex WebSocket 会话数,gauge |
# 失败率
rate(headroom_requests_failed_total[5m])
/ clamp_min(rate(headroom_requests_total[5m]) + rate(headroom_requests_failed_total[5m]), 1)
# 节省悄悄停摆(按原因分组)
sum by (reason) (rate(headroom_compression_failed_total[5m]))
# 流量构成
sum by (provider) (rate(headroom_requests_by_provider[5m]))
两个细节有源码背书:model 标签的基数上限来自 headroom/telemetry/context.py 的 MAX_DISTINCT_MODELS = 1024,record_request 首次触顶时折叠为 model="other" 且只告警一次(headroom/proxy/prometheus_metrics.py);压缩失败计数器特意区分 timeout 与 error,注释说明这样能判断"是压缩预算太紧还是真 bug"(headroom/proxy/prometheus_metrics.py)。
Anthropic 订阅面板
仅在 Anthropic OAuth/订阅账号下有效。OTel 独有,gauge,无标签。
| 指标 | 含义 |
|---|---|
headroom.subscription.5h_utilization_pct |
5 小时限流窗口已用比例(0–100) |
headroom.subscription.7d_utilization_pct |
7 天窗口的同一比例 |
headroom.subscription.5h_seconds_to_reset |
距 5 小时窗口重置的秒数 |
headroom.subscription.7d_seconds_to_reset |
距 7 天窗口重置的秒数 |
headroom.subscription.overage_usd |
已消耗的超额用量额度(美元) |
这五个 gauge 在 OTel 侧实现为 create_observable_gauge + 回调(headroom/observability/metrics.py),由 record_subscription_window() 从订阅追踪器的 state 字典回填数值,只在 extra_usage 开启时更新 overage_usd(headroom/observability/metrics.py)。
归因面板:节省到底来自哪里
| 指标 | 含义 |
|---|---|
headroom_savings_attributed_tokens_total{source,realized} |
按命名来源拆分的节省 token。source="tool_search" 即工具 schema 延迟加载 |
headroom_savings_attributed_usd_total{source,realized} |
按来源拆分的节省金额。gauge,可能为负——不要对它 rate() |
headroom_savings_attribution_events_total{source,realized} |
每个来源贡献了多少次 |
headroom_waste_signal_tokens_total{signal} |
在输入中检测到的浪费模式——json_bloat、base64、repetition、reread 等。这是诊断信号,不是节省量 |
这些行是用来"解释"头条数字的,永远不要把它们加到总节省上。Prometheus 导出端对归因行也刻意做了类型区分:headroom_savings_attributed_usd_total 被声明为 # TYPE ... gauge 并标注 "may be negative"(headroom/proxy/prometheus_metrics.py);OTel 侧对应的是 create_up_down_counter(headroom/observability/metrics.py)。
压缩内部指标
| 指标 | 含义 |
|---|---|
headroom.compression.tokens.input(OTel) |
进入压缩流水线的 token |
headroom.compression.tokens.output(OTel) |
出来的 token |
headroom.compression.tokens.saved(OTel) |
差值。仅压缩层的 pipeline 视角 |
headroom.compression.runs(OTel) |
流水线执行次数。注意:按 pipeline run 计,不是按请求计 |
headroom.compression.pipeline.duration(OTel,秒) |
流水线耗时 |
headroom.compression.transforms{transform}(OTel) |
哪些 transform 被触发了。高基数——在 collector 端丢弃或聚合 |
这些指标由 record_pipeline_run() 一次写入:tokens 前/后差值、pipeline 耗时、每个 transform 一行、每个 stage 一行(跳过 pipeline_total 与 _ 前缀内部键)、以及 waste signal 明细(headroom/observability/metrics.py)。
五个会搞坏 Dashboard 的问题
-
只有节省计数器能跨重启存活。 60 个 Prometheus 指标族中有 55 个在代理重启时归零。只有
headroom_persistent_savings_*持久,且要求HEADROOM_WORKSPACE_DIR位于持久卷上——否则每次部署都会重置。 -
任何地方都没有分位数。 用均值。见延迟面板一节。
-
headroom_latency_ms对流式请求的计时方式不同。 流式请求的计时起点在压缩之后,所以端到端是latency + overhead;非流式则只有latency。不要把两者混在同一个面板里。 -
5xx 会抹掉它自己的节省。 上游失败的请求会从所有节省与 token 计数器中剔除。Provider 故障期间,节省率会显得"异常漂亮"而吞吐在跌——这是假象。
-
设置了代理 token 时
/metrics需要鉴权。 配置了HEADROOM_PROXY_TOKEN后,任何非 loopback 的抓取器都必须发送Authorization: Bearer <token>。loopback 永远豁免。
文档里提到、但代码里不存在的指标
如果面板返回空值,多半是这个原因。下面这些名字出现在对外文档中,但代码并不产生它们:
headroom_compression_ratio · headroom_latency_seconds(及 _bucket)· headroom_cache_hits_total · headroom_cache_misses_total · headroom_cost_usd_total · headroom_requests_total 上的 mode="optimize" 标签
另外,仓库自带的 examples/grafana/headroom-dashboard.json 的所有面板都过滤了 pool 和 hook 两个没有任何指标会产生的标签——对应下拉框会永远是空的;其余指标名本身是正确的。导入该 dashboard 后需先移除这两处过滤条件。
配置参考
# Prometheus —— 什么都不用做,GET /metrics 始终开启
# OpenTelemetry
pip install "headroom-ai[proxy,otel]"
export HEADROOM_OTEL_METRICS_ENABLED=1
export HEADROOM_OTEL_METRICS_ENDPOINT=https://otel.corp.example/v1/metrics
export HEADROOM_OTEL_METRICS_HEADERS="authorization=Bearer XXX"
export HEADROOM_OTEL_RESOURCE_ATTRIBUTES="service.instance.id=$HOSTNAME"
| 变量 | 默认值 | 说明 |
|---|---|---|
HEADROOM_OTEL_METRICS_ENABLED |
0 |
总开关 |
HEADROOM_OTEL_METRICS_EXPORTER |
otlp_http |
或 console。不存在 gRPC exporter |
HEADROOM_OTEL_METRICS_ENDPOINT |
未设置 | 原样传递——不会自动拼接 /v1/metrics |
HEADROOM_OTEL_METRICS_HEADERS |
未设置 | k=v,k2=v2 格式 |
HEADROOM_OTEL_METRICS_EXPORT_INTERVAL_MS |
10000 |
导出间隔 |
HEADROOM_OTEL_SERVICE_NAME |
headroom-proxy |
服务名 |
HEADROOM_OTEL_RESOURCE_ATTRIBUTES |
未设置 | 请在这里设置 service.instance.id——Headroom 不会替你做,多副本会互相撞 |
用 curl -s localhost:8787/stats | jq .otel 验证当前 OTel 配置。
这些环境变量并非文档口径,而是直接由 headroom/observability/metrics.py 的 OTelMetricsConfig.from_env() 解析:
- 总开关:
_parse_bool(os.environ.get("HEADROOM_OTEL_METRICS_ENABLED"), default=False),接受1/true/yes/on与0/false/no/off(大小写不敏感)。 - Exporter 白名单:未知取值会打 warning 并回落到
otlp_http——源码里只有console与otlp_http两个分支,configure_otel_metrics()对后者构造OTLPMetricExporter并原样传入endpoint与headers,再挂到PeriodicExportingMetricReader上(headroom/observability/metrics.py)。 - Headers 解析:按逗号拆分再按第一个
=拆键值,空值与无=的片段静默丢弃(_parse_key_value_pairs,headroom/observability/metrics.py)。 - 导出失败不致命:若未安装 OTel SDK,
configure_otel_metrics()只记录一条 warning("Install headroom-ai[otel]…")并返回 no-op 兼容的 meter(headroom/observability/metrics.py)。
多租户标签:register_otel_metric_attribute_provider() 可以把请求级属性(tenant、team、成本中心等)附加到每个 OTel 数据点。上限为 16 个属性、每个 256 字符——对应源码常量 _MAX_DYNAMIC_ATTRIBUTES = 16 与 _MAX_DYNAMIC_ATTRIBUTE_LENGTH = 256(headroom/observability/metrics.py)。该 seam 的设计约束值得注意:provider 在请求上下文中执行、必须返回"无内容"的标量标签,且失败的 provider 会被静默忽略,保证观测层不会打断流量(headroom/observability/metrics.py);同时动态属性的优先级刻意低于正式维度标签(provider/model/source 等),见 _attrs() 的合并逻辑(headroom/observability/metrics.py)。
离线(air-gapped)部署:HEADROOM_OFFLINE=1 会关闭全部出站流量——匿名用量信标(默认是开启的)、更新检查、以及模型下载。
进一步深入:仓库内的相关入口
- 指标导出的完整拼装逻辑:headroom/proxy/prometheus_metrics.py 的
export(),含标签转义防注入细节(_escape_label_value,防止单个畸形 model 名 500 掉整个 scrape,headroom/proxy/prometheus_metrics.py)。 - OTel 指标注册与记录入口:headroom/observability/metrics.py(
HeadroomOtelMetrics各类 instrument 与record_*方法)。 - 端点挂载:headroom/proxy/server.py(
/metrics)与:8787/stats中的.otel状态段(headroom/proxy/server.py)。 - 行为验证测试:tests/test_prometheus_label_escaping.py、tests/test_prometheus_obs_counters.py、tests/test_proxy_cache_ttl_metrics.py、tests/test_persistent_metrics.py、tests/test_observability_metrics.py。
- 官方配套 dashboard(导入前记得删除
pool/hook过滤):examples/grafana/headroom-dashboard.json。
适用前提再强调一遍:以上指标名、单位(Prometheus 毫秒 / OTel 秒)与默认值均以当前仓库代码为准;/metrics 始终开启,OTel 需要额外安装 headroom-ai[proxy,otel] 并显式打开总开关;需要分位数(p95/p99)时应使用 headroom perf CLI 而非依赖 /metrics。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00