OpenMetrics 与指标规范治理:暴露格式、命名单位与兼容性

系统讲解 OpenMetrics 规范与指标治理:文本暴露格式与内容协商、命名与单位约定(base unit、_total/_seconds/_bytes)、HELP/TYPE/UNIT 元数据、Exemplar 语法、OpenMetrics 与 Prometheus 文本格式的兼容与迁移,以及用 linter 与 CI 落地指标规范治理的完整方法。

同一个延迟指标,A 团队叫 latency(单位毫秒),B 团队叫 request_duration_ms(单位毫秒),C 团队叫 http_request_duration_seconds(单位秒);同一个计数器,有人叫 requests,有人叫 requests_total。当这些指标汇入同一个 Prometheus 时,跨服务的聚合查询、告警复用、大盘拼接全部失效——不是技术做不到,而是数据本身没有统一的语义。

指标规范治理要解决的就是这件事。而 OpenMetrics 是这个问题的标准化答案:它规定了暴露格式、命名与单位约定、元数据、Exemplar 语法,让「指标长什么样」不再是各家的自由发挥。本文从格式规范讲到治理落地,包括用 linter 和 CI 把规范变成可执行的约束。

1. 指标治理要解决的四个问题

问题一:命名混乱
  latency / latency_ms / request_latency_seconds 三套并存
  无法跨服务写一条聚合查询

问题二:单位不明
  duration 是秒还是毫秒?只看名字猜不出
  跨服务求和时单位不一致 → 结果荒谬

问题三:类型错用
  把单调递增的计数写成 gauge,或把瞬时值写成 counter
  导致 rate() / increase() 结果错误

问题四:标签爆炸
  用标签承载高基数维度(用户 ID、请求 ID)
  存储与查询双双崩溃

前三个问题都可以靠规范 + 自动检查解决;第四个问题属于基数治理,需要专门的优化手段。规范的价值在于:把"约定"从文档里搬到 CI 里,让不合规的指标在合并前就被拦下。

2. OpenMetrics 暴露格式

2.1 文本格式全貌

OpenMetrics 是 Prometheus 文本格式的标准化演进,核心结构如下:

# HELP http_requests_total 已处理的 HTTP 请求总数
# TYPE http_requests_total counter
# UNIT http_requests_total requests
http_requests_total{method="get",code="200"} 1027 1696233600.123
http_requests_total{method="post",code="500"} 3 1696233600.123
# EOF
要素说明:
  # HELP   人类可读描述,同一指标只出现一次,必须在使用前
  # TYPE   指标类型:counter / gauge / histogram / gaugehistogram / summary / info / stateset
  # UNIT   单位(OpenMetrics 新增),如 seconds / bytes / requests
  # EOF    流结束标记(OpenMetrics 新增),告诉抓取端"本次抓取完整"
  样本行   metric_name{label="v"} value [timestamp]
  时间戳可选,单位毫秒(OpenMetrics 中为秒,见 2.4)

2.2 # EOF 的意义

Prometheus 文本格式没有结束标记,抓取端只能靠连接关闭或超时判断「抓完了」。这带来一个隐患:传输中途截断时,抓取端无法区分"就这些"与"还没传完",可能写入不完整的数据。

OpenMetrics 的做法:
  正常结束 → 发送 "# EOF\n",抓取端确认完整
  连接断开且未见 # EOF → 抓取端标记为不完整,可重试或丢弃

实践价值:
  在高频抓取(如 1s 间隔)或大指标量(数万序列)场景下,
  截断检测能显著减少"看似正常实则残缺"的数据

2.3 内容协商(Content Negotiation)

抓取端通过 HTTP Accept 头声明自己支持的格式,服务端据此决定返回哪种格式:

请求:
  Accept: application/openmetrics-text; version=1.0.0; charset=utf-8
          , text/plain; version=0.0.4

含义:
  首选 OpenMetrics 1.0.0
  若服务端不支持,退回 Prometheus 文本 0.0.4

