go服务暴露自定义指标必须完成注册、绑定、打点三步闭环,缺一不可;若/metrics为空或仅有go_*指标,根本原因是指标注册器与promhttp.handler()绑定的注册器不一致,或注册未在http服务启动前完成。

Go 服务暴露自定义指标,不是“写了 NewCounterVec 就能被 Prometheus 抓到”,而是必须完成注册、绑定、打点三步闭环——漏掉任一环,/metrics 里就只看得到 go_goroutines,你的 http_requests_total 永远不会出现。
为什么 /metrics 返回空或只有 go_* 指标
根本原因是指标注册器(registry)和 promhttp.Handler() 绑定的 registry 不一致。比如你调了 prometheus.MustRegister(),它默认注册到全局 prometheus.DefaultRegisterer;但如果你用了 promhttp.HandlerFor(reg, ...),而 reg 是个新创建的空注册器,那自然啥业务指标都没有。
- 检查是否混用注册方式:全局注册(
prometheus.MustRegister())和自定义注册(reg.MustRegister())不能交叉使用 - 确认
promhttp.Handler()和promhttp.HandlerFor(reg, ...)是否指向同一个reg实例 - 注册必须发生在 HTTP server 启动前,推荐统一放在
func main()开头,别塞进init()(包初始化顺序不可控) - 若用自定义 registry,记得手动加运行时指标:
reg.MustRegister(prometheus.NewGoCollector()),否则连go_memstats_alloc_bytes都不会出现
CounterVec.WithLabelValues() panic:expected N label values but got M
这是标签数量不匹配最典型的报错。定义时指定了几个 label 名,打点时就必须传满几个值,一个都不能少,顺序也不能错。
- 定义:
reqCounter := prometheus.NewCounterVec(..., []string{"method", "status", "path"}) - 正确打点:
reqCounter.WithLabelValues("GET", "200", "/api/users") - 错误写法:
reqCounter.WithLabelValues("GET", "200")→ panic - 高频场景建议缓存子指标:
get200 := reqCounter.WithLabelValues("GET", "200"),后续直接get200.Inc(),避免重复 map 查找开销
Gin/Echo 中 /metrics 返回空业务指标
框架封装容易掩盖底层细节。Gin 的 gin.WrapH(promhttp.Handler()) 会丢 Content-Type,Echo 的 echo.WrapHandler() 在 v4.10 之前有兼容问题——都不是“挂路由”本身的问题,而是 handler 适配失效。
- Gin 正确写法:
router.GET("/metrics", func(c *gin.Context) { promhttp.Handler().ServeHTTP(c.Writer, c.Request) }) - Echo 正确写法:
e.GET("/metrics", echo.WrapHandler(promhttp.Handler())),但需确认 echo 版本 ≥ 4.10 - GoFrame 用户注意:
g.Server().BindHandler()必须用ghttp.NewHandlerFunc()包一层,BindObject不支持http.Handler接口 - 所有框架下,确保
promhttp.Handler()挂载在业务路由启动前,且没被中间件(如 gzip、auth)提前写 response body
路径带 ID 导致指标爆炸:/order/123456789 怎么处理
把原始 URL 路径直接当 label 值塞进 WithLabelValues(),等于给 Prometheus 制造无限 series —— 几万订单就生成几万个指标序列,存储和查询全崩。
- 必须做路径归一化:用正则或框架能力提取路由模板,如 Gin 的
c.FullPath()返回/api/orders/:id,而非/api/orders/123456789 - 禁止将用户 ID、手机号、traceID 等高基数字段作为 label;改用采样、聚合或单独计数器(如按
error_type分类) - 标签名严格小写+下划线:
tenant_id合法,tenant-id或TenantID会导致panic: invalid metric name - 标签值不能为空字符串或
nil,否则该 series 会被静默丢弃(Prometheus server 不报错,但你看不见)
指标注册不是“写完就跑”,而是启动期一次性的契约行为;标签设计不是“越细越好”,而是对 cardinality 的持续克制。这两点,是绝大多数人在本地跑通后,上线就被打爆的真实原因。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











