
Go 服务接入 OpenTelemetry 的最小可行配置
不写一堆 exporter 和 propagator 就跑不起来,但也不用一上来就配 Jaeger + Prometheus + Logging 全链路。先让 Tracer 和 Meter 能打日志、能导出 span,才是关键。
常见错误是直接 copy 官方 example,结果 otel.Tracer 返回 nil 或 span 总是 spancontext: invalid——本质是没调用 otel.SetTracerProvider,或者 provider 初始化失败后被静默忽略。
- 必须在
main()开头就初始化sdktrace.NewTracerProvider,并用otel.SetTracerProvider注册,否则所有Tracer().Start()都走默认 noop 实现 - 本地调试优先用
stdoutexporter(不是loggingexporter),它会把 span 结构体 JSON 打到终端,比查日志快得多:stdoutexporter.NewUnstarted()+.Start()显式触发 - 别在 init 函数里初始化 OTel,Go 的 init 执行顺序不可控,容易导致 tracer provider 还没 ready,HTTP handler 就已经开始处理请求了
HTTP 中间件自动注入 trace context 的正确姿势
手动在每个 handler 里写 otel.GetTextMapPropagator().Extract 不现实,但用第三方中间件(比如 otelmux 或 otelchi)又容易和自定义的 request ID、log correlation 冲突。
核心矛盾在于:OpenTelemetry 的 propagation.HTTPTraceContext 只处理 traceparent,而很多旧系统或前端 SDK 用的是 b3 或 uber-trace-id。不兼容就断链。
立即学习“go语言免费学习笔记(深入)”;
- 用
otel.GetTextMapPropagator()前,先设好复合 propagator:otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.B3{})) - 中间件里提取 context 后,务必用
otel.GetTextMapPropagator().Inject把 context 写回 response header,否则下游服务收不到 trace 上下文 - 别依赖
r.Context()直接传入 span——要显式用trace.SpanFromContext(r.Context())拿当前 span,再用span.AddEvent记录请求路径、状态码等,否则 span 里只有空壳
避免 Go runtime goroutine 泄漏的 span 生命周期管理
Go 里最隐蔽的坑:异步任务(比如 go func() { ... }())里创建的 span 没 close,导致 span 数据卡在 batch exporter 缓冲区,内存缓慢上涨,几小时后 OOM。
OpenTelemetry 的 Span.End() 不是可选操作——它触发采样判定、属性合并、事件 flush,漏掉就等于丢数据,还拖慢整个 exporter。
- 所有
Tracer.Start()必须配对span.End(),哪怕在 defer 里也得确保执行;用defer span.End()是底线,不是最佳实践 - goroutine 内部开启 span 时,别把父 context 直接传进去——要用
trace.ContextWithSpan(context.Background(), parentSpan)构造新 context,否则父子 span 时间线错乱 - 数据库查询、HTTP client 调用这些外部依赖,优先用官方 instrumented 包(如
go.opentelemetry.io/contrib/instrumentation/database/sql),它们已内置 span 生命周期管理,比自己 wrap 更可靠
本地开发与生产环境 exporter 的切换陷阱
本地跑 stdoutexporter 很爽,但上线切到 jaegerexporter 或 otlphttp 时,常出现 “no spans exported” 或连接超时——问题往往不在 endpoint 配置,而在 TLS 和 timeout。
otlphttp.NewExporter 默认启用了 TLS,而本地 Jaeger 或 collector 若没配证书,就会静默失败(连 error 都不返回)。更麻烦的是,默认 timeout 是 5 秒,K8s 网络抖动时直接丢 span。
- 生产环境用
otlphttp.NewExporter时,显式关 TLS:otlphttp.WithInsecure(),或配好otlphttp.WithTLSCredentials - 务必设置
otlphttp.WithTimeout(30 * time.Second),短于这个值,batch exporter 在网络波动时大概率丢数据 - 不要在代码里硬编码 exporter 地址,用环境变量驱动:
os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"),配合 Go 的otel/sdk/trace的WithSyncer/WithBatcher组合灵活替换
链路监控不是加几个 SDK 就完事,真正卡住人的永远是 context 传递的断裂点、span 生命周期的遗漏、以及 exporter 在不同网络环境下的行为差异——这些地方没日志、不报错、只悄悄丢数据。










