gin的logger()不满足慢日志需求,因其无耗时阈值判断、不标记慢请求、不采集路由参数、查询参数、错误堆栈等关键上下文;自定义slowlog中间件需在c.next()前后计时,超阈值时结构化输出path、method、latency、status、query、error及c.param、c.getquery、c.keys、c.errors等字段,并异步写入防性能反模式。

为什么 Gin 的 Logger() 中间件不满足慢日志需求
因为 Logger() 默认只记录全部请求的耗时,没有阈值判断,也不会对慢请求做额外标记或分类;它不区分 50ms 和 2s 的请求,更不会自动采集上下文(如路由参数、查询参数、响应状态码、错误堆栈)——而这些恰恰是排查慢请求的关键信息。
如何用自定义中间件实现带阈值和上下文的 Slow Log
核心是拦截请求开始和结束时间,对比阈值,并在超时时主动收集并输出结构化日志。关键点不是“记录所有”,而是“只在慢时深挖”:
- 用
time.Now()记录入口时间,time.Since()算耗时,避免依赖gin.Context.WriterSize()等不可靠指标 - 阈值建议设为
300 * time.Millisecond,太低会刷屏,太高会漏掉真实瓶颈(比如数据库查询卡在 400ms) - 必须在
c.Next()后读取c.Errors和c.Writer.Status(),否则拿不到实际响应状态和中间件抛出的错误 - 推荐用
zap或log/slog输出结构化日志,字段至少包含:path、method、latency、status、query、error
示例片段:
func SlowLog(threshold time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
latency := time.Since(start)
if latency > threshold {
log.Printf("[SLOW] %s %s %v %d %v",
c.Request.Method,
c.Request.URL.Path,
latency,
c.Writer.Status(),
c.Request.URL.Query())
}
}
}
哪些字段容易被忽略但对定位慢因至关重要
光有耗时和路径远远不够。以下字段一旦缺失,90% 的慢日志会变成“已知慢,但不知为何慢”:
-
c.Param("id")和c.GetQuery("page"):动态路由和分页参数直接影响 DB 查询性能,不记录就无法复现 -
c.Keys(如c.Keys["user_id"]):如果鉴权中间件注入了用户 ID,慢请求是否集中在某类用户?需一并打点 -
c.Errors.ByType(gin.ErrorTypePrivate):框架内部错误(如 JSON 解析失败)可能阻塞后续逻辑,但不会改变 HTTP 状态码 -
runtime.Caller(1):可选,用于定位是哪个 handler 函数拖慢了整条链路(尤其在嵌套路由或 group 中)
并发高时写日志引发的性能反模式
慢日志本身不能成为新瓶颈。常见踩坑点:
- 直接用
fmt.Printf或log.Println:在 QPS > 500 场景下,I/O 阻塞明显,反而拉长整体延迟 - 每条慢日志都调用
runtime.Stack():开销极大,仅在 debug 模式下开启 - 未限制日志频率:同一路径连续慢请求反复刷屏,掩盖真正异常点。可用简单滑动窗口(如 1 分钟内同 path 最多记 5 条)限流
- 把慢日志写进和业务同一线程池:应确保日志异步落盘(如
zap.L().With(...).Info()配合zap.AddSync(os.Stderr)已足够,无需自己 goroutine)
最稳妥的做法是:只在超阈值时采样关键字段 + 异步结构化输出,不尝试“全量捕获”,而要“精准快照”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











