# 暴露格式 · /metrics 与 OpenMetrics

> 指标怎么从进程里吐出来？一个纯文本的 HTTP 端点。这一章讲清这个「简单到近乎粗糙」的格式为什么赢了、OpenMetrics 标准化了什么，以及 Prometheus 源码里其实有三套解析器。

- 作者：David（道雾轩）
- 专栏：指标采集与观测（https://daiw.org/manual/observability.md）
- 最后更新：2026-07-15
- 原文：https://daiw.org/manual/observability/exposition-format
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 暴露格式 · /metrics 与 OpenMetrics

[类型定好了](https://daiw.org/manual/observability/metric-types)，指标怎么**从进程里吐出去**？答案朴素得让人意外：**一个返回纯文本的 HTTP 端点**。这一章讲清这个格式，以及“简单”为什么是它最大的优势。

## 就是一段文本

`curl http://your-app:8080/metrics` 得到的就是这样一段东西：

```text
# HELP http_requests_total HTTP 请求总数
# TYPE http_requests_total counter
http_requests_total{method="POST",status="200"} 1027
http_requests_total{method="POST",status="500"} 3
# HELP process_resident_memory_bytes 常驻内存字节数
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 1.8874368e+07
```

规则少到一眼看完：

- **`# HELP <名字> <描述>`** —— 人类可读的说明。
- **`# TYPE <名字> <类型>`** —— [四种类型之一](https://daiw.org/manual/observability/metric-types)。
- **数据行** —— `指标名{标签="值",...} 数值 [时间戳]`。时间戳**可选**（[几乎总是省略](https://daiw.org/manual/observability/scrape-loop)，让 Prometheus 用抓取时刻）。

<Callout type="info">
  **这个格式的设计哲学是“**简单压倒一切**”——它不高效、不紧凑、甚至有点浪费，但它赢了。为什么？**

  - **人能直接读** —— 出问题时 `curl` 一下，**当前所有指标一目了然**。这个“随时可自查”的性质在排障时价值巨大——你不需要任何工具就能确认“这个服务到底吐没吐这个指标”。
  - **谁都能实现** —— 不需要 SDK、不需要 protobuf、不需要客户端库。**用任何语言拼字符串都能吐**。这是它能席卷整个生态（每个数据库、每个中间件都提供 `/metrics`）的根本原因。
  - **HTTP 是通用底座** —— 复用了整个 HTTP 生态：反向代理、认证、TLS、防火墙规则、curl……**不用发明任何东西**。

  **代价是浪费**：指标名和标签在每一行**重复**出现，文本比二进制大得多。但 Prometheus 认为这笔交易划算——网络传输的那点开销，远不及“**任何人 5 分钟就能接入**”带来的生态价值。这是个“**用机器效率换人类效率和生态采纳**”的经典决策，也解释了它为什么能成为[事实标准](https://daiw.org/manual/observability/the-three-projects)。
</Callout>

## OpenMetrics：把事实标准写成规范

这个格式最初只是 Prometheus 的实现细节，后来被标准化为 **OpenMetrics**。它规范化并收紧了几点：

- **`_total` 后缀** —— [counter 必须以 `_total` 结尾](https://daiw.org/manual/observability/metric-types)，从“惯例”变成“规范”。
- **`# EOF` 结束标记** —— 明确标记文档结束，避免“截断的响应被当成完整数据”这类隐患。
- **exemplar 支持** —— 可以在采样点上挂一个 **exemplar**（带 trace_id 的样例），这是 [metrics 与 traces 关联](https://daiw.org/manual/observability/three-pillars)的桥，[第 7 部分](https://daiw.org/manual/observability/three-pillars)细讲。
- **时间戳精度、单位约定**等细节。

<Callout type="info">
  **实战上你几乎不用关心 OpenMetrics 与旧格式的区别——客户端库会处理，Prometheus 靠 HTTP 的 `Accept` 头协商用哪个。** 值得记住的只有两点：①**counter 命名带 `_total`**；②要做 [metrics↔traces 关联](https://daiw.org/manual/observability/three-pillars)时，exemplar 需要 OpenMetrics 格式。
</Callout>

## 源码里其实有三套解析器

抓回来的文本要被解析。看 `model/textparse/` 目录，有意思的是**不止一套解析器**：

| 解析器 | 源码 | 解析什么 |
| --- | --- | --- |
| Prometheus 文本 | `promparse.go`（+ `promlex.l` 词法） | 经典 `/metrics` 文本格式 |
| OpenMetrics | `openmetricsparse.go`(+ `openmetricslex.l`) | OpenMetrics 格式（带 exemplar） |
| Protobuf | `protobufparse.go` | 二进制格式（[native histograms](https://daiw.org/manual/observability/three-pillars) 走这条） |
| NHCB | `nhcbparse.go` | 经典直方图转 native histogram |

<Callout type="info">
  **两个值得注意的细节：**

  - **词法分析器是**生成**的** —— `promlex.l` 是 [golex](https://daiw.org/manual/observability/reading-the-source) 的输入，生成 `promlex.l.go`（`model/textparse/README.md` 说明了怎么重新生成）。**解析 `/metrics` 这么“简单”的格式，他们仍然上了正经的词法分析器**——因为这是[抓取热路径](https://daiw.org/manual/observability/scrape-loop)，每秒要解析几十万行，性能极度敏感。
  - **protobuf 格式回来了** —— 早年 Prometheus 弃用了 protobuf 暴露格式（为了简单），但 **[native histograms](https://daiw.org/manual/observability/three-pillars) 又把它请了回来**——因为直方图的复杂结构用文本表达太笨重。**这是“简单优先”原则的一次务实妥协**：主路径保持文本，复杂结构走二进制。
</Callout>

源码里还有个细节能说明性能压力：解析器用了 `yoloString`（把 `[]byte` **零拷贝**转成 `string`，见 `promparse.go:337` 附近）——**连一次字符串拷贝都要省**。这就是热路径的样子。

## 实战：自查清单

`/metrics` 是你排查采集问题的**第一现场**：

```bash
# 1. 服务到底吐没吐?
curl -s http://localhost:8080/metrics | head -20

# 2. 我关心的指标在不在?
curl -s http://localhost:8080/metrics | grep http_requests_total

# 3. 有多少条 series?(基数自查!)
curl -s http://localhost:8080/metrics | grep -vc '^#'

# 4. 哪个指标的 series 最多?(揪出基数元凶)
curl -s http://localhost:8080/metrics | grep -v '^#' | sed 's/{.*//' | sort | uniq -c | sort -rn | head
```

<Callout type="warn">
  **第 4 条命令值得你现在就去跑一遍。** 它按指标名统计 series 数，**排在最前面的就是你的基数大户**。很多团队直到 Prometheus 内存爆了才去查，其实**在服务端 `curl` 一下就能提前发现**——某个指标吐出几万行，基本就是标签里混进了[高基数的东西](https://daiw.org/manual/observability/defining-metrics)（user_id、URL 路径带 ID、错误消息……）。[第 2 部分](https://daiw.org/manual/observability/defining-metrics)和[第 9 部分](https://daiw.org/manual/observability/three-pillars)会讲怎么根治，但这个自查命令是**最便宜的早期预警**。
</Callout>

## 小结与下一章

暴露格式就是**一个返回纯文本的 HTTP 端点**：`# HELP`/`# TYPE` 加数据行（`名字{标签} 值 [时间戳]`，时间戳几乎总省略）。它的哲学是“**简单压倒一切**”——[人能直接 curl 读](https://daiw.org/manual/observability/exposition-format)、[任何语言拼字符串就能实现](https://daiw.org/manual/observability/exposition-format)、复用 HTTP 生态；代价是文本浪费，但换来了**生态采纳**，这是它成为[事实标准](https://daiw.org/manual/observability/the-three-projects)的根本。**OpenMetrics** 把它标准化（[`_total` 后缀](https://daiw.org/manual/observability/metric-types)、`# EOF`、[exemplar](https://daiw.org/manual/observability/three-pillars)）。源码 `model/textparse/` 里有**三套解析器**（文本 / OpenMetrics / protobuf，外加 NHCB），词法分析器是 [golex 生成](https://daiw.org/manual/observability/reading-the-source)的、还用 `yoloString` 零拷贝——**因为这是每秒解析几十万行的热路径**；[native histograms](https://daiw.org/manual/observability/three-pillars) 让 protobuf 格式回归，是“简单优先”的一次务实妥协。实战上，[`curl /metrics` 是排查采集问题的第一现场](https://daiw.org/manual/observability/exposition-format)，也是**最便宜的基数自查**。

指标端点有了，Prometheus 怎么知道**去抓谁**？下一章看服务发现。👉 [服务发现](https://daiw.org/manual/observability/service-discovery)。

## 参考资料

- [1] Prometheus 源码 `model/textparse/`（`promparse.go`、`openmetricsparse.go`、`protobufparse.go`、`nhcbparse.go`、`README.md`、`promlex.l`）。
- [2] OpenMetrics 规范（CNCF）。
- [3] Prometheus 官方文档：Exposition formats。
