OpenTelemetry 的 Tracer 必须绑定已初始化的 SDK 配置,提前全局声明会导致获取 noop tracer 而丢 span;HTTP 中间件需手动解析 traceparent 并注入 context;生产禁用 AlwaysSample,应使用 ParentBased 采样器;跨进程传播必须用 propagator.Inject/Extract,context.WithValue 无效。

为什么 otel.Tracer 不能直接用全局变量存?
因为 OpenTelemetry 的 Tracer 实例必须绑定到当前 SDK 配置(比如采样器、导出器),而 SDK 初始化是异步或延迟生效的。如果在 init() 或包级变量里提前调用 otel.Tracer("my-service"),拿到的可能是默认 noop tracer,后续埋点全丢。
- 正确做法:等
otel.Init(或手动配置sdktrace.NewTracerProvider)完成后再获取Tracer - 常见错误现象:
Span不上报、SpanContext总是空、Jaeger/OTLP 后端收不到任何数据 - 建议封装一个
GetTracer()函数,在main()初始化完 SDK 后才首次调用它
HTTP 中间件里怎么透传 trace context?
Go 标准库的 http.ServeMux 和多数框架(如 Gin、Echo)都不自动处理 traceparent 头。你得手动从 Request.Header 解析并注入到 context.Context,否则下游服务无法续链。
- 必须用
propagation.TraceContext{}.Extract从req.Header拿 context,再用otel.GetTextMapPropagator().Extract(推荐) - 别漏掉
req.URL.Path和req.Method—— 它们该作为Span的 name 和属性,否则所有 HTTP span 都叫HTTP GET - Gin 用户注意:
c.Request.Context()是只读副本,必须用c.Request = c.Request.WithContext(...)覆盖才能让后续 handler 看到新 context
sdktrace.AlwaysSample() 在生产环境为什么危险?
它会让每个请求都生成完整 Span 并尝试导出,不经过任何采样逻辑。小流量还行,一旦 QPS 上千,OTLP exporter 会积压、超时、触发重试风暴,甚至拖垮整个服务的网络和 CPU。
- 生产必须用
sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.1))这类带父子决策的采样器 - 注意:
TraceIDRatioBased(0.01)是按 trace ID 哈希后取 1%,不是每 100 个请求采 1 个 —— 对低频服务可能长期没数据 - 本地调试可用
AlwaysSample,但上线前务必检查TracerProvider构建时是否还开着它
为什么 context.WithValue 不能替代 otel.GetTextMapPropagator().Inject?
因为跨进程传递 trace context 只能靠 HTTP header、gRPC metadata 这类序列化载体。context.WithValue 只在单进程内有效,一发 HTTP 请求出去,下游服务根本看不到你塞进 context 的东西。
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
ctx = context.WithValue(ctx, "trace-id", span.SpanContext().TraceID().String())—— 下游拿不到 - 正确流程:用
propagator.Inject(ctx, carrier)把 context 写入req.Header(carrier 是http.Header实例) - Carrier 类型必须匹配:HTTP 用
propagation.HeaderCarrier,gRPC 用metadata.MD,别混用










