必须用gin.LoggerWithWriter配合gin.LoggerConfig才能自定义日志格式,因gin.Logger()硬编码格式且不接受参数;直接改DefaultWriter仅能换输出位置,无法添加trace_id、拆分Latency、过滤路径或标红错误码。

gin.LoggerWithWriter 是唯一能改日志格式的入口
直接改 gin.DefaultWriter 只能换输出位置,日志内容还是固定模板:[GIN] 200 | 1.2ms | ::1 | GET /path。想加 trace_id、拆开 {{.Latency}}、过滤 /healthz、给 5xx 标红——全做不到。必须用 gin.LoggerWithWriter 配合 *gin.LoggerConfig。
关键点:
-
Format字段必须以\n结尾,否则多条日志会挤在一行 - 时间推荐用
{{.Time}}(带空格),别用{{.StartTime}}(无格式) - 想同时写文件和控制台,用
io.MultiWriter(fileWriter, os.Stdout) - 注册时要显式调用
router.Use(gin.LoggerWithWriter(...)),gin.Default()不会自动加载它
gin.LoggerWithFormatter 要自己实现 LogFormatter 函数
gin.LoggerWithFormatter 不是“配置即生效”的接口,它只是把日志组装逻辑委托给你写的函数。你得实现 func(gin.LogFormatterParams) string,且返回值不能含换行符(gin 自己会加)。
容易踩的坑:
- 状态码颜色要用 ANSI 序列,比如
\033[97;41m标红,末尾必须跟\033[0m重置,否则后续日志染色错乱 -
ClientIP建议调c.ClientIP(),别直接用参数里的字段,避免代理穿透失效 - 时间格式用
start.Format("2006-01-02 15:04:05"),别依赖系统时区;需 UTC 就先start.UTC().Format(...) - 别在 formatter 里调
log.Print或fmt.Println,会导致重复输出或并发冲突
生产环境写文件必须处理轮转和并发安全
只用 os.OpenFile 配 os.O_APPEND 远不够。两个问题会立刻暴露:
- 单个日志文件无限增长 → 必须用
lumberjack.Logger做按大小/时间轮转 - 裸
*os.File并发写不安全 →io.MultiWriter不加锁,得包一层bufio.NewWriter或直接用lumberjack自带缓冲 - 多个微服务实例绝不能共用同一日志路径,否则文件损坏风险极高
示例片段:
cfg := &gin.LoggerConfig{
Format: "{{.Time}} | \033[97;41m{{.Status}}\033[0m | {{.Latency}} | {{.ClientIP}} | {{.Method}} | {{.Path}}\n",
}
writer := &lumberjack.Logger{
Filename: "./logs/access.log",
MaxSize: 100, // MB
MaxBackups: 7,
MaxAge: 28, // days
Compress: true,
}
router.Use(gin.LoggerWithWriter(io.MultiWriter(writer, os.Stdout), cfg))
状态码和耗时必须在 c.Next() 后准确捕获
直接读 c.Writer.Status() 很危险:如果前面挂了 gin.Recovery() 或 gin.BasicAuth(),拿到的可能是 500 或 401,而非业务真实返回码。
可靠做法是包装 c.Writer,拦截 WriteHeader:
- 在中间件开头创建自定义
responseWriter,嵌入gin.ResponseWriter - 重写
WriteHeader(int),只记录首次调用的状态码 - 耗时用
time.Since(start),start必须是time.Now()(非UTC()),否则纳秒级误差影响Latency精度 - 确保日志中间件注册顺序在
Recovery和鉴权中间件之后
最复杂的不是格式,而是状态码来源是否可信——这点在微服务链路中一旦出错,排查成本翻倍。











