必须用 echo.wraphandler(promhttp.handler()) 包装指标处理器,因 echo 路由要求 func(echo.context) error 签名;http 延迟 histogram 需在响应写出后 observe();label 值不能为空,须清洗并设默认值;指标变量须为包级变量且仅注册一次。

echo.WrapHandler(promhttp.Handler()) 是必须的包装步骤
直接把 promhttp.Handler() 传给 e.GET("/metrics", ...) 会编译失败,因为 Echo 的路由函数要求签名是 func(e echo.Context) error,而 promhttp.Handler() 返回的是标准 http.Handler。不包装就用不了。
正确写法只有一种稳定路径:e.GET("/metrics", echo.WrapHandler(promhttp.Handler()))。别试图自己写适配器——Echo 官方的 echo.WrapHandler 已处理好 Context 生命周期、panic 恢复和 header 写入逻辑。
顺带提醒:一定要启用 Recovery 中间件,否则指标 handler 内部 panic(比如 label 值为空)会导致整个 /metrics 返回 500,Prometheus 抓取失败且无日志提示:
router.Use(middleware.Recovery())
HTTP 延迟 Histogram 必须在响应真正写出后 Observe()
常见错误是在 next(ctx) 前就调 histogram.Observe(),或者只在成功路径里记录,漏掉 http.Error()、panic、context timeout 等退出分支。结果是 P99 延迟严重偏低,监控失真。
可靠做法是用 defer + 启动计时器,并确保所有出口都走同一段统计逻辑:
- 在中间件开头调
start := time.Now() - 用
defer包裹histogram.Observe(time.Since(start).Seconds()) - 但必须保证
start在 defer 作用域外定义,否则闭包捕获的是空值 - 如果用了
ctx.Response().BeforeFunc()或类似 hook,注意它不触发于 writeHeader 失败或 panic 场景
更稳妥的替代方案:用 echo.HTTPErrorHandler 全局接管错误响应,并在那里补一次 Observe()。
label 值为空字符串会导致 Prometheus 拒绝采集
比如你写 httpDuration.WithLabelValues(r.Method, ""),Prometheus 在 scrape 时会报 invalid metric family name 或 duplicate sample for timestamp。这不是警告,是硬性拒绝,该时间序列永远进不了 TSDB。
根本原因是空 label 值让指标失去区分度,且 Prometheus 认为这是非法输入。更隐蔽的问题是:WithLabelValues("") 多次调用会注册多个匿名序列,引发 cardinality 爆炸。
防御措施要写死在中间件里:
- 对所有 label 输入做
strings.TrimSpace()清洗 - 关键字段如
endpoint、status_code设默认值:if path == "" { path = "unknown" } - 避免从 query、header、body 动态提取 label —— 那些字段天然可能为空或含非法字符
不要用 promauto.NewHistogram(),注册冲突会直接 os.Exit(1)
如果你在多个文件里都写了 promauto.NewHistogram(...),或者把指标定义放在 handler 函数里(每次请求都新建),启动时就会 panic:duplicate metrics collector registration。这是因为 promauto 内部自动调了 prometheus.MustRegister(),而 Prometheus 对同名指标注册零容忍。
唯一安全的模式是:
- 所有指标变量声明为包级变量,例如:
var httpReqDuration = prometheus.NewHistogram(...) - 注册动作只做一次,在
init()或main()开头:prometheus.MustRegister(httpReqDuration) - 绝不把
NewXXX()放在 HTTP handler、goroutine、循环或 init 函数之外的任何地方
高并发下还要注意:Histogram 本身是线程安全的,但如果你手动封装了带锁的 wrapper,反而会成为性能瓶颈——原生 client_golang 的实现已足够高效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











