必须用 otelsql.open() 替换 sql.open() 并传入原始 driver,否则 trace context 不透传、span duration 错误、事务状态丢失;需配合 context-aware 方法、otelsql.registerdbstatsmetrics、gorm 显式传 ctx 及脱敏处理。

otelsql.Open 必须替换原 driver,不能只包 db.Query
Go 的 database/sql 包本身不感知 trace context,手动在 db.Query() 前后调 tracer.Start()/span.End() 是无效的:span duration 错、事务状态丢失、context 不透传,且无法捕获连接池等待、prepare 失败等底层行为。
正确做法是用 otelsql.Open() 替换 sql.Open(),并传入原始 driver 实例(如 &mysql.MySQLDriver{}):
import (
"database/sql"
"github.com/go-sql-driver/mysql"
"github.com/XSAM/otelsql"
)
<p>db, err := otelsql.Open("mysql", dsn,
otelsql.WithAttributes(semconv.DBSystemMySQL),
otelsql.WithSQLCommenter(), // 可选:把 trace_id 注入 SQL 注释
)
if err != nil {
panic(err)
}
defer db.Close()</p>
-
otelsql.Open()返回的*sql.DB已自动 instrumented,所有QueryContext、ExecContext、BeginTx等方法都会生成 span - 必须使用
Context-aware 方法(如QueryContext),否则 span 无法关联上游 trace - 别漏掉
otelsql.RegisterDBStatsMetrics(db),它会导出连接池指标(db.connections.open、db.connections.idle等)
GORM 用户要显式传 ctx,不能靠 db.WithContext().Exec() 就完事
GORM v2+ 虽然支持 WithContext(),但默认不启用 OpenTelemetry 集成。若只写 db.WithContext(ctx).First(&u),span 会创建,但 SQL 文本、参数、错误堆栈等关键属性不会自动注入。
必须配合 otelsql 的 hook 机制,并确保 GORM 使用的是 instrumented 的 *sql.DB:
gormDB, err := gorm.Open(mysql.New(mysql.Config{
Conn: db, // ← 这里传 otelsql.Open 返回的 *sql.DB
}), &gorm.Config{})
if err != nil {
panic(err)
}
<p>// 查询时必须带 ctx
ctx := r.Context() // 来自 HTTP handler
var user User
err := gormDB.WithContext(ctx).First(&user).Error</p>
- 如果用
gorm.Open()直接连 DSN,GORM 会自己调sql.Open(),绕过otelsql—— 这是常见断链点 - GORM 的
Debug()模式日志里会出现[otel] span started,可快速验证是否生效 - 高基数字段(如
user.email)别直接塞进 span attribute,改用span.SetAttributes(attribute.String("db.statement", safeSQL)),其中safeSQL应脱敏或截断
SQL 执行时间不准?检查 otelsql 是否用了 WithQueryFormatter
默认情况下,otelsql 记录的 db.statement 是原始带问号的 SQL(如 SELECT * FROM users WHERE id = ?),duration 也只统计到 driver 层返回,不包含 GORM 解析、结构体映射等耗时,导致“SQL 很快但接口很慢”的误判。
要获得更贴近真实延迟的数据,启用 WithQueryFormatter:
db, err := otelsql.Open("mysql", dsn,
otelsql.WithQueryFormatter(func(query string, args ...interface{}) string {
// 可选:格式化 + 截断,避免超长 SQL 撑爆 backend
return fmt.Sprintf("%s %v", query, args)
}),
)
- 该回调在 span 创建前触发,所以
db.statement属性会包含实际参数值(慎用于生产,防敏感信息泄露) - duration 仍是 driver 层执行时间;如需端到端 DB 耗时,应在 GORM 外层再套一层 span,覆盖
First()全过程 - 注意:开启后 span attribute 体积显著增大,Jaeger/Tempo 等后端可能限长截断,需提前确认配置
为什么 Span 里看不到 error 信息?RecordError 不是自动的
otelsql 不会自动调 span.RecordError(),即使 db.QueryRowContext() 返回 error,span 也只会标记为 status=Error,但无 stacktrace 或 message。
必须手动补全:
row := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", id)
var name string
err := row.Scan(&name)
if err != nil {
span := trace.SpanFromContext(ctx)
span.RecordError(err) // ← 关键:显式记录
span.SetStatus(codes.Error, err.Error())
return err
}
- 别依赖
span.End()自动处理 error:它只看span.EndOptions,不 inspect error - 若用 GORM,可在
callbacks中统一注入:gormDB.Callback().Query().After("gorm:query").Register(...) - error message 里含用户输入(如 email)时,务必脱敏,否则违反可观测性安全规范
链路中 SQL span 看似完整,但 duration 偏低、error 缺失、statement 不带参数——这些问题几乎都源于没意识到 otelsql 是“增强型 driver wrapper”,而非“自动埋点 SDK”。它需要你主动选择用哪个函数、传什么 context、补哪条 error,稍一跳步,数据就残缺。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











