请求日志中间件怎么做(含耗时与状态码)

结论先行

核心套路是“Next 前后包夹”:中间件先记录开始时间,c.Next() 跑完整条链后,再读 c.Writer.Status()(最终状态码)、c.Writer.Size()(响应字节数)、算 time.Since(start) 耗时,拼上 method/path/ClientIP 输出。Gin 内置的 gin.Logger() 就是基础版;生产往往自写,换成结构化日志并加 request-id。

字段与来源

字段取法
状态码c.Writer.Status()(未显式写过响应时返回 200 语义值)
响应大小c.Writer.Size()
耗时c.Next() 之后 time.Since(start)——才包含全部 handler
客户端 IPc.ClientIP()(注意代理信任配置,见第 25 章)
请求错误遍历 c.Errors(Next 后已收集完毕)
链路 ID中间件生成 X-Request-Id,写入响应头与日志

代码示例

func AccessLog() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        reqID := uuid.NewString()
        c.Header("X-Request-Id", reqID)
        c.Set("req_id", reqID)

        c.Next() // 跑完整条链

        attrs := []any{
            "req_id", reqID,
            "method", c.Request.Method,
            "path", c.Request.URL.Path,
            "status", c.Writer.Status(),
            "latency_ms", time.Since(start).Milliseconds(),
            "bytes", c.Writer.Size(),
            "ip", c.ClientIP(),
        }
        if len(c.Errors) > 0 {
            slog.Error("http", append(attrs, "errors", c.Errors.String())...)
            return
        }
        slog.Info("http", attrs...)
    }
}

func main() {
    r := gin.New()
    r.Use(gin.Recovery())
    r.Use(AccessLog()) // 注意顺序:Recovery 最外,日志次之,可记录到 4xx/5xx
    // 业务路由...
}

注意点

  • 计时必须放 Next 之后:只记 handler 前的时间等于没记。
  • 别把请求 body/响应体打进日志:含敏感数据且放大磁盘;真要采样调试再单独处理。
  • 高流量接口做采样或只记慢请求/错误,避免日志本身成为性能瓶颈。
  • 全局日志中间件对未匹配请求(404)同样生效(Gin 全局中间件覆盖 NoRoute 链,以官方行为为准)。

常见追问 / 记忆点

  • 追问:状态码在 handler 没写时是多少?答:Gin 的 Writer 默认状态码为 200,handler 结束时若没显式写,响应就是 200。
  • 追问:为什么需要 request-id?答:把网关、业务日志、错误日志串成一条链路,排障不用大海捞针。
  • 记忆点:Next 后取 status/size/latency + 结构化输出 + request-id;先 Recovery 后日志。
笔记加载中…