HTTP客户端需用otelhttp.NewClient()包装并调用http.NewRequestWithContext(ctx)传入带span的ctx,由otelhttp自动注入traceparent头;服务端须用otelhttp.NewHandler()并显式配置propagator,确保正确解析和延续trace上下文。

HTTP客户端请求怎么自动注入trace_id?
Go 的 http.Client 本身不携带上下文传播能力,必须手动把 context.Context 传进去,否则 otelhttp 中间件根本看不到 trace 上下文。常见错误是直接用 http.Get(url) 或新建没带 context 的 client —— 这会导致下游服务收不到 traceparent header,链路直接断掉。
正确做法是:用 otelhttp.NewClient() 包一层,并在每次 Do() 前把带 span 的 context 注入进去:
client := otelhttp.NewClient(http.DefaultClient) req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/v1/users", nil) resp, err := client.Do(req) // ctx 里已有当前 span,otelhttp 会自动 inject header
- 别用
http.Get()、http.Post()等快捷函数,它们不接受 context - 确保传入的
ctx是从span.SpanContext()派生出来的(比如trace.ContextWithSpan(ctx, span)) - 如果用了第三方 HTTP 客户端(如
resty),需手动调用otel.GetTextMapPropagator().Inject()写入 headers
HTTP服务端怎么接收并延续trace上下文?
服务端靠 otelhttp.NewHandler() 中间件解析 traceparent header 并创建子 span。但很多人漏掉关键一步:没配置全局 propagator,导致 header 解析失败,span parent_id 为空。
必须在程序启动时显式设置:
import "go.opentelemetry.io/otel/sdk/propagation"
// ...
otel.SetTextMapPropagator(propagation.TraceContext{})
- 若同时用了
b3或jaeger格式,要换用propagation.NewCompositeTextMapPropagator() - handler 返回的
http.Handler必须作为最外层中间件,不能被其他 middleware(如日志、鉴权)提前 consume 了 request body 或 headers - 注意:Gin/Echo 等框架需用对应适配器(如
otelgin.Middleware),不能直接套otelhttp.NewHandler
为什么本地跑通了,上 K8s 就丢 span?
典型现象是:本地 curl 能看到完整链路,但 Pod 间调用只有一跳。根本原因通常是 exporter 配置没走通,或 service name 没设对,导致 traces 被 backend 丢弃。
-
service.name必须通过resource.WithAttributes(semconv.ServiceNameKey.String("xxx"))显式传入,不能依赖环境变量或默认值 - Exporter 地址写成
http://otel-collector:4318/v1/traces时,确认 Service DNS 可解析、端口开放、且 collector 实际监听4318(不是4317gRPC) - K8s 下若启用了 Istio,它默认会覆盖 trace header;需在
Sidecar配置中显式放行traceparent和tracestate
Span 名称为什么全是 “HTTP GET”?怎么按路由区分?
otelhttp 默认用 method + path pattern(如 GET /api/v1/:id)生成 span name,但如果你用的是自定义路由(如 Gin 的 GET("/user/:id")),而 handler 里又没把路由信息塞进 context,span 名就退化成静态字符串。
解决方法是在 handler 开头手动重命名 span:
span := trace.SpanFromContext(r.Context())
span.SetName("GET /user/{id}") // 或拼接实际参数:fmt.Sprintf("GET /user/%s", id)
- 更稳妥的方式是用框架适配器(如
otelgin)并开启WithPublicEndpoint()或自定义SpanNameFormatter - 避免在 span 结束后调用
SetName(),无效 - 注意:span name 过长(>128 字符)可能被 collector 截断,建议控制在合理长度
链路追踪真正难的不是埋点,而是跨进程 context 传递的每个环节都得对齐格式、不丢字段、不乱序——尤其在混用多种 SDK 或网关介入时,header 的读写顺序和 key 名大小写都可能成为断点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











