
为什么本地跑不通 TracerProvider 的 span 上报
根本原因通常是 exporter 没配对或网络不通,不是代码写错了。OpenTelemetry Go SDK 默认不自动上报,必须显式注册 Exporter 并启动 TracerProvider;漏掉 resource.WithTelemetrySDK() 或忘记调用 Shutdown() 会导致 span 丢弃但无报错。
- 检查
OTEL_EXPORTER_OTLP_ENDPOINT环境变量是否指向真实可用的 collector(比如http://localhost:4318/v1/traces),别只写localhost:4318 - 用
stdoutexporter快速验证逻辑:替换掉 otlp exporter,看到 JSON 输出就说明 tracer 初始化成功 - Go 程序退出前必须调用
tp.Shutdown(context.Background()),否则最后一批 span 会静默丢失 - 避免在
init()里初始化全局TracerProvider—— 单元测试并发时可能 panic
如何让 HTTP handler 自动注入 trace context
Go 的 net/http 不自带中间件机制,得手动 wrap http.Handler。OpenTelemetry 提供了 otelhttp.NewHandler,但它只处理入向请求,不会自动把 traceID 注入下游 HTTP 调用。
- 入向:用
otelhttp.NewHandler(yourMux, "/")包一层,它会从traceparentheader 解析 context 并注入到request.Context() - 出向:所有下游 HTTP 请求必须用
otelhttp.Transport包装 client.Transport,否则 span 断链 - 别直接用
http.DefaultClient—— 它的 transport 是默认值,不带 trace 注入能力 - 如果用了 Gin/Echo,别信“自动集成”包,90% 都没处理好 context 透传,老老实实用
otelhttp.NewHandler包 router
Span 生命周期管理最容易被忽略的三个点
Go 是基于 context 传递 span 的,不是靠 goroutine 局部变量,一旦 context 丢了,span 就断了,且不会报错。
- goroutine 启动时,必须用
trace.ContextWithSpan把当前 span 注入新 context,不能直接传原始 request.Context() - 数据库查询、RPC 调用等异步操作返回后,要确保仍在同一个 span context 下结束 span,否则会生成孤立 span
- 用
defer span.End()前,确认 span 不是 nil —— 比如在单元测试里没初始化 provider,Tracer.Start()返回的 span 就是 nil
单元测试里怎么验证 trace 是否正确生成
别 mock 整个 OpenTelemetry,用 testtrace 包捕获内存中的 span 数据最稳。它的 SpanRecorder 可以拦截所有 End() 调用,不需要启动 collector 或网络。
立即学习“go语言免费学习笔记(深入)”;
- 测试前创建
sr := &testtrace.SpanRecorder{},传给sdktrace.NewTracerProvider的WithSpanProcessor - 执行业务逻辑后,用
sr.CompletedSpans()拿到所有 span 列表,检查 name、parent span ID、attributes 是否符合预期 - 注意:测试中不要调用
Shutdown(),否则CompletedSpans()返回空切片 - 如果 span 有 child,用
sp.ParentSpanID()和sp.SpanContext().SpanID()手动比对父子关系,别依赖顺序
真正的难点不在集成,而在 context 传递的每一步都得显式做——Go 不像 Java 有字节码增强,也不像 Node.js 有 async_hooks 全局钩子。少一次 context.WithValue 或漏传一个 context,链路就断了,而且断得无声无息。










