必须在main()开头注册全局TracerProvider并设置propagator,否则所有span为nil且无任何错误提示;ginotel.Middleware需在路由注册前使用;出向HTTP和数据库调用须分别用otelhttp.Transport和otelsql.InjectDriver。

不注册 TracerProvider 就直接用 ginotel.Middleware,所有 span 都是 nil,Jaeger 里完全看不到 trace —— 这不是配置没生效,是链路根本没启动。
main() 第一行必须注册全局 TracerProvider
Go 的 otel.Tracer() 永远不会 panic,但若没提前设置 provider,它返回的 span 永远为 nil。静默失效,无报错、无日志、无 trace_id。
- 必须在
main()函数最开头(甚至早于gin.Default())创建并注册:sdktrace.NewTracerProvider(...)→otel.SetTracerProvider(tp)→otel.SetTextMapPropagator(propagation.TraceContext{}) - 别放
init():单元测试并发调用时可能 panic;别在 handler 或中间件里 new:会导致多个 provider 冲突、采样错乱、exporter 连接泄漏 - 注册后必须显式调用
otel.SetTextMapPropagator,否则traceparentheader 无法被正确提取和传播
ginotel.Middleware 必须在路由注册前加载
如果把 ginotel.Middleware 放在 r.GET(...) 之后,或嵌套在其他自定义中间件内部,span 名称会退化成 HTTP GET,丢失真实路由路径(如 /api/users/:id)。
- 正确顺序:
r := gin.Default()→r.Use(otelgin.Middleware("my-service"))→ 再注册r.GET等路由 - 不要同时启用
otelhttp.NewHandler和ginotel.Middleware:会导致双 span,且上下文覆盖混乱 - 若需自定义 span 名称,用
otelgin.WithSpanNameFormatter,例如func(c *gin.Context) string { return "Gin:" + c.FullPath() }
出向 HTTP 调用必须用 otelhttp.Transport
入向用了 ginotel.Middleware,但出向还用原始 http.Client,链路只有一跳 —— 下游服务收不到 traceparent,span 直接断裂。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 构造 client 时必须替换 transport:
&http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)} - 千万别直接赋值
http.DefaultClient = &http.Client{...}:会影响其他未 instrument 的库(比如某些 SDK 内部 HTTP 调用) - 若使用
resty或gqlgen等封装库,需确认其底层是否透传了 context;否则要手动 wrap transport 或注入 propagator
数据库调用不能靠手写 StartSpan/End
在 db.Query() 前后手动起 span,只测了 SQL 执行时间,完全忽略连接池等待、驱动解析、参数绑定、网络往返等关键耗时。
- 必须用官方插件:
sql.OpenDB(otelsql.InjectDriver("mysql", &mysql.MySQLDriver{})),而不是sql.Open() - MySQL 驱动必须是
github.com/go-sql-driver/mysql;modernc.org/sqlite等纯 Go 驱动不兼容 hook 点 - 敏感字段(如 SQL 中的 token、user_id)默认不脱敏,生产环境应配
otelsql.WithSpanNameFormatter固定 span 名,避免 span attribute 泄露
最容易被忽略的点:所有组件(HTTP 入口、出向 client、DB 驱动)都依赖同一个 TracerProvider 实例。provider 创建时传的 sampler、exporter、resource,决定了整个链路的行为边界 —— 换句话说,漏掉任意一环,链路就断在那一跳,而且你很难察觉。

