服务端响应:
  Content-Type: application/openmetrics-text; version=1.0.0; charset=utf-8
  或 Content-Type: text/plain; version=0.0.4; charset=utf-8
# 手工验证服务端是否支持 OpenMetrics
curl -s -H 'Accept: application/openmetrics-text' http://localhost:9100/metrics | tail -3
# 若最后一行是 "# EOF" 则支持;否则退回的是 Prometheus 文本格式

# 查看响应头确认协商结果
curl -sI -H 'Accept: application/openmetrics-text' http://localhost:9100/metrics \
  | grep -i content-type

ℹ️ 核心:兼容性靠的是「协商 + 回退」,而不是「强制升级」。服务端应同时支持两种格式,抓取端用 Accept 表达偏好。这样新旧客户端可以共存,迁移不需要大爆炸式切换。

2.4 时间戳单位的差异

这是迁移时最容易踩的坑:

Prometheus 文本格式:时间戳单位是【毫秒】
OpenMetrics:       时间戳单位是【秒】(含小数部分)

# Prometheus 文本
metric 42 1696233600123

# OpenMetrics
metric 42 1696233600.123
风险:
  客户端库若在切换格式时未同步调整单位,时间戳会相差 1000 倍
  表现为数据被丢弃(时间戳过于久远/未来)或严重错位
  自查:迁移后对比同一指标在两种格式下的写入时间

3. 命名与单位约定

3.1 三条硬规则

规则一:使用基本单位(base unit),不加前缀
  ✅ request_duration_seconds   ❌ request_duration_ms
  ✅ response_size_bytes        ❌ response_size_kilobytes
  理由:单位可换算,但指标名不可;统一基本单位后
        跨服务聚合才有意义,展示层的换算交给 Grafana

规则二:单位作为后缀,且是复数
  seconds / bytes / meters / volts / amperes / joules / ratio
  例外:无单位的比例用 ratio,百分比用 ratio(0~1)而非 0~100

规则三:类型决定后缀
  counter        → 以 _total 结尾(如 http_requests_total)
  gauge          → 无固定后缀,描述瞬时值
  histogram      → _bucket / _sum / _count
  summary        → 分位数 / _sum / _count
  info           → 以 _info 结尾,值恒为 1
  stateset       → 以 _state 结尾(OpenMetrics 特有)

3.2 反例清单

反例问题正确写法
latency_ms用了非基本单位latency_seconds
requests(counter)缺 _total 后缀requests_total
http_requests_total(gauge)类型与后缀矛盾改名或改为 counter
memory_usage单位不明memory_usage_bytes
cpu_percent(0~100)用了百分比而非 ratiocpu_usage_ratio(0~1)
user_id 作为标签高基数移出标签,改用日志/链路
ServiceName大写与驼峰service_name(小写蛇形)
http_status(值 200)数值型状态码做标签值尚可,但做指标名不行标签 code="200"

3.3 标签命名约定

合法字符:[a-zA-Z_][a-zA-Z0-9_]*
推荐风格:小写蛇形(snake_case)
语义对齐:优先复用 OpenTelemetry 语义约定,便于与链路/日志对齐
  service.name      → service_name
  http.request.method → method(或 http_method)
  http.response.status_code → status_code(或 code)
保留标签:__name__(指标名)、__address__ 等由 Prometheus 内部使用,
         业务标签不得以 __ 开头

指标名与标签的统一,是让指标、链路、日志三者在语义层面对齐的前提。日志字段的同类约定可参考 结构化日志与语义约定 ,两者共用同一套命名哲学。

4. Exemplar 与元数据

4.1 Exemplar 语法

Exemplar 是 OpenMetrics 相对 Prometheus 文本格式的关键增量,它允许在样本行尾附加「示例引用」(通常是 trace_id):

# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{le="0.5"} 24054 # {trace_id="a1b2c3"} 0.42 1696233600.123
http_request_duration_seconds_bucket{le="1.0"} 24110 # {trace_id="d4e5f6"} 0.91 1696233601.456
http_request_duration_seconds_bucket{le="+Inf"} 24110
http_request_duration_seconds_sum 53423.0
http_request_duration_seconds_count 24110
# EOF
语法要点:
  # {label="value"} value timestamp
  trace_id 是约定俗成的 key,也可用 span_id / request_id
  只有 OpenMetrics 格式支持,Prometheus 文本格式不支持
  仅 counter 与 histogram 有语义(gauge 的 exemplar 意义弱)

从指标跳到具体 trace 的完整工程链路(存储、保留窗口、Grafana 配置)在 Exemplar 与指标-链路关联 中有详细展开。

4.2 _created 与 info/stateset

OpenMetrics 还引入了几个 Prometheus 文本格式没有的约定:

_created 时间戳序列:
  metric_name_created  表示该指标(或该序列)的创建时间
  用途:区分"新序列刚创建"与"老序列值为 0"
       避免 rate() 在序列刚出现时算出异常峰值

info 类型:
  build_info{version="1.2.3",commit="abc"} 1
  值恒为 1,信息全在标签里;改标签值即表示"信息变了"
  用途:把版本、构建信息等非数值元数据表达为指标

stateset 类型:
  feature_enabled{feature="x"} 0/1,且用 # UNIT 声明
  是 OpenMetrics 1.0 新增,用于表达"一组互斥或共存的状态"
  兼容性:许多后端尚不支持,落地前需确认

5. 兼容性与迁移

5.1 三方矩阵

组件OpenMetrics 支持情况注意事项
Prometheus 抓取端2.x 起支持协商,需 --enable-feature=openmetrics 类开关(版本而异)建议同时支持两种格式
客户端库多数 Go/Java/Python 库可输出两种注意时间戳单位差异
推送网关 / remote_write走 Protobuf,与文本格式无关Exemplar 需显式开启
后端(Mimir/Thanos)支持 OpenMetrics 语义_created 与 stateset 支持度不一

5.2 迁移步骤

第一步:并行暴露
  服务端同时支持两种格式,用 Accept 协商决定返回哪种
  此时抓取端仍以 Prometheus 文本为主,验证 OpenMetrics 输出正确

第二步:验证一致性
  对同一指标,分别抓两种格式,比对样本值、标签、时间戳
  重点核对时间戳单位(毫秒 vs 秒)

第三步:开启 Exemplar(若需要)
  确认后端与存储链路支持,且保留窗口满足排障需求

第四步:切换抓取端偏好
  把 Accept 头改为优先 OpenMetrics
  观察写入错误率与数据缺口

第五步:清理旧格式
  确认无客户端依赖后,评估是否关闭 Prometheus 文本输出

5.3 迁移中的常见故障

故障一:时间戳错位 1000 倍
  现象:数据全部被拒或落到错误的时间点
  原因:客户端库切换格式时未改时间戳单位
  对策:先小流量验证,比对时间戳

故障二:`# EOF` 未发送
  现象:抓取端报 "unexpected end of stream" 或标记不完整
  原因:服务端拼接响应时忘了加结束标记
  对策:用 curl 检查最后一行

故障三:Exemplar 丢失
  现象:Grafana 直方图上没有菱形点
  原因:抓取端未开启 exemplar 存储或远端未开 send_exemplars
  对策:检查 --enable-feature=exemplar-storage 与远端配置

6. 把规范变成可执行的约束

6.1 用 linter 检查

规范写在文档里没人看,写进 CI 才会被遵守。Prometheus 生态的 promlint 与 pint 可以做基础检查:

# promtool 自带 lint:检查命名、类型、HELP 一致性
promtool check metrics < /metrics.txt

# pint(Prometheus 规则与指标 linter)可做更细的规则检查
pint lint --checks all metrics.prom

# 常见输出示例
#  metrics.txt:12: http_request_duration_ms should use base unit 'seconds'
#  metrics.txt:30: counter 'requests' should have '_total' suffix

