必须用 gin.LoggerWithWriter + gin.LoggerConfig 才能自定义日志格式,因 gin.Logger() 内部硬编码格式且不接受参数,仅替换 DefaultWriter 无法修改内容,无法添加 trace_id、拆分 Latency、过滤路径或标红错误码。

直接改 gin.DefaultWriter 不够用,真正要控制日志格式,必须用 gin.LoggerWithWriter + gin.LoggerConfig;否则你看到的还是固定模板的 [GIN] 200 | 1.2ms | ::1 | GET /path。
为什么 gin.Logger() 不能自定义格式
gin.Logger() 是个封装好的中间件,内部硬编码了日志格式字符串,不接受任何参数。它只负责调用 gin.LoggerWithWriter 并传入默认的 gin.DefaultWriter 和一个写死的 Format 字段。你替换 DefaultWriter 只能改输出目标,改不了内容。
- 想加 trace_id、user_id、request_id?做不到
- 想把
{{.Latency}}拆成毫秒数和单位分开记录?做不到 - 想过滤掉健康检查路径(如
/healthz)的日志?做不到 - 想让错误状态码(
5xx)自动标红或加前缀?做不到
用 gin.LoggerWithWriter 替换默认中间件
这是 Gin 提供的唯一支持格式定制的公开接口。你需要手动注册它,而不是依赖 gin.Default() 自带的 Logger()。
- 必须显式调用
router.Use(gin.LoggerWithWriter(...)),不能靠gin.Default()自动注入 -
gin.LoggerWithWriter第一个参数是io.Writer,第二个是可选的*gin.LoggerConfig -
Format字段必须以\n结尾,否则多条日志会挤在同一行 - 时间字段推荐用
{{.Time}}(带空格),避免和后续字段粘连;别用{{.StartTime}},它不包含格式化信息
示例:
cfg := &gin.LoggerConfig{
Format: "{{.Time}} | {{.Status}} | {{.Latency}} | {{.ClientIP}} | {{.Method}} | {{.Path}} | {{.RequestBody}}\n",
}
router.Use(gin.LoggerWithWriter(io.MultiWriter(fileWriter, os.Stdout), cfg))
生产环境绕不开的三个坑
格式只是第一步,落地到微服务场景,以下三点不处理好,日志很快失控:
-
os.OpenFile必须带os.O_APPEND,否则每次重启都会覆盖旧日志;但仅这样还不够,得用lumberjack.Logger做轮转,否则单个文件无限增长 - 并发写文件时,裸
*os.File不是线程安全的——io.MultiWriter本身不加锁,必须包一层bufio.NewWriter或用lumberjack自带缓冲 - 微服务里多个实例共用同一日志路径?别这么干。每个实例日志文件名应含 hostname 或 pod id,例如
access-$(hostname).log,否则日志混杂无法归因
结构化日志不是可选项,是微服务上线前提
文本日志在单体应用里还能 grep,到了微服务集群里,grep -r "500" ./logs/ 已经失去意义。ELK、Loki、Grafana 都依赖 JSON 字段做聚合与告警。
- 别在
Format里拼 JSON 字符串(比如"{\"status\":{{.Status}}}"),容易引号逃逸出错;改用 zap/zapcore + 自定义中间件更稳 - 如果坚持用
gin.LoggerWithWriter,至少把关键字段对齐:统一用{{.Time}}(ISO8601)、{{.Status}}(数字)、{{.Latency}}(自带单位),方便后续正则提取 - 调试阶段可以保留颜色输出(
gin.ForceConsoleColor()),但生产环境必须关掉——ANSI 转义符会让 JSON 解析失败
最常被忽略的是:日志格式定了,但没配好采样率。高频探针请求(如 k8s /readyz)不该全量记,否则有效信息全被淹没。这层过滤必须在中间件里做,不能靠日志系统后期丢弃。


















