不能在中间件里用time.Since()直接累加计数器,因路由未匹配、handler panic被recover捕获、自定义HTTPErrorHandler才是请求生命周期终点,仅HTTPErrorHandler能确保所有请求(含4xx/5xx/panic/超时)都被统计。

为什么不能在中间件里用 time.Since() 算耗时后直接累加计数器
看似简单:中间件开头 start := time.Now(),结尾 duration := time.Since(start),再塞进 Prometheus Counter 或 Histogram —— 但这样会漏掉大量真实请求。原因有三:
一是路由未匹配时中间件根本不会执行;二是 handler panic 后被 middleware.Recover() 捕获,但你的中间件早已退出,无法记录这次“失败耗时”;三是如果用了自定义 HTTPErrorHandler(比如统一处理 404/500),它才是请求生命周期的真正终点,而中间件在 panic 恢复后就不再运行。
必须在 HTTPErrorHandler 中读取并上报耗时
HTTPErrorHandler 是 Echo 唯一保证每次请求结束(无论成功、4xx、5xx、panic 还是超时)都会调用的钩子。要拿到准确耗时,就得在这里取值,且前提是:你在更早的中间件中已把开始时间存入 c 上下文。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 先注册一个基础中间件,负责打点起始时间:
e.Use(func(next echo.Handler) echo.Handler { return echo.HandlerFunc(func(c echo.Context) error { c.Set("startTime", time.Now()) return next.ServeHTTP(c) }) }) - 再重写
HTTPErrorHandler,从c.Get("startTime")取值,并确保调用原始错误处理器(避免破坏日志、panic 恢复等逻辑):oldHandler := e.HTTPErrorHandler e.HTTPErrorHandler = func(err error, c echo.Context) { if start, ok := c.Get("startTime").(time.Time); ok { dur := time.Since(start) // 上报到 Prometheus HistogramVec,例如: // reqDurationHist.WithLabelValues(c.Request().Method, normalizePath(c)).Observe(dur.Seconds()) } oldHandler(err, c) }
如何避免耗时指标维度爆炸
直接按 c.Request().URL.Path 打 label 会因带参数路径(如 /user/123、/user/456)导致 label 组合数失控。Prometheus 对高基数 label 极其敏感,可能拖垮采集和查询性能。
- 必须对 path 做归一化:用正则将动态段替换为占位符,例如
/user/:id、/order/:oid/status;不要用strings.ReplaceAll等模糊替换 - 只保留必要 label:
method(GET/POST)、path(归一化后)、status_code(c.Response().Status);禁用RemoteAddr、query、header等任意动态值 - 初始化
HistogramVec时显式声明 label 名:reqDurationHist = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "echo_http_request_duration_seconds", Help: "HTTP request duration in seconds", Buckets: []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5}, }, []string{"method", "path", "status_code"})
注意 c.Response().Status 在 handler panic 时的取值
当 handler panic 被 middleware.Recover() 拦截后,c.Response().Status 默认是 500,但这个值只有在 HTTPErrorHandler 调用前才稳定。如果你在中间件里提前读取,可能拿到 0 或旧值。所以所有指标(状态码、耗时、路径)必须统一在 HTTPErrorHandler 内读取,这是唯一可信时机。别试图在 recover 中间件里做统计——它不覆盖超时、连接中断等场景,也不保证执行顺序。

















