Go服务启用OpenTelemetry HTTP中间件需手动注入trace ID,因net/http默认不传播trace上下文;必须用otelhttp.NewHandler包裹handler、显式设置W3C propagator,并确保中间件在路由注册前加载。

Go服务中启用OpenTelemetry HTTP中间件需手动注入trace ID
默认情况下,net/http 不会自动传播 trace 上下文,即使你已配置了 otelhttp.NewHandler,前端请求的 traceparent 也不会被解析并延续到后续 span 中。必须显式启用传播器(如 W3C TraceContext)并确保中间件包裹在正确位置。
- 用
otelhttp.NewHandler包裹你的http.Handler,而不是直接 wraphttp.HandleFunc - 初始化时调用
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) - 若使用 Gin,需注册
gin.MiddlewareFunc形式的 otel 中间件,且必须放在路由注册前,否则 span 名称会是HTTP GET /而非真实路径 - 检查请求 header 是否含
traceparent:本地测试可用curl -H 'traceparent: 00-12345678901234567890123456789012-1234567890123456-01' http://localhost:8080/api/v1/user
Gin 框架集成 otelhttp 后 span 名称固定为 “HTTP GET”
这是因为 otelhttp.NewHandler 默认只记录 method 和 protocol,不解析路由模板。Gin 的路由是动态匹配的,otelhttp 无法自动获取 /api/v1/:id 这类 pattern,只能看到最终匹配后的原始 URL 路径(如 /api/v1/123),但默认 span name 仍退化为方法名。
- 解决方案:用
otelhttp.WithSpanNameFormatter自定义名称,例如func(_ *http.Request, r *http.Response) string { return "Gin:" + r.Request.URL.Path } - 更推荐方式:在 Gin 中间件里手动创建子 span,并用
span.SetName()设置语义化名称,比如"GET /api/v1/user",同时调用span.SetAttributes(attribute.String("http.route", c.FullPath())) - 注意不要重复 start span:Gin 中间件和
otelhttp同时启用会导致双 span,应只保留其一(推荐 Gin 中间件方式,控制力更强)
导出器连接 Jaeger 或 OTLP 时 timeout 或 connection refused
常见于开发环境用 localhost 配置却未真正运行后端服务,或容器网络隔离导致 DNS 解析失败。OpenTelemetry Go SDK 默认使用 http.DefaultClient,超时时间长(30s),容易卡住整个请求流程。
- 使用
otlphttp.NewClient时务必设置otlphttp.WithEndpoint("jaeger:4318")(不是http://jaeger:4318),协议由 client 自动补全 - 本地调试建议启动 Jaeger:
docker run -d -p 16686:16686 -p 4317:4317 -p 4318:4318 --name jaeger jaegertracing/all-in-one:latest - 生产环境避免用
stdout导出器,它会阻塞 goroutine;改用otlphttp并配置otlphttp.WithTimeout(3 * time.Second) - 若用 Kubernetes,确认 service 名可被 Pod 内 DNS 解析,优先用
jaeger-collector.observability.svc.cluster.local:4318替代localhost
trace 数据丢失:span 未上报或 parent-child 关系断裂
最常发生在异步操作(goroutine、callback、database query)中忘记传递 context。Go 的 context 是 trace 的载体,一旦丢弃或新建空 context,新 span 就变成 root span,链路断裂。
立即学习“go语言免费学习笔记(深入)”;
- 所有异步调用前,必须用
trace.ContextWithSpan(ctx, span)或至少ctx = trace.ContextWithRemoteSpanContext(ctx, sc)传递上下文 - 数据库操作(如
sqlx、gorm)需配合otelmysql或otelgorm等插件,否则 query span 不会继承父 span - 日志库(如
zerolog、zap)要集成otellog,才能将 trace_id 注入 log 字段,方便关联排查 - 检查 span 状态:调用
span.End()前可加span.SetStatus(codes.Ok, ""),避免因 panic 导致 span 未结束而丢弃
实际部署时最容易忽略的是 context 传递的完整性——尤其在 error handling 分支、defer 逻辑、以及第三方库回调中。一个没传 context 的 goroutine,就足以让整条链路在那个节点断开。


















