OpenTelemetry + Jaeger 是 Golang 微服务分布式追踪的事实标准,jaeger-client-go 已归档;需用 OpenTelemetry SDK 配合 jaeger exporter,避免 UDP 静默丢包、nil pointer dereference、context 传递中断等常见问题,确保全链路追踪可靠落地。

OpenTelemetry + Jaeger 是当前 Golang 微服务分布式追踪的事实标准,jaeger-client-go 已归档,硬用会断链、panic、CI 失败。
Jaeger Agent 连不上?UDP 默认静默丢包
本地或 Docker 环境下,jaeger.WithAgentEndpoint() 默认走 UDP 6831,连不上 agent 不报错、不重试、不 log —— Span 就直接丢了,UI 里空空如也。
常见错误配置:jaeger.WithAgentHost("localhost") 在容器里指向自己,不是 Jaeger Agent 容器。
实操建议:
- 开发阶段优先改用
jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/trace")),HTTP 失败会明确返回 error,方便定位 - Docker Compose 中,
WithAgentHost必须填 service 名(如"jaeger-agent"),不是"localhost" - 确认 jaeger-all-in-one 镜像版本 ≥ v1.30,并启动时暴露 UDP 端口:
-p 6831:6831/udp - 生产环境若坚持 UDP,加
jaeger.WithAgentTimeout(1 * time.Second)避免卡住
TracerProvider 初始化失败导致 span 为 nil
otel.Tracer("xxx").Start(ctx, "yyy") 崩在 nil pointer dereference,根本原因是 otel.GetTracerProvider() 返回 nil —— 没调 otel.SetTracerProvider(tp),或 tp 构造时 exporter 创建失败(比如 Agent 地址错、网络不通)但没检查 err。
实操建议:
- 初始化必须放在
main()开头,不能写在init()里(配置还没加载) - 显式校验:
if tp == nil { log.Fatal("tracer provider is nil") } - 本地调试加一句:
log.Printf("tracer provider: %v", otel.GetTracerProvider()) - ServiceName 必须非空且一致:用
semconv.ServiceNameKey.String(os.Getenv("SERVICE_NAME")),别硬编码或留空
Gin/gRPC/HTTP 客户端上下文传不下去
UI 里只看到单层 Span,下游服务无数据 —— 不是没埋点,是 context 断了。Golang 的 context.Context 不自动跨 goroutine、不自动进 HTTP header、不自动进 gRPC metadata。
实操建议:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- Gin 用
otelgin.Middleware("my-service"),它内部调用propagation.HeaderCarrier提取traceparent,不是uber-trace-id - HTTP 客户端必须用
otelhttp.NewClient(),别包装http.DefaultClient;下游请求会自动 injecttraceparent - gRPC 客户端和服务端都得注册拦截器:
otelgrpc.UnaryClientInterceptor()和otelgrpc.UnaryServerInterceptor() - 跨 goroutine 调用(如
go handleAsync(ctx))必须传ctx,不能只传业务参数
Span 生命周期和嵌套容易踩坑
Span 嵌套过深会让 Jaeger UI 卡死、存储暴涨;粒度太粗又看不出 DB 慢还是缓存慢;defer span.End() 在 panic 分支漏掉,Span 就永远卡在 “running” 状态。
实操建议:
- 入口 Span(如 Gin handler)用
server类型,关键依赖(DB、Redis、下游 HTTP)各自建client类型子 Span - 避免 for 循环里
tracer.Start(),改用一个 Span +span.SetAttributes(attribute.Int("loop.count", i)) - 手动 StartSpan 时,parent ctx 必须来自
req.Context(),禁止用context.Background() - error 处理分支必须显式
span.RecordError(err)+span.End(),别只 defer - 采样率生产环境别用
const=1,改用jaeger.WithSampler(jaeger.NewProbabilisticSampler(0.01))
真正难的不是写几行 tracer.Start(),是确保每个 goroutine、每次 HTTP RoundTrip、每条 gRPC 调用、每个 SQL 执行,都带着同一个 trace context 流转到底 —— 漏一处,链就断。

















