span注入本质是透传context.context而非仅写traceid,需在每次跨goroutine或网络调用时显式调用propagator.inject()和extract(),否则trace断裂。

Span 注入的本质是把 context 传下去,不是手动塞 traceID
Go 里做 Span 注入,核心不是“往 HTTP header 或 RPC metadata 里写字符串”,而是把带 context.Context 的 span 透传下去。OpenTelemetry 的 otel.GetTextMapPropagator() 负责序列化/反序列化,但真正起作用的是你是否在每个调用点都用了 propagator.Inject() 和 propagator.Extract() —— 否则 trace 会在第一个跨 goroutine 或网络跳转处断掉。
HTTP 客户端请求前必须 Inject,否则下游收不到 trace 上下文
常见错误是只在服务端 Extract,却忘了客户端发起请求时要 Inject。比如用 http.Client 发请求前,得把当前 span 的 context 注入到 req.Header:
ctx := context.Background() // 假设已有 active span span := trace.SpanFromContext(ctx) ctx = trace.ContextWithSpan(ctx, span) propagator := otel.GetTextMapPropagator() req, _ := http.NewRequestWithContext(ctx, "GET", "http://upstream/api", nil) propagator.Inject(ctx, propagation.HeaderCarrier(req.Header))
- 必须用
propagation.HeaderCarrier(req.Header),不能直接写req.Header.Set(...)—— propagator 会按 W3C TraceContext 标准写traceparent和tracestate - 如果用了自定义 HTTP transport(比如带重试、超时封装),Inject 必须放在最终发出请求前的那一次,不是第一次构造 req 时
- gRPC 场景同理,要用
grpc_metadata.MD+propagator.Inject(),载体类型换成propagation.HeaderCarrier(md)
服务端 Extract 失败的三个高频原因
Extract 不成功,trace 就无法续上。最常遇到的是:
- HTTP handler 没从
req.Context()提取:必须用propagator.Extract(req.Context(), propagation.HeaderCarrier(req.Header)),而不是直接 new 一个空 context - header key 大小写不一致:某些代理(如 Nginx 默认)会把 header 名转成小写,而 OpenTelemetry propagator 默认期望
Traceparent这种首字母大写的 key;解决办法是用propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})并确保底层 carrier 支持大小写不敏感读取,或在 proxy 层显式保留大小写 - 没启用全局 propagator:必须在程序启动时调用
otel.SetTextMapPropagator(propagation.TraceContext{}),否则otel.GetTextMapPropagator()返回的是空实现
goroutine 分叉时 Context 必须显式传递,不能靠闭包捕获
Go 里 goroutine 不自动继承父 context,这是 Span 断裂最隐蔽的来源。下面这段代码会让子 goroutine 的 span 丢失:
go func() {
// ❌ 错误:没传 ctx,span.Context() 无法被提取
doWork()
}()
正确做法是:
go func(ctx context.Context) {
// ✅ 正确:显式传入,后续 span.FromContext(ctx) 才能拿到 parent span
doWorkWithContext(ctx)
}(req.Context())
- 所有
go func()、time.AfterFunc()、http.HandlerFunc内部启的 goroutine 都要检查 context 是否透传 - 数据库查询、消息队列 publish/consume、定时任务回调,只要脱离当前调用栈,就必须带 context 参数
- 别依赖
context.TODO()或context.Background()来“兜底”——它们没有 span 关联信息











