首页
/ Headroom Proxy 指标技术指南:Prometheus 与 OpenTelemetry 双端点的指标体系、PromQL 实践与 Dashboard 避坑

Headroom Proxy 指标技术指南:Prometheus 与 OpenTelemetry 双端点的指标体系、PromQL 实践与 Dashboard 避坑

2026-09-06 14:53:26作者:农烁颖Land

本文围绕 Headroom 代理的官方指标指南 docs/metrics-technical-guide.md 展开,系统讲解 GET /metrics(Prometheus)与 OTLP/HTTP(OpenTelemetry)两套监控端点的定位差异,覆盖节省量、延迟、缓存、流量健康度、订阅窗口、节省归因和压缩内部六大面板的完整指标表与可直接复制的 PromQL;并结合 headroom/proxy/prometheus_metrics.pyheadroom/observability/metrics.py 的源码,说明每个指标在代码中的产生位置与口径差异。读完后你能独立完成:为 Headroom 代理搭建 Prometheus/Grafana 面板、配置多租户 OTel 导出,并规避五类最常见的 Dashboard 失真问题。

两套端点:先分清 Prometheus 与 OTel 的分工

Headroom 代理(默认监听 :8787)提供两个观测面:

端点 获取方式 适用场景
PrometheusGET /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.pyrecord_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.pyexport() 方法拼装。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) 节省金额(美元),按层拆分:compressiontool_schemaoutput_shapingprovider_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.pylatency_*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_stageupstream_connectmemory_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.pyheadroom.proxy.request.durationheadroom.proxy.overhead.durationheadroom.proxy.ttfb.duration 三个 histogram),但使用的是默认桶,所有请求都落进同一个桶,histogram_quantile() 会返回无意义值。均值是可靠的。如果今天就要真实的 p95/p99,用 headroom perf CLI。

另外:每个 _sum 必须除以它自己_count。overhead 和 TTFB 只在大于 0 时才采样记录——源码中是 if overhead_ms > 0 / if ttfb_ms > 0recordheadroom/observability/metrics.py),因此它们的 count 天然小于 latency 的 count。

stage 级指标的双标签设计也有源码依据:stage_timing_sum(path, stage) 元组为键,目的是让同一个指标名区分例如 openai_responses_wsupstream_connectanthropic_messagesupstream_connectheadroom/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_expiryprefix_changeunknown
# 按 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_tokenscache_write_tokenscache_write_5m/1h_tokenshit_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 的流量分布——anthropicopenaigeminibedrock
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} 压缩失败——timeouterror。因失败开放(fail-open),流量继续走但节省悄悄停了。值得配告警
headroom_compression_quarantine_total{event} 连续超时后压缩被隔离禁用——activatedskippedreleased
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.pyMAX_DISTINCT_MODELS = 1024record_request 首次触顶时折叠为 model="other" 且只告警一次(headroom/proxy/prometheus_metrics.py);压缩失败计数器特意区分 timeouterror,注释说明这样能判断"是压缩预算太紧还是真 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_usdheadroom/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_bloatbase64repetitionreread 等。这是诊断信号,不是节省量

这些行是用来"解释"头条数字的,永远不要把它们加到总节省上。Prometheus 导出端对归因行也刻意做了类型区分:headroom_savings_attributed_usd_total 被声明为 # TYPE ... gauge 并标注 "may be negative"(headroom/proxy/prometheus_metrics.py);OTel 侧对应的是 create_up_down_counterheadroom/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 的问题

  1. 只有节省计数器能跨重启存活。 60 个 Prometheus 指标族中有 55 个在代理重启时归零。只有 headroom_persistent_savings_* 持久,且要求 HEADROOM_WORKSPACE_DIR 位于持久卷上——否则每次部署都会重置。

  2. 任何地方都没有分位数。 用均值。见延迟面板一节。

  3. headroom_latency_ms 对流式请求的计时方式不同。 流式请求的计时起点在压缩之后,所以端到端是 latency + overhead;非流式则只有 latency。不要把两者混在同一个面板里。

  4. 5xx 会抹掉它自己的节省。 上游失败的请求会从所有节省与 token 计数器中剔除。Provider 故障期间,节省率会显得"异常漂亮"而吞吐在跌——这是假象。

  5. 设置了代理 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 的所有面板都过滤了 poolhook 两个没有任何指标会产生的标签——对应下拉框会永远是空的;其余指标名本身是正确的。导入该 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.pyOTelMetricsConfig.from_env() 解析:

  • 总开关_parse_bool(os.environ.get("HEADROOM_OTEL_METRICS_ENABLED"), default=False),接受 1/true/yes/on0/false/no/off(大小写不敏感)。
  • Exporter 白名单:未知取值会打 warning 并回落到 otlp_http——源码里只有 consoleotlp_http 两个分支,configure_otel_metrics() 对后者构造 OTLPMetricExporter 并原样传入 endpointheaders,再挂到 PeriodicExportingMetricReader 上(headroom/observability/metrics.py)。
  • Headers 解析:按逗号拆分再按第一个 = 拆键值,空值与无 = 的片段静默丢弃(_parse_key_value_pairsheadroom/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 = 256headroom/observability/metrics.py)。该 seam 的设计约束值得注意:provider 在请求上下文中执行、必须返回"无内容"的标量标签,且失败的 provider 会被静默忽略,保证观测层不会打断流量(headroom/observability/metrics.py);同时动态属性的优先级刻意低于正式维度标签(provider/model/source 等),见 _attrs() 的合并逻辑(headroom/observability/metrics.py)。

离线(air-gapped)部署HEADROOM_OFFLINE=1 会关闭全部出站流量——匿名用量信标(默认是开启的)、更新检查、以及模型下载。

进一步深入:仓库内的相关入口

适用前提再强调一遍:以上指标名、单位(Prometheus 毫秒 / OTel 秒)与默认值均以当前仓库代码为准;/metrics 始终开启,OTel 需要额外安装 headroom-ai[proxy,otel] 并显式打开总开关;需要分位数(p95/p99)时应使用 headroom perf CLI 而非依赖 /metrics

登录后查看全文
热门项目推荐
相关项目推荐