最稳妥方式是用gin.HandlerFunc写中间件:Next()前记录开始时间,Next()后读c.Writer.Status()和耗时,注意Status()必须在Next()后调用否则为0,panic时需配合recovery或判c.Errors。

怎么在 Gin 中拦截并记录每个请求的耗时和状态码
直接用 gin.HandlerFunc 写中间件最稳妥,别试图改写 Engine.ServeHTTP 或劫持 ResponseWriter 的底层接口——Gin 的 ResponseWriter 是封装过的,直接类型断言容易 panic。
核心逻辑是:在 Next() 前记开始时间,之后读取 c.Writer.Status() 和 c.Writer.Size(),再算耗时。注意 Status() 必须在 Next() 之后调用,否则返回 0。
-
c.Writer.Status()只有在写响应头后才有效,Gin 默认延迟写头,所以必须等Next()返回 - 如果 handler panic 了,
Status()会是 0,得靠recovery中间件配合或手动捕获 - 耗时单位建议用
time.Since(start).Microseconds(),避免浮点数或纳秒级数字过大
func monitorMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
status := c.Writer.Status()
cost := time.Since(start).Microseconds()
log.Printf("method=%s path=%s status=%d cost=%dμs size=%d",
c.Request.Method, c.Request.URL.Path, status, cost, c.Writer.Size())
}
}
如何避免日志刷屏或丢失关键字段
高频接口(比如健康检查 /health)打太多日志会淹没真实问题,而错误请求又可能因 panic 没走到中间件末尾,导致状态码和耗时为空。
- 对特定路径(如
/metrics、/health)加白名单或黑名单,用strings.HasPrefix(c.Request.URL.Path, "/health")快速跳过 - 用
c.Errors判断是否有未处理错误:if len(c.Errors) > 0 { status = 500 },比只依赖Status()更可靠 - 把
c.ClientIP()、c.GetHeader("User-Agent")也记进去,但注意移动端 UA 可能超长,建议截断前 128 字符
Gin 的 Abort() 和重定向会影响监控结果吗
会。调用 c.Redirect() 或 c.Abort() 后,后续中间件不执行,但当前中间件的 c.Next() 已返回,Status() 是 302 或你设的值;而 c.AbortWithError(401, "xxx") 会设状态码并终止,Status() 可正常读取。
立即学习“go语言免费学习笔记(深入)”;
-
c.Redirect(http.StatusFound, "/login")→Status()是 302,Size()通常为 0(没写响应体) -
c.Abort()单独调用不会改状态码,Status()还是 0,得配c.Status(403)才能被监控捕获 - 想统一处理所有中断路径,建议在中间件末尾补一句:
if status == 0 { status = 500 }
要不要把监控数据发到 Prometheus 或写入本地文件
看场景。Prometheus 需要暴露 /metrics 端点并用 promhttp,Gin 本身不带指标收集能力,得引入 github.com/prometheus/client_golang 并自己注册 CounterVec 和 HistogramVec;写文件则要注意并发写和 rotate。
- 用
prometheus.NewHistogramVec记耗时,标签设[]string{"path", "method", "status"},别加query参数,不然 label 爆炸 - 本地文件推荐用
lumberjack.Logger做轮转,MaxSize设 100MB,MaxBackups不超过 5,避免磁盘打满 - 如果只是调试,用
log.SetOutput(os.Stdout)+ JSON 格式更省事,别一上来就搞远端上报
真正难的不是埋点,而是区分“慢是业务逻辑卡顿”还是“下游依赖超时”,这得靠在 handler 里分段打点,而不是只靠入口中间件。


