6.2 CI 中的指标规范门禁

# .github/workflows/metrics-lint.yml
name: metrics-lint
on: [pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: 抓取被测服务的 /metrics
        run: |
          docker compose up -d app
          sleep 5
          curl -s http://localhost:8080/metrics > metrics.txt
      - name: 运行 promtool lint
        run: |
          promtool check metrics < metrics.txt
      - name: 自定义命名规则检查
        run: |
          python3 scripts/check_metric_names.py metrics.txt
# scripts/check_metric_names.py —— 自定义命名规则(节选)
import re, sys

ALLOWED_UNITS = ("seconds", "bytes", "meters", "volts", "amperes",
                 "joules", "ratio", "requests", "total", "info", "state")

def check(name: str) -> list[str]:
    errs = []
    if re.search(r"[A-Z]", name):
        errs.append(f"{name}: 指标名必须小写")
    if re.search(r"_(ms|us|ns|kb|mb|gb|kilobytes|megabytes)$", name):
        errs.append(f"{name}: 使用了非基本单位,改用 seconds/bytes")
    if name.endswith("_total") is False and is_counter(name):
        errs.append(f"{name}: counter 必须以 _total 结尾")
    return errs

if __name__ == "__main__":
    for line in open(sys.argv[1]):
        m = re.match(r"^([a-zA-Z_:][a-zA-Z0-9_:]*)", line)
        if m:
            for e in check(m.group(1)):
                print(e)

6.3 治理清单

□ 全站统一基本单位:seconds / bytes / ratio(0~1)
□ counter 一律 _total 结尾,histogram 用 _bucket/_sum/_count
□ 指标名小写蛇形,标签名复用 OTel 语义约定
□ 每个指标必须有 HELP 与 TYPE,可选 UNIT
□ 禁止以 __ 开头命名业务标签
□ 高基数维度(用户 ID、请求 ID)不得进标签
□ CI 中跑 promtool / pint,不合规即阻断合并
□ 迁移 OpenMetrics 前核对时间戳单位与 # EOF
□ 新指标上线前登记到指标目录(metric catalog)

7. 与更广的可观测性体系的关系

指标规范不是孤立的「洁癖」,它直接影响下游三件事:

影响一:查询与告警复用
  统一命名后,一条 rate(http_requests_total[5m]) 可跨服务复用
  告警规则可以模板化,而不是每个服务写一套

影响二:跨信号关联
  指标名/标签与链路属性、日志字段对齐后,
  指标 → 链路 → 日志的跳转才能真正自动化

影响三:长期存储效率
  规范的类型与单位让降采样、聚合、压缩算法更有效
  命名混乱则会让存储层无法做"同族指标"的优化

想理解指标从暴露到存储的完整链路(抓取、TSDB、远端写),可参考 Prometheus 深入解析 ;端到端的 SDK 埋点与导出器配置可参考 OpenTelemetry 完整指南 。规范是这些环节共同的地基。

小结

OpenMetrics 把指标从「各家自定义的文本」推进到「有规范、可协商、可校验的协议」。落地时抓住三件事:格式层用 Accept 协商 + 回退,服务端同时支持 OpenMetrics 与 Prometheus 文本,迁移时重点核对时间戳单位(秒 vs 毫秒)与 # EOF;语义层坚持基本单位、_total 后缀、小写蛇形与 OTel 语义约定,让跨服务聚合与跨信号关联成为可能;治理层把规范写进 CI,用 promtool/pint 与自定义脚本做门禁,让不合规指标在合并前被拦下。规范看似琐碎,但它决定了你的可观测性数据到底是「一堆数字」还是「一套可复用的语言」。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「infra」更多文章

  1. 仪表盘设计与可读性工程:信息层级、图表选型与避免误读
  2. 日志采样、去重与降噪:从每天 TB 级日志里捞出信号
  3. 边缘与 IoT 可观测性:弱网缓冲、设备侧采集与带宽约束