/metrics返回空或404的根本原因是未将promhttp.Handler()显式挂载到路由,仅调用prometheus.MustRegister()注册指标无法暴露HTTP端点;必须通过http.Handle("/metrics", promhttp.Handler())或Gin的r.GET("/metrics", gin.WrapH(promhttp.Handler()))完成挂载。

为什么/metrics返回空或404?
根本原因不是指标没定义,而是没把 promhttp.Handler() 挂到路由上。Gin默认不暴露任何监控端点,prometheus.MustRegister() 只负责把指标注册进全局收集器,不等于自动提供HTTP接口。
必须显式注册 handler:
- 用标准库:
http.Handle("/metrics", promhttp.Handler()),再http.ListenAndServe(":8080", nil) - 用Gin:
r.GET("/metrics", gin.WrapH(promhttp.Handler())),注意gin.WrapH是必需的包装层 - 别写成
r.Use(promhttp.Handler())—— 这是中间件用法,类型不匹配,编译会报错
直方图桶(Buckets)怎么设才合理?
默认的 prometheus.DefBuckets 从 0.005 秒开始,跨度太大,对毫秒级接口完全失真。比如一个平均耗时 80ms 的 API,95% 请求落在 100ms 内,但默认桶里最近的是 0.1s 和 0.25s,导致 P95 被粗粒度归到 0.25s 桶,误差翻倍。
推荐按实际压测结果设桶,例如:
- 高频轻量接口(如健康检查):
[]float64{0.001, 0.005, 0.01, 0.025, 0.05, 0.1} - 普通业务接口(如订单查询):
[]float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5} - 重计算接口(如报表导出):
[]float64{1, 5, 10, 30, 60, 120}
桶区间必须严格递增,且不能重复;一旦定义好就别 runtime 修改,否则 Prometheus 会拒绝该指标系列。
Gin中间件里怎么正确捕获真实耗时和状态码?
直接读 c.Writer.Status() 在 handler 执行完后才能拿到值,但中间件里需要在 WriteHeader 被调用时就记录——因为有些响应(如 304、204)不走 Write,只走 WriteHeader。
必须用 wrapper 包装 ResponseWriter:
- 实现
WriteHeader(int)方法,在里面更新状态码和起始时间差 - 实现
Write([]byte)方法,仅用于标记 body 已写入(避免多次统计) - 别依赖
c.Abort()或 panic 捕获——recover 中间件可能拦截了错误,但耗时已结束,状态码却没来得及写 - 路径要标准化:
c.FullPath()返回/user/:id,不是/user/123,否则直方图维度爆炸
为什么直方图指标一直为 0?
最常见原因是:定义了 prometheus.NewHistogramVec,但没调 prometheus.MustRegister();或者注册了,但中间件里调的是 .WithLabelValues("GET", "/api/v1/user").Observe(duration.Seconds()),而 label 值跟注册时的 []string{"method", "path"} 不匹配(比如漏了 status)。
检查三件事:
- 注册语句是否执行(放在
init()或 main 开头,确保只一次) - 调用
.Observe()时传入的 label 数量和顺序是否与NewHistogramVec构造时一致 - 确认
duration是time.Since(start)得到的time.Duration,别误传int64或毫秒值(Observe要秒单位)
直方图不支持动态添加 label,定义后维度就固定了,改 label 名或数量必须重启服务。


















