Go微服务调用链追踪核心是统一传播trace_id/span_id并集成OpenTelemetry;需用otelhttp自动拦截HTTP请求、手动创建子span传递context、配置OTLP/Jaeger导出器并调用shutdown。
Go 微服务中实现调用链追踪,核心是统一传播 trace_id 和 span_id,并集成 OpenTelemetry(OTel)——它已取代 OpenTracing 成为事实标准,且官方 SDK 对 Go 支持成熟、轻量、无侵入式中间件依赖。
otelhttp 自动拦截 HTTP 客户端和服务端 span绝大多数 Go 微服务基于 HTTP(如 REST/gRPC-HTTP gateway),otelhttp 是最省力的起点。它通过包装 http.RoundTripper 和 http.Handler 实现自动注入/提取 trace 上下文。
注意:必须确保所有 HTTP 请求都走被包装的客户端,否则 span 会断开;服务端 handler 也需显式注册,不能直接传 nil 或裸函数。
otelhttp.NewTransport() 包装底层 transport,再传给 http.Client
otelhttp.NewHandler() 包装原始 handler,而非直接 http.ListenAndServe()
gin 或 echo,要替换默认 middleware,例如 gin 中用 gin.WrapH(otelhttp.NewHandler(...))
otelhttp.WithBodyCapture(),但生产环境禁用(性能和隐私风险)import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"client := &http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), }
mux := http.NewServeMux() mux.HandleFunc("/api/user", userHandler) http.ListenAndServe(":8080", otelhttp.NewHandler(mux, "/"))
数据库查询、消息队列消费、本地方法调用等无法被 otelhttp 覆盖的路径,必须显式创建 span 并将 context.Context 向下传递。漏传 context 是链路断裂最常见原因。
关键点:
ctx 创建新 span,不要用 context.Background()
trace.SpanFromContext(ctx) 检查是否已有有效 span,避免意外新建 root spanpgx/v5 + otelpgx),否则需手动 wrap QueryContext 等方法func processOrder(ctx context.Context, orderID string) error {
ctx, span := tracer.Start(ctx, "process_order")
defer span.End()
// 传递 ctx 给下游
if err := db.QueryRowContext(ctx, "SELECT ...").Scan(&name); err != nil {
span.RecordError(err)
return err
}
return nil}
配置 OTel Exporter 到 Jaeger / OTLP / Zipkin
Go SDK 默认不导出数据,必须显式配置 exporter。推荐优先选 OTLP(协议统一、支持指标/日志/trace 一体),其次 Jaeger(兼容老环境)。
常见陷阱:
jaeger.NewExporter 默认用 UDP,容器内易丢包;应改用 jaeger.WithAgentEndpoint + 显式 IP+端口,或切到 jaeger.WithCollectorEndpoint
otlphttp.NewExporter 需设置 WithEndpoint("otel-collector:4
318"),路径默认是 /v1/traces,别漏写 https:// 或配错端口(4317=grpc, 4318=http)shutdown() 会导致进程退出前最后一批 trace 丢失(尤其测试或短命 job)AlwaysSample() 仅用于调试;生产建议用 ParentBased(TraceIDRatioBased(0.01)) 控制量级exp, err := otlphttp.NewExporter(otlphttp.WithEndpoint("otel-collector:4318"))
if err != nil { /* handle */ }
defer exp.Shutdown(context.Background())
tp := trace.NewTracerProvider(
trace.WithBatcher(exp),
trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(0.01))),
)
真正难的不是埋点,而是确保每个 goroutine、每个 callback、每个第三方库调用都携带并透传 context —— 这需要团队约定 + 代码审查,光靠工具覆盖不了所有分支。