GORM v2 必须使用实现 gorm/logger.Writer 接口的 writer,不能直接传 logrus.Out;需自定义 LogrusWriter 结构体实现 Printf 方法,并设 LogLevel 为 logger.Info 以完整捕获 SQL 日志。

直接用 GORM 的 logger.Config 配合 logrus.Writer
GORM v2 不再接受任意 io.Writer,必须实现 gorm/logger.Writer 接口。logrus 本身不实现这个接口,所以不能直接传 logrus.StandardLogger().Out。你需要包装一层,让 logrus 能被 GORM logger 消费。
常见错误是直接把 logrus.WithFields(...).Infof 塞进 logger.New —— 这会 panic,因为类型不匹配。
- 必须定义一个结构体(比如
LogrusWriter),实现Printf(string, ...interface{})方法 - 该方法内部调用
logrus.Debug或logrus.Info,而非fmt.Printf -
LogLevel建议设为logger.Info:设logger.Error会漏掉 SQL 执行前的准备日志;设logger.Warn以上则看不到参数绑定过程
示例关键片段:
type LogrusWriter struct {
logger *logrus.Logger
}
<p>func (l *LogrusWriter) Printf(format string, args ...interface{}) {
l.logger.Debug(fmt.Sprintf(format, args...))
}</p><p>// 初始化 GORM 时:
newLogger := logger.New(&LogrusWriter{logger: logrus.StandardLogger()}, logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Info,
Colorful: false,
})
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
</p>
为什么不能复用全局 logrus 实例的 Out 字段
logrus.Logger.Out 是 io.Writer,而 GORM v2 的 logger.Writer 是另一个接口,两者方法签名不同:Write([]byte) vs Printf(string, ...interface{})。强行类型断言会失败,运行时报 panic: interface conversion: io.Writer is *os.File, not logger.Writer。
另外,logrus.SetOutput(os.Stdout) 改的是全局输出目标,但 GORM logger 不走这条链路 —— 它只认自己配置的 Writer 实例。
- 别试图用
logrus.StandardLogger().Out = xxx替换底层 writer 来“对接” - 也别在
Printf里调fmt.Fprintln(l.logger.Out, ...):这绕过了 logrus 的 level 控制和字段注入能力 - 如果要用结构化字段(如
sql、rows、duration),得在Printf中解析 format 字符串提取关键信息,再用logrus.WithField输出
如何让 SQL 日志带上下文(trace_id、service_name)
GORM logger 的 Printf 是无上下文的纯字符串回调,不携带 context.Context 或请求 ID。想打结构化日志并注入 trace_id,必须在 Printf 方法内做字符串解析 + 字段提取。
典型日志格式如:SELECT * FROM users WHERE id = ? [1] [1.234ms]。你可以用正则或简单切分拿到 SQL 片段和耗时,再拼进 logrus 字段:
func (l *LogrusWriter) Printf(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fields := logrus.Fields{"component": "gorm"}
<pre class="brush:php;toolbar:false;">if strings.HasPrefix(msg, "SELECT ") || strings.HasPrefix(msg, "UPDATE ") {
// 提取 SQL 和 duration(示例逻辑,需按实际格式调整)
if durMatch := regexp.MustCompile(`\[(\d+\.\d+ms)\]$`).FindStringSubmatch([]byte(msg)); len(durMatch) > 0 {
fields["duration"] = string(durMatch[1:])
}
fields["sql"] = strings.TrimSpace(strings.Split(msg, "[")[0])
}
l.logger.WithFields(fields).Debug(msg)}
- 注意:GORM 默认日志不含 trace_id,你得在 HTTP middleware 或 DB wrapper 层把 context 透传下去,再通过 thread-local(如
goroutine.Local)或全局 map 绑定到当前 goroutine - 更稳妥的做法是改用
bun或自定义gorm.Plugin,在Process阶段拿到context.Context和原始*gorm.Statement - 如果只是调试,用
logger.Info级别 +Colorful: true已足够肉眼识别慢查询
线上环境要关掉 SQL 日志或降级到 Warn
开启 logger.Info 后,每条 SQL 都会触发一次 logrus 调用,高频写入会显著拖慢吞吐。压测时容易观察到 CPU 占用突增、P99 延迟翻倍。
- 生产环境建议设
LogLevel: logger.Warn,只记录慢 SQL(由SlowThreshold控制)和错误 - 绝对不要在
Printf里做文件 I/O、网络调用或复杂正则匹配——这些操作在 SQL 执行路径上,会放大延迟 - 如果必须全量采集 SQL,应走独立通道:用
Plugin拦截Process事件,把 SQL 发给 Kafka 或本地 ring buffer,异步落盘
最常被忽略的一点:GORM 的 logger.Config 是值拷贝,修改后必须重新赋值给 gorm.Config.Logger,否则新配置不生效。











