必须显式调用gorm.WithOpentelemetry()并配合otelsql.InjectDriver注册底层驱动,否则SQL span仅覆盖业务层执行时间,遗漏连接池等待、驱动解析及网络往返等真实瓶颈。

不启用 GORM 的 OpenTelemetry 支持,或未正确配置底层 driver,所有 SQL span 都是空壳——它只测业务层执行时间,完全漏掉连接池等待、驱动解析、网络往返等真实瓶颈。
如何启用 GORM v1.25+ 内置的 OpenTelemetry 支持
GORM 自 v1.25 起提供原生 OTel 集成,但它是 opt-in 的,**默认关闭**。必须显式传入 gorm.WithOpentelemetry() 才会注册 instrumented callback。
常见错误是只初始化了全局 TracerProvider,却忘了在 gorm.Open() 时启用:
- ❌ 错误写法:
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) - ✅ 正确写法:
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{Plugin: []gorm.Plugin{opentelemetry.NewPlugin(opentelemetry.WithTracerProvider(tp))}})(注意:实际应使用gorm.WithOpentelemetry(),不是手动插 plugin)
更推荐的初始化方式(GORM 官方文档推荐):
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
panic("failed to connect database")
}
// 必须显式启用
db = db.Session(&gorm.Session{Context: context.Background()}) // 确保 session 有 context 基础
db.Use(opentelemetry.NewPlugin(opentelemetry.WithTracerProvider(tp)))
⚠️ 注意:opentelemetry.NewPlugin 来自 go.opentelemetry.io/contrib/instrumentation/github.com/go-gorm/gorm/otelgorm,不是 GORM 自带模块,需额外引入。
为什么 GORM 的 OTel 插件仍依赖 otelsql 注入 driver
GORM 的 OTel 插件只负责 hook 到 GORM 的 callback(如 Process, AfterFind),**它不接管底层 SQL 执行链路**。真正的连接获取、参数绑定、网络 IO 仍在 database/sql 层。
所以即使启用了 otelgorm,若没用 otelsql.InjectDriver 包装 MySQL driver,你看到的 span 耗时依然严重偏低。
必须组合使用:
- 用
otelsql.InjectDriver("mysql", &mysql.MySQLDriver{})替换原始 driver - 再用
sql.OpenDB(otelsql.InjectDriver(...))创建*sql.DB - 最后将该
*sql.DB传给gorm.Open()(而非 DSN 字符串)
否则 otelgorm 生成的 span 名是 SELECT FROM users,但 duration 只包含 GORM 结构体映射时间,net.peer.ip、db.statement 等关键属性也缺失。
Span name 退化与高基数问题
GORM + otelsql 默认按原始 SQL 生成 span name,比如 SELECT * FROM users WHERE id = ? 和 SELECT * FROM users WHERE id = 123 会被视为两个不同 span,导致 backend(如 Tempo/Jaeger)因高基数崩溃或查询变慢。
解决方法是统一命名 + 脱敏 SQL:
- 用
otelsql.WithSpanNameFormatter固定 span 名,例如"gorm.Query" - 用
otelsql.WithAttributesFromSQL控制是否记录完整 SQL(生产环境建议关掉) - 避免在 span 中注入敏感字段,如
user_id、email—— 这些应作为 span event 或 log 记录,而非 attribute
示例配置:
driver := otelsql.InjectDriver(
"mysql",
&mysql.MySQLDriver{},
otelsql.WithSpanNameFormatter(func(query string) string { return "gorm.Query" }),
otelsql.WithAttributesFromSQL(false),
)
db, _ := sql.OpenDB(driver)
gormDB, _ := gorm.Open(mysql.New(mysql.Config{Conn: db}), &gorm.Config{})
goroutine 中 GORM 查询丢失 parent span 的根本原因
典型翻车场景:http.Handler 里起 go func() { db.First(&u) },结果这个查询 span 总是 root,trace_id 全 0。
这不是 GORM 的锅,而是 Go context 不跨 goroutine 自动传播。GORM 的 OTel 插件从 db.Session.Context() 提取 span,而你传进去的是 context.Background()。
必须显式传递带 span 的 context:
- 在 handler 中:用
r.Context()创建带 span 的 session:db.WithContext(r.Context()).First(&u) - 在 goroutine 中:不能直接
go db.First(),得go func(ctx context.Context) { db.WithContext(ctx).First(&u) }(r.Context()) - 若用
db.Session(&gorm.Session{Context: r.Context()}),也要确保该 session 被传进 goroutine
漏掉这一步,所有异步 GORM 查询都脱离链路——它能跑、不 panic、日志里甚至有 span,但 trace_id 是全 0,Jaeger 里查不到关联。
最易被忽略的点:GORM 的 OTel 插件和 otelsql driver 是两层,缺一不可;且二者都依赖你在 main() 开头已正确注册 TracerProvider 和 TextMapPropagator。漏掉任一环,span 就静默失效,连 debug 日志都不会打。


















