Go 接入 OpenTelemetry 链路追踪:从 SDK 初始化到全埋点实战
手把手教你在 Go 应用中接入 OpenTelemetry 链路追踪:SDK 初始化、HTTP 服务端与客户端埋点、Redis 客户端埋点、Collector 管道配置,最终导出到观测云 APM 实现服务拓扑与火焰图分析。含完整代码示例。
Go 语言的 OpenTelemetry 链路追踪接入遵循"初始化 SDK → 埋点 HTTP 服务端 → 埋点出站调用"的三步套路——Go 没有 Java 那样的 Agent 魔法,埋点需要显式编码,但换来的是对链路的完全掌控。
核心要点速览
- Go 埋点三件套:TracerProvider 初始化、otelhttp 中间件、各客户端的 OTel 包装;
- 上下文靠
context.Context显式传递,goroutine 泄漏是断链首因; - Collector 接收 OTLP 后转发后端,解耦应用与存储;
- 导出端点指向观测云 DataKit(4317/4318)即可在观测云 APM 看链路。
第一步:初始化 SDK
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("datakit-host:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
res, _ := resource.New(ctx,
resource.WithAttributes(semconv.ServiceName("order-service")),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
return tp, nil
}
两个关键点:service.name 决定后端的服务标识,务必规范命名;程序退出前调用 tp.Shutdown(ctx) 确保缓存的 Span 全部发出。
第二步:埋点 HTTP 服务端
用 otelhttp 中间件包住 Handler,入站请求自动生成 Span:
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
mux.Handle("/orders", otelhttp.NewHandler(ordersHandler, "orders.list"))
中间件自动完成三件事:从请求头提取 traceparent 续上上游链路、创建 SERVER 类型 Span、记录状态码与耗时。
第三步:埋点出站调用
链路要完整,出站的每一跳都不能漏:
HTTP 客户端——换用 otelhttp 包装的 Transport:
client := &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
req, _ := http.NewRequestWithContext(ctx, "GET", "http://inventory/check", nil)
resp, err := client.Do(req) // 自动创建 CLIENT Span 并注入 traceparent
注意必须用带 ctx 的请求构造方法——上下文断了,链路就断了。
Redis 客户端——go-redis 有官方 OTel 插件:
import "github.com/redis/go-redis/extra/redisotel/v9"
rdb := redis.NewClient(&redis.Options{Addr: "redis:6379"})
if err := redisotel.InstrumentTracing(rdb); err != nil { log.Fatal(err) }
数据库操作(database/sql)同理,用 otelsql 包装驱动即可。
第四步:Collector 管道配置
应用不直连后端,先发给 Collector:
receivers:
otlp:
protocols:
grpc: {endpoint: 0.0.0.0:4317}
http: {endpoint: 0.0.0.0:4318}
processors:
batch: {timeout: 5s}
exporters:
otlp:
endpoint: "http://datakit:4318"
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
批处理降低网络开销,未来换后端只改 exporter——这就是 Collector 存在的意义。
goroutine 里的上下文陷阱
Go 特有的坑:异步 goroutine 默认拿不到请求上下文。错误示范:
go processAsync() // 链路断了!
正确姿势是显式传递:
go func(ctx context.Context) { processAsync(ctx) }(r.Context())
排障时"链路在某服务处消失",九成是这个原因。详见《上下文传播》。
观测云落地:链路直接进 APM
把上文 exporter 端点指向观测云 DataKit 的 OTLP 地址即可——DataKit 接收链路数据后上报观测云,APM 模块自动绘制服务拓扑、生成火焰图,慢调用与 ERROR Span 高亮呈现。搭配在日志中注入 trace_id,还能从日志一键跳转到完整链路,排障动线完全闭环。
常见问题(FAQ)
Q:Go 有自动埋点方案吗? 官方 eBPF 自动埋点(opentelemetry-go-instrumentation)可零代码接入,但成熟度与 Span 细节不如显式埋点;生产建议关键服务显式埋点。
Q:Span 属性加多少合适? 只加有检索价值的(订单号、用户等级等);高基数值(如完整 SQL)放属性会推高成本,考虑只记录脱敏后的摘要。
Q:采样在哪配? SDK 侧 sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(0.2))),或交给 Collector 做尾部采样——见《采样详解》。
Q:测试环境不想发数据怎么办? 初始化时判断环境变量,dev 环境用 noop TracerProvider,一行切换。