必须用OpenTelemetry Go SDK+jaegerexporter替代已归档的jaeger-client-go;TracerProvider须在main()开头初始化并校验非nil,Gin需用otelgin.Middleware自动埋点并透传context,否则链路必然断裂。

别用 jaeger-client-go,它已归档;Gin 对接 Jaeger 的唯一可靠路径是 OpenTelemetry Go SDK + jaegerexporter,否则链路必然断在第一跳——不是“没效果”,是 span 根本不生成。
初始化 TracerProvider 必须在 main() 开头完成
常见现象:otel.GetTracerProvider() 返回 nil,后续所有 tracer.Start() 都退化为 noop,Jaeger UI 一片空白。
本质原因:OpenTelemetry 的全局 TracerProvider 是单例,且依赖配置(如 JAEGER_AGENT_HOST、OTEL_EXPORTER_JAEGER_ENDPOINT)就绪。若在 init() 或 handler 里才初始化,配置还没加载,jaeger.NewExporter() 就会失败,otel.SetTracerProvider(tp) 实际没生效。
- 把配置加载(如
viper.ReadInConfig()或os.Getenv())和initTracer()调用放在main()最前面,紧随flag.Parse()之后 -
initTracer()内必须校验tp != nil,并显式调用otel.SetTracerProvider(tp) - 本地调试加一句
log.Printf("tracer provider: %v", otel.GetTracerProvider()),确认输出不是<nil> - 生产环境禁用
stdoutexporter,但开发阶段建议临时加上,直接看 JSON 输出,比等 UI 刷新快得多
Gin 中间件必须用 otelgin.Middleware() 自动埋点
典型表现:Jaeger UI 里只看到单层 Span,下游服务无数据;或 req.Context().Value(otel.TraceContextKey) 为空——不是没写中间件,是 context 没绑定对。
手动在每个 handler 里 StartSpan 容易漏、难维护,且无法对齐请求生命周期。真正有效的入口埋点靠标准中间件自动提取 traceparent 并创建 root span。
- 注册顺序必须在 logger、auth、recovery 等所有中间件之前,否则它们拿不到有效 span
- 必须用
otel.GetTextMapPropagator().Extract(req.Context(), propagation.HeaderCarrier(req.Header))提取上游上下文,别手动读X-B3-TraceId或uber-trace-id - 创建新 span 后,得用
req = req.WithContext(ctx)把带 span 的 context 注入 request,再交给next(c) - 别在
defer span.End()前调用w.WriteHeader()或写 response body,某些中间件(如gin.Recovery())会在 panic 后重写 header,触发 writer closed panic
UDP 上报静默丢包,开发阶段优先切 HTTP Collector
最坑的是:没数据、没错误、没 warning ——jaeger.WithAgentEndpoint() 默认走 UDP,连不上 agent 就直接丢弃,日志里啥也不报。
Docker 环境下,WithAgentHost("localhost") 是错的:容器内 localhost 指自己,得换成 jaeger-agent(docker-compose service 名)。
- 确认
jaeger-all-in-one容器暴露了6831/udp:启动命令必须含-p 6831:6831/udp,且镜像版本 ≥ v1.30(旧版默认监听6832) - 开发阶段优先用
jaeger.WithCollectorEndpoint()+ HTTP,端口14268,失败会明确返回 error,方便定位 - HTTP 方式需确保 Collector 服务可访问,如 docker-compose 中用 service 名而非
localhost - 生产环境若坚持用 UDP,务必检查宿主机防火墙、K8s NetworkPolicy 是否放行 UDP 6831
跨 goroutine 和 DB 调用必须显式透传 context.Context
90% 的“链路只有一跳”问题都出在这里:Go 不自动继承 context,异步任务或 DB 查询没 wrap,span 就断了。
- 异步任务如
go doWork(ctx),ctx必须是带 span 的req.Context(),不能是context.Background() - MySQL:用
otelmysql.Wrap包装*sql.DB,否则db.Query不计入 span - PostgreSQL:同理,用
otelpostgresql.Wrap - 手动
StartSpan时,parent必须来自req.Context(),即span := tracer.Start(ctx, "db.query"),不是tracer.Start(context.Background(), )
复杂点在于:链路验证不能只看 UI 是否有数据,要确认每一跳的 traceparent header 是否真实透传、span 的 parent_span_id 是否非空、gRPC/HTTP client 是否用了对应拦截器——漏掉任一环,链路就是残缺的。


















