必须在HTTPErrorHandler中统计耗时,因中间件漏统计404、panic、超时等场景;需中间件存startTime到ctx,HTTPErrorHandler中读取并上报,且路径须归一化避免高基数问题。

为什么不能只在中间件里用 time.Since() 记耗时
因为耗时指标会漏统计:路由未匹配(404)时中间件根本不执行;handler panic 被 middleware.Recover() 捕获后,中间件早已退出;自定义 HTTPErrorHandler 才是请求生命周期的终点——它会处理所有 4xx/5xx、panic、超时等场景。只在中间件里记时间,等于把真实请求量按“成功走完中间件链”来统计,和 Prometheus 里看到的 http_requests_total 对不上。
必须把开始时间存进 c 上下文,并在 HTTPErrorHandler 中读取
中间件负责埋点,HTTPErrorHandler 负责收尾。两步缺一不可:
- 在全局中间件中调用
c.Set("startTime", time.Now()),确保所有请求(含 404)只要进入路由树就打上起点 - 重写
e.HTTPErrorHandler,从c.Get("startTime")取值,再调time.Since(start)算耗时 - 必须保留原 handler 行为:
oldHandler(err, c)不能省,否则日志、panic 恢复、响应写入都会丢失
示例代码片段:
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Set("startTime", time.Now())
return next(c)
}
})
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)
}
路径 label 必须归一化,否则 Prometheus 会崩
直接用 c.Request().URL.Path 打 label,会导致 /user/123、/user/456 这类路径生成无限多 label 组合,Prometheus 称之为“高基数问题”,采集卡顿、查询超时、内存暴涨都是常态。
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
- 必须用正则将动态段替换为占位符,例如
/user/:id、/order/:oid/status - 禁用
strings.ReplaceAll这类模糊替换,它可能把/user/123abc错误归一为/user/:idabc - 只保留三个 label:method(
GET/POST)、path(归一化后)、status_code(c.Response().Status) - 绝对不要带
RemoteAddr、query、Authorization、Cookie等任意动态值
耗时统计要和真实响应完成时刻对齐
中间件里 next(c) 返回 ≠ 响应已写出。如果 handler 内部 panic 后被 recover,或客户端提前断连导致 Write() 失败,仅靠中间件计时会虚高且无法反映用户实际等待时间。
- 真正可信的终点是
ResponseWriter完成Write()或WriteHeader()的那一刻 - 标准解法是包装
c.Response().Writer,在Write()和WriteHeader()中更新状态码和字节数,并标记“已写出” -
HTTPErrorHandler中的耗时,只有配合这种包装 writer 才能和 status、size 严格对齐
归一化路径函数和包装 writer 的实现细节容易被跳过,但它们才是让指标可查、可信、不拖垮监控系统的实际门槛。









