不注册tracerprovider就调otel.tracer必返回空span,导致jaeger无trace_id;必须在main()开头创建sdktrace.tracerprovider并立即调otel.settracerprovider注册,且全局唯一、不可放init()或handler中。

不注册 TracerProvider 就调 otel.Tracer,一定返回空 span
这是 Gin 项目接入 OpenTelemetry 后 Jaeger 里完全看不到 trace_id 的最常见原因。代码能跑、没 panic、日志也没报错,但所有 span 都是 noop —— 因为 otel.Tracer 底层找不到已注册的 TracerProvider,自动 fallback 到空实现。
- 必须在
main()函数最开头完成:创建sdktrace.NewTracerProvider(带 Jaeger 或 OTLP exporter),并立即调用otel.SetTracerProvider(tp) - 别放
init()里:Go 的init执行顺序不可控,HTTP server 可能比 provider 先启动 - 别在 handler 里临时 new:全局只能有一个
TracerProvider,重复创建会导致资源泄漏和 span 丢失
otelgin.Middleware 不是加了就生效,依赖三个前提条件
只写 r.Use(otelgin.Middleware("my-service")) 是不够的。Gin 中间件本身不负责导出,它只是生成 span 并注入 context;真正让 span 被采集,靠的是底层 otelhttp 和已注册的 TracerProvider。
-
otel.SetTracerProvider(tp)必须在r.Use(...)之前执行 -
tp必须包含可用的 exporter(比如jaegerexporter或otlphttp.NewExporter),且 endpoint 可连通 - 确认导入的是新版:
go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin,不是已废弃的otelgin.NewMiddleware
数据库和下游 HTTP 调用不会自动埋点,必须显式 wrap
Gin 中间件只覆盖 HTTP 入口,DB 查询、gRPC 调用、Redis 操作等全部不参与自动链路。手写 span.Start()/End() 是典型误区:它只测业务逻辑耗时,漏掉连接池等待、驱动解析、网络往返等真实瓶颈。
- MySQL:用
otelmysql.Wrap(sql.Open()),且驱动必须是github.com/go-sql-driver/mysql - HTTP 客户端:禁用
http.DefaultClient,改用otelhttp.NewClient(&http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}) - gRPC:客户端和服务端都要用
otelgrpc包封装,且所有client.Call(ctx, ...)必须传入带 span 的ctx
goroutine 和异步任务里 span 断裂,本质是 context 没传下去
Go 不自动继承 context,go doWork() 这种写法会让子 goroutine 拿不到 parent span,生成孤立 root span——你在 Jaeger UI 看到的“断链”,90% 是这里漏了。
- 所有异步调用必须显式传
c.Request.Context()(Gin)或req.Context()(net/http) - 不要用
context.Background()或context.TODO()启动新 goroutine - 定时任务、消息队列消费等场景,也要确保从原始请求 context 衍生新 context,而非新建











