必须在 HTTPErrorHandler 中统一计算耗时和状态码,因中间件可能被跳过或 panic 导致指标漏采;起点设于首层中间件,终点锚定 HTTPErrorHandler,标签需规整收敛,注册须前置。

为什么不能在中间件里用 time.Since() 算耗时后直接累加计数器
因为中间件可能被跳过——比如路由未匹配、panic 触发 recovery、连接提前中断,这些路径下中间件的 defer 或结束逻辑根本不会执行,导致指标漏采。更隐蔽的问题是:handler panic 后 recovery 中间件虽能恢复,但原始 handler 的耗时已无法准确获取。
真正可靠的耗时起点必须在请求进入的第一层(如自定义中间件开头),终点必须落在统一出口——echo.HTTPErrorHandler 是唯一稳定锚点,它在所有请求结束(成功/失败/panic/超时)时必调用。
- 别在中间件里写
defer metrics.Counter.Inc()或metrics.Histogram.Observe(time.Since(start).Seconds()) - 中间件中只做一件事:存
c.Set("startTime", time.Now()) - 所有指标更新(状态码、路径、耗时)必须在重写的
HTTPErrorHandler里读取c.Get("startTime")并计算
如何安全地在 HTTPErrorHandler 中提取耗时和状态码
HTTPErrorHandler 的参数只有 error 和 echo.Context,但它能访问完整响应上下文。关键点在于:状态码必须从 c.Response().Status 读,而不是依赖 c.Request().Context().Value() 或 header;耗时必须用 time.Since() 而非 time.Now().Sub(),避免系统时间跳变导致负值样本被 Prometheus 拒绝。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 确保前置中间件已设置:
c.Set("startTime", time.Now()) - 在
HTTPErrorHandler中:start, ok := c.Get("startTime").(time.Time)if ok { duration := time.Since(start).Seconds() } - 状态码用
c.Response().Status,它在 writeHeader 后已确定,比r.Header.Get("Status")可靠得多 - 路径需归一化:把
/user/123→/user/{id},否则 label 组合爆炸
如何用 Prometheus.CounterVec 区分接口维度又不爆 cardinality
盲目按 c.Request().RemoteAddr 或完整 URL 打点,会迅速拖垮 Prometheus。必须预设可控标签集,并对高基数字段做规整。
- 推荐标签组合:
method、path(规整后)、status(用strconv.Itoa(c.Response().Status)) - 禁止使用:
c.Request().URL.String()、c.Request().UserAgent()、c.Request().RemoteAddr -
path规整逻辑建议放在中间件或HTTPErrorHandler中统一处理,例如正则替换/\d+→/{id} -
WithLabelValues()传入的值不能为空字符串,否则prometheus会 panic
注册指标和打点时机的硬性约束
指标对象不是定义完就能用的。没注册、错注册、重复注册,都会导致数据缺失或 panic。
-
prometheus.MustRegister()必须在init()或服务启动早期调用,不能放在 handler 或中间件里 - 若用了自定义 registry(如
prometheus.NewRegistry()),后续必须用promhttp.HandlerFor(registry, promhttp.HandlerOpts{}),不能继续用默认promhttp.Handler() - Go 运行时指标(goroutines、GC、内存)需手动注册:
prometheus.MustRegister(prometheus.NewGoCollector()) - 别在
/metricshandler 里调用runtime.ReadMemStats()——每次抓取都触发 GC 扫描,增加抖动


















