根本原因是echo.WrapHandler包装失败或注册时机错误;常见于Echo v4.9及更早版本兼容性问题、路由注册晚于中间件注入、或响应头被其他中间件覆盖。

为什么 e.GET("/metrics", echo.WrapHandler(promhttp.Handler())) 会返回 404
根本原因不是路由没写,而是 echo.WrapHandler 包装失败或注册时机不对。常见情况包括:
-
echo.WrapHandler在 v4.10 之前存在兼容性问题,v4.9 及更早版本可能 panic 或静默失效 - 路由注册发生在
p.Use(e)之后,但p.Use(e)本身不注册任何路由,只注入指标收集逻辑 - 中间件链里有自定义 Recovery 或 Logger 拦截了
/metrics的响应头,导致 Content-Type 被覆盖为text/plain而非text/plain; version=0.0.4; charset=utf-8 - 用了
http.Handle("/metrics", ...)但没关掉http.DefaultServeMux冲突,比如同时启了两个 server
验证方式:先注释掉所有中间件,只留 e.GET("/metrics", echo.WrapHandler(promhttp.Handler())),curl 测试;再逐步加回中间件定位哪一层破坏了响应。
Context 里怎么安全提取 label 值用于 Histogram Observe()
延迟直方图必须在响应真正写出后调用 Observe(),否则测的是中间件耗时而非端到端延迟。关键点是:不能在 next(c) 前记录,也不能忽略 panic 恢复路径。
- 用
c.Response().Status和c.Response().Size()获取状态码和响应体大小,但c.Response().Status在 panic 后可能为 0,需配合defer+recover()补充判断 - 延迟计算必须基于
time.Since(start),start 时间戳要设在中间件最开头,且不能被后续中间件覆盖(比如 logger 中间件重置了 context) - label 值如
route应从c.Request().URL.Path映射为预定义模板(如GET /user/{id}),而不是直接填原始路径,避免高基数 - 不要用
c.Param("id")拼 label,Echo 的Param在未注册路由时返回空字符串,会导致WithLabelValues("")注册无效序列
自定义 CounterVec 为什么 Inc() 后 /metrics 里看不到数值
现象是指标定义了、注册了、也调了 Inc(),但 curl /metrics 只看到 go_goroutines,你的 http_requests_total 一行都没有。核心问题只有两个:
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 指标变量不是包级变量,而是在 handler 函数里
new出来的——每次请求都新建一个 CounterVec,Inc()作用于临时对象,生命周期结束即丢弃 - 注册动作没在
init()或main()开头执行,而是在某个 HTTP handler 触发后才调prometheus.MustRegister(),此时promhttp.Handler()已启动,读不到新注册的指标 - 用了
promauto.NewCounterVec()却没传注册器,它默认往prometheus.DefaultRegisterer注,但你又手动创建了reg := prometheus.NewRegistry()并传给了promhttp.HandlerFor(),结果指标进错注册器
修复方法:把 var httpReqCounter = prometheus.NewCounterVec(...) 放包顶层,func init() { prometheus.MustRegister(httpReqCounter) } 紧跟其后,确保服务启动前完成注册。
label 值为空字符串引发的 cardinality 爆炸怎么防
空 label 值不会直接报错,但会让 Prometheus 接收端拒绝该时间序列,日志里出现 invalid metric family name 或更隐蔽地生成大量匿名序列(http_requests_total{method="GET",status=""} 和 http_requests_total{method="GET",status=""} 被视为不同序列),几万请求就拖垮 Prometheus。
- 所有 label 输入必须过
strings.TrimSpace(),再判空;比如status := strings.TrimSpace(c.Response().Status()) - 对关键 label 设 fallback,默认值用
"unknown"而非空字符串,例如if status == "" { status = "unknown" } - 禁止把
c.Request().RemoteAddr、c.Param("token")这类高变字段直接当 label,它们天然就是高基数来源 - 用
WithLabelValues()前加断言:if method == "" || status == "" { return },跳过非法组合打点
最易被忽略的一点:Echo 的 c.Path() 在 404 路由下返回空字符串,如果没做兜底,所有 404 请求都会打到同一个 http_requests_total{route="",status="404"} 上,看似安全,实则掩盖了真实路由匹配失败问题。










