请求日志中间件怎么做(含耗时与状态码)
结论先行
核心套路是“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 |
| 客户端 IP | c.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 后日志。