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

不注册 TracerProvider 就调 otel.Tracer,一定返回空 span
这是最常被忽略的“静默失效”:代码能跑、没 panic、日志也没报错,但 Jaeger 里永远看不到 trace_id。根本原因是 otel.Tracer 调用时,底层找不到已注册的 TracerProvider,自动 fallback 到 noop 实现。
必须在 main() 函数最开头完成两件事:
- 创建
sdktrace.NewTracerProvider(带 exporter) - 立即调用
otel.SetTracerProvider注册它
别放在 init() 里——Go 的 init 执行顺序不可控,HTTP server 可能比 provider 先启动;也别在某个 handler 里临时 new,全局只能有一个 provider。
HTTP 中间件必须用 otelhttp.NewHandler,手写 header 解析是错的
自己解析 req.Header.Get("traceparent") + tracer.Start() 看似可行,实则漏掉关键语义:
- server span 缺少
net.peer.ip、http.status_code等标准属性 - 无法自动记录 response 写入时机,span duration 不准
- goroutine 切换后 context 断裂,后续异步逻辑拿不到 parent span
对标准 http.Handler,直接 wrap:http.ListenAndServe(":8080", otelhttp.NewHandler(mux, "my-service"));对 Gin/Echo 等框架,必须用对应插件(如 ginotel.Middleware),否则 span name 会是 GET / 而非真实路由名。
数据库、gRPC、Redis 调用必须用 instrumented client,原生库不埋点
db.Query() 前后手动 span.Start()/span.End() 是典型误区:它只覆盖业务代码执行时间,完全不包含连接池等待、驱动解析、网络往返等真实耗时。
正确做法是使用 OpenTelemetry 官方插件:
- SQL:用
go.opentelemetry.io/contrib/instrumentation/database/sql,注册时调sql.OpenDB(driver),不是sql.Open() - MySQL 驱动必须用
github.com/go-sql-driver/mysql,modernc.org/sqlite 等纯 Go 驱动不兼容 hook 点 - gRPC 客户端/服务端用
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc
所有插件都要求显式传入 context,比如 client.Call(ctx, ...),否则 span 无法关联。
goroutine 和异步任务里 span 断裂,本质是 context 没传下去
HTTP handler 里写 go doWork(context.Background()),就等于主动切断链路——新 goroutine 里的 tracer.Start() 生成的是 root span,trace_id 全 0,parent_span_id 为空。
修复只有一条规则:显式传递带 span 的 context。
- 同步调用:
doWork(r.Context()),不是doWork(context.Background()) - 异步启动:
go doWork(ctx),其中ctx来自 handler 的r.Context() - 第三方回调(如 ent hook、gorm callback)若不支持 context,需手动用
otel.GetTextMapPropagator().Inject()补traceparentheader
Span 生命周期管理不能靠运气:span.End() 必须执行,漏掉会导致 batch exporter 缓冲区堆积、内存缓慢上涨,几小时后可能 OOM。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











