Gin默认日志中间件无法打印请求体和响应体,因c.Request.Body为单次读取流且ResponseWriter被封装;需重置Body或包装ResponseWriter,并按Cors→Auth→Logger→Recovery顺序注册中间件。
为什么 Gin 默认日志中间件打不出请求体和响应体
因为
是单次读取的
流,日志中间件一读就空了,后续业务代码调用
会报
;而
被 Gin 封装过,直接读会 panic 或丢内容。
别在日志中间件里调用
或
—— 它们会消费 Body
要记录请求体,得先用
拿到原始字节,再用
和
把 Body 重置回去
记录响应体必须包装
(比如自定义
结构体),拦截
和
,并在
后读缓冲区
生产环境建议只开 debug 模式才记录 body:
怎么用 Zap 替换默认 logger 实现高性能结构化日志
默认
只输出文本,不支持字段、级别过滤、日志切割,Zap 的
更适合开发调试,
更适合高吞吐服务。
安装:
初始化时选对类型:性能敏感用
,开发调试用
中间件中记录字段必须显式传参:
别忘了
,否则进程退出时缓冲区日志会丢失
日志中间件顺序放错会导致 panic 不留痕迹或鉴权失效
中间件执行是链式入栈 + 出栈返回,注册顺序决定逻辑依赖关系。日志中间件如果放在
前面,panic 发生时日志根本没刷出来;如果放在鉴权后面,未授权请求也会被记日志,还可能因
返回 nil 导致 panic。
正确顺序(从左到右):
→
→
→
中必须用
显式存值,下游用
判断,不能只靠非空断言
避免全局
,改用路由组控制范围:
怎么写一个带耗时统计和 IP 标识的轻量日志中间件
不需要引入 Zap 也能快速写出实用日志,关键是把时间戳、状态码、路径、IP、耗时这五项打全,并控制好 body 读取时机。
立即学习
“
go语言免费学习笔记(深入)
”;
用
拿真实 IP(注意配置
防伪造)
计时用
+
+
状态码必须从
读(不是
),因为
不会触发 WriteHeader
简单示例:
实际项目里最常漏掉的是
的时机判断和
的可信代理配置,这两处一错,日志就失去可观测性。
c.Request.Bodyio.ReadCloserc.ShouldBindJSON()invalid characterhttp.ResponseWriterc.ShouldBindJSON()c.PostFormioutil.ReadAll(c.Request.Body)bytes.NewBuffer(bodyBytes)ioutil.NopCloserResponseWriterresponseWriterWrite()WriteHeader()c.Next()if gin.Mode() == gin.DebugModegin.Logger()SugaredLoggerLoggergo get -u go.uber.org/zapzap.NewProduction()logger.Sugar()logger.Info("request", zap.String("path", c.Request.URL.Path), zap.Int("status", c.Writer.Status()))defer logger.Sync()Recoveryc.Get("user")Cors()Auth()Logger()Recovery()Authc.Set("user", user)user, ok := c.Get("user")r.Use()api := r.Group("/api").Use(auth, logger)c.ClientIP()TrustedProxiesstart := time.Now()c.Next()cost := time.Since(start)c.Writer.Status()c.Writer.Written()c.AbortWithStatus()func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
cost := time.Since(start)
log.Printf("[GIN] %s %s %d %v %s",
c.Request.Method,
c.Request.URL.Path,
c.Writer.Status(),
cost,
c.ClientIP())
}
}c.Writer.Status()ClientIP()