Go程序暴露/metrics端点需注册promhttp.Handler()而非手动拼接,使用CounterVec按标签分类统计,确保registry实例一致,避免重启归零需合理配置Prometheus重置容忍机制。

怎么让 Go 程序暴露 /metrics 端点
Go 程序要被 Prometheus 抓取,必须提供一个 HTTP 接口返回符合格式的指标文本,默认路径是 /metrics。这不靠框架自动完成,得手动注册 http.Handle 并挂上 Prometheus 的 handler。
常见错误是直接用 http.HandleFunc 自己拼字符串——Prometheus 不认,会报 text format parsing error。必须用官方库提供的 promhttp.Handler()。
- 导入
"github.com/prometheus/client_golang/prometheus/promhttp" - 在启动 HTTP server 前加一句:
http.Handle("/metrics", promhttp.Handler()) - 确保这个 handler 在其他路由之前注册,否则可能被
http.NotFoundHandler拦截 - 别在 handler 外层加 JSON 中间件或 gzip 压缩——
promhttp.Handler()自带text/plain; charset=utf-8和合适的压缩逻辑
用 prometheus.NewCounter 还是 prometheus.NewCounterVec
计数器选错类型,后续查监控时就只能看到一堆没标签的数字,根本分不清是哪个 API、哪个状态码、哪个用户组触发的。
prometheus.NewCounter 适合全局唯一指标(比如总启动次数),而真实业务里几乎都要分类统计,这时必须用 prometheus.NewCounterVec,它支持按标签维度切分。
立即学习“go语言免费学习笔记(深入)”;
- 定义 Vec 时必须传
prometheus.CounterOpts和[]string{"method", "status_code", "path"}这类标签名 - 打点时用
counterVec.WithLabelValues("GET", "200", "/api/users"),顺序和定义一致,错一位就 panic - 标签值不能含空格、换行、引号;路径中带变量(如
/user/:id)要预处理成固定标签(如"/user/{id}"),否则会导致指标爆炸 - 别在循环里反复调用
WithLabelValues——它内部有 map 查找,高频调用影响性能;应提前存好prometheus.Counter实例
为什么 promhttp.Handler() 返回 404 或 500
不是端口没开,也不是防火墙问题,而是指标注册器(prometheus.Register)和 handler 没对上同一个 registry 实例。
默认情况下 promhttp.Handler() 读的是全局 registry,但如果你用了自定义 registry(比如为了隔离测试环境指标),却忘了把 handler 指向它,就会返回空内容或 500 错误。
- 检查是否误用了
prometheus.NewRegistry()却没传给promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) - 确认所有
prometheus.MustRegister()都作用于同一个 registry 实例 - 如果用了多个 registry(如主应用 + 健康检查模块),每个都要单独暴露端点,比如
/metrics/app和/metrics/health - 运行时执行
curl http://localhost:8080/metrics,若返回空且状态码 200,大概率是 registry 为空;若返回 500,看日志里有没有duplicate metrics collector registration attempted
Go 应用重启后 Prometheus 图表断崖式归零
Prometheus 是拉模型,本身不保存历史,但 Go 的计数器(Counter)是单调递增的。如果程序重启,计数器从 0 开始,Prometheus 会认为发生了“重置”,自动做 counter reset 修正——前提是它能识别这是重置而非网络抖动丢包。
但默认配置下,Prometheus 对重置的容忍窗口只有 10 分钟。如果应用频繁启停,或者抓取间隔设得太长(如 scrape_interval: 30s),它可能来不及判断就当成异常丢弃数据。
- 在 Prometheus 配置里加
honor_labels: true和合理的scrape_timeout(建议 ≤scrape_interval的 2/3) - Go 端避免用
time.Now().Unix()初始化 counter 初始值——Counter 不该设初值,它是靠累积增长的 - 如果确实需要跨进程延续计数值(比如灰度发布时部分实例先升级),得自己持久化 last value 到 Redis 或本地文件,并在启动时用
prometheus.NewCounter(prometheus.CounterOpts{...})+Set()恢复(注意:这违背 Counter 语义,仅限特殊场景) - 更稳妥的做法是改用
Gauge记录瞬时值(如当前并发请求数),它天然支持上下波动
最常被忽略的一点:Gauge 类型指标虽然能上下浮动,但 Prometheus 的 rate() 函数不适用于 Gauge,计算 QPS 必须用 Counter。所以选型不是看“好不好写”,而是看“你要算什么”。










