Go 接入 OpenTelemetry 链路追踪:从 SDK 初始化到全埋点实战

手把手教你在 Go 应用中接入 OpenTelemetry 链路追踪:SDK 初始化、HTTP 服务端与客户端埋点、Redis 客户端埋点、Collector 管道配置,最终导出到观测云 APM 实现服务拓扑与火焰图分析。含完整代码示例。

最佳实践
Go 接入 OpenTelemetry 链路追踪:从 SDK 初始化到全埋点实战封面

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,一行切换。

系列阅读

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台