关键在于context传递:90%断链因未用otelhttp.newhandler包裹http handler或误用context.withvalue替代trace.contextwithspan,导致span无法继承trace_id和parent_span_id。

Go 服务接入 OpenTelemetry 做全链路追踪,关键不在于“能不能接”,而在于“是否漏掉了 context 传递”——90% 的追踪断链都源于 context.WithValue 替代了 trace.ContextWithSpan,或 HTTP handler 中忘了用 otelhttp.NewHandler 包裹。
HTTP 服务端自动注入 span 时,必须用 otelhttp.NewHandler
直接在 handler 函数里手动 tracer.Start 只能建孤立 span,无法继承上游 trace_id 和 parent_span_id。OpenTelemetry 的传播依赖标准 traceparent header,而这个解析和注入必须由中间件完成。
常见错误:写一个自定义 middleware 手动读 r.Header.Get("traceparent") 却没调用 propagators.Extract,导致 carrier 解析失败,span 被当成 root。
- 正确做法是用官方
otelhttp.NewHandler,它内部已集成 W3C propagator 和 span 生命周期管理 - 若需自定义逻辑(如按 path 过滤采样),应在
otelhttp.WithFilter中处理,而非绕过它 - 注意:它只包装
http.Handler,对http.HandlerFunc需先转成Handler,例如otelhttp.NewHandler(http.HandlerFunc(yourFunc), "your-route")
context.WithValue 不能替代 trace.ContextWithSpan
很多开发者误以为把 span 存进 context.WithValue(ctx, key, span) 就能跨 goroutine 传递 trace 上下文,这是错的。OpenTelemetry 的 span 关联依赖的是 trace.SpanContext 在 context 中的特定 key(trace.ContextKey),不是任意 key。
典型现象:goroutine 中调用 tracer.Start(ctx, "sub-op"),生成的 span 总是独立 trace,parent_id 为空。
- 必须用
trace.ContextWithSpan(ctx, span)注入,它会把 span 的SpanContext写入标准位置 - 跨 goroutine 时,确保传入的是带 span 的 ctx(不是原始 request ctx),例如
go doWork(trace.ContextWithSpan(ctx, span)) - 数据库操作、RPC 调用等异步场景,尤其容易在这里掉链
gRPC 客户端和服务端需分别配置 otelgrpc 拦截器
gRPC 的 metadata 传播机制与 HTTP 不同,otelhttp 对它完全无效。不显式配置拦截器,客户端发出的请求永远带不出 traceparent,服务端也收不到。
错误示例:只在 client 端加了 otelgrpc.WithClientTrace,但 server 端仍用原生 grpc.Server(),结果 server span 全是 root。
- 客户端:创建
grpc.Dial时传入grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()) - 服务端:创建
grpc.NewServer时传入grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()) - 注意拦截器顺序:如果还用了其他 interceptor(如 auth),
otelgrpc应放在最外层,确保最先接触 context
指标(Metrics)和追踪(Traces)默认不共享 pipeline,需手动关联
很多人以为启用了 otelmetric 和 oteltrace 就自动“全链路”了,其实指标数据默认没有 trace_id、span_id 维度,查问题时无法把某次慢请求的延迟指标和它的 trace 对上。
根本原因:metric recorder 默认使用空 context,不携带当前 span 的 context。
- 上报指标前,用
metric.WithAttribute("trace_id", span.SpanContext().TraceID().String())手动补维度(仅限调试,别在高频打点中用) - 生产推荐方案:用
resource.WithAttributes(semconv.ServiceNameKey.String("my-service"))+view规则聚合,再结合 trace_id 查询(如 Jaeger + Prometheus 联查) - 更彻底的做法是启用 OpenTelemetry 的
metric.WithInstrumentationScope并配合 SDK 的View过滤,但 Go SDK 当前对 trace 关联支持仍有限
真正卡住落地的,往往不是 SDK 初始化那几行代码,而是第三层 goroutine 里忘了传 ctx、或是 HTTP 中间件和 gRPC 拦截器混用时 propagation 机制不一致。trace 能不能连上,本质是 context 流动的确定性问题,不是配置多寡的问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











