跳转至

OpenTelemetry Go SDK

OpenTelemetry Go SDK 通过 API、SDK 和框架插桩库采集 Go 应用的遥测数据。与 Java Agent 等运行时自动插桩方案不同,Go 应用通常需要在代码中初始化 SDK,并使用相应的插桩库包装 Web 框架、HTTP 客户端、数据库或消息队列。

本文使用 DataKit 的 OpenTelemetry 采集器接收 OTLP 数据并转发到观测云:

Go 应用 + OpenTelemetry Go SDK -> OTLP -> DataKit -> 观测云

本文示例通过 OTLP/HTTP + Protobuf 上报 Trace 和 Metric。OpenTelemetry Go 的 Trace、Metric 已达到稳定状态;Log SDK 的成熟度和生态支持仍可能变化,生产环境接入前应核对当前官方状态。

前置条件

  • Go 1.23 或更高版本;
  • 已安装 DataKit,且 DataKit 已连接到目标观测云工作空间;
  • Go 应用到 DataKit 的网络可达:OTLP/HTTP 使用 DataKit HTTP 端口 9529,OTLP/gRPC 默认使用 4317
  • 已确认应用所使用的框架或组件存在对应的 OpenTelemetry Go 插桩库,或计划通过 OpenTelemetry API 手工创建 Span 和 Metric。

一、开启 OpenTelemetry 采集器

进入 DataKit 安装目录下的 conf.d/opentelemetry。如果尚未创建采集器配置,复制示例文件:

cd /usr/local/datakit/conf.d/opentelemetry
sudo cp opentelemetry.conf.sample opentelemetry.conf

确认 opentelemetry.conf 至少包含以下接收配置:

[[inputs.opentelemetry]]
  # 如需在观测云中将自定义属性作为标签保留,请在此加入白名单。
  # 属性名中的点会被转换为下划线,例如 team.name -> team_name。
  customer_tags = ["team", "project"]

  [inputs.opentelemetry.http]
    http_status_ok = 200
    trace_api = "/otel/v1/traces"
    metric_api = "/otel/v1/metrics"
    logs_api = "/otel/v1/logs"

  [inputs.opentelemetry.grpc]
    addr = "127.0.0.1:4317"
    max_payload = 16777216

以上配置开启以下接收地址:

协议 数据类型 DataKit 接收地址
OTLP/HTTP + Protobuf Trace http://<DataKit-IP>:9529/otel/v1/traces
OTLP/HTTP + Protobuf Metric http://<DataKit-IP>:9529/otel/v1/metrics
OTLP/HTTP + Protobuf Log http://<DataKit-IP>:9529/otel/v1/logs
OTLP/gRPC Trace、Metric、Log http://<DataKit-IP>:4317

如果应用与 DataKit 不在同一主机,需按实际部署调整 DataKit HTTP 监听地址、防火墙或其他网络访问控制。使用 OTLP/gRPC 时,还需要将 addr 改为应用可访问的监听地址,例如 0.0.0.0:4317。不要将 OTLP 接收端口直接暴露到公网。

重启 DataKit 使配置生效:

sudo datakit service restart

检查 DataKit HTTP 服务是否可达:

curl http://127.0.0.1:9529/v1/ping

二、应用接入 OpenTelemetry

安装 Go SDK 和插桩库

在 Go 项目目录中安装官方 SDK、OTLP/HTTP Exporter 和 net/http 插桩库:

go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
go get go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
go mod tidy

go.modgo.sum 会记录依赖版本。生产环境应提交这两个文件,并在升级 OpenTelemetry 依赖后执行编译、单元测试和链路回归测试。

初始化 SDK

以下示例同时完成:

  1. OTEL_SERVICE_NAMEOTEL_RESOURCE_ATTRIBUTES 读取资源属性;
  2. 创建 OTLP/HTTP Trace Exporter 和 Metric Exporter;
  3. 注册全局 TracerProviderMeterProvider 和 W3C 上下文传播器;
  4. 使用 otelhttp 包装 HTTP Handler,并创建一个业务子 Span 和自定义 Counter;
  5. 在进程退出时刷新并关闭 Provider,避免缓冲区中的数据丢失。
package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    "go.opentelemetry.io/otel/propagation"
    sdkmetric "go.opentelemetry.io/otel/sdk/metric"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
    res, err := resource.New(
        ctx,
        resource.WithFromEnv(),
        resource.WithTelemetrySDK(),
        resource.WithHost(),
        resource.WithOS(),
        resource.WithProcess(),
    )
    if err != nil {
        return nil, fmt.Errorf("create resource: %w", err)
    }

    traceExporter, err := otlptracehttp.New(ctx)
    if err != nil {
        return nil, fmt.Errorf("create trace exporter: %w", err)
    }

    tracerProvider := sdktrace.NewTracerProvider(
        sdktrace.WithResource(res),
        sdktrace.WithSampler(
            sdktrace.ParentBased(sdktrace.TraceIDRatioBased(1.0)),
        ),
        sdktrace.WithBatcher(traceExporter),
    )

    metricExporter, err := otlpmetrichttp.New(ctx)
    if err != nil {
        _ = tracerProvider.Shutdown(ctx)
        return nil, fmt.Errorf("create metric exporter: %w", err)
    }

    meterProvider := sdkmetric.NewMeterProvider(
        sdkmetric.WithResource(res),
        sdkmetric.WithReader(
            sdkmetric.NewPeriodicReader(
                metricExporter,
                sdkmetric.WithInterval(30*time.Second),
            ),
        ),
    )

    otel.SetTracerProvider(tracerProvider)
    otel.SetMeterProvider(meterProvider)
    otel.SetTextMapPropagator(
        propagation.NewCompositeTextMapPropagator(
            propagation.TraceContext{},
            propagation.Baggage{},
        ),
    )

    shutdown := func(ctx context.Context) error {
        return errors.Join(
            meterProvider.Shutdown(ctx),
            tracerProvider.Shutdown(ctx),
        )
    }
    return shutdown, nil
}

func main() {
    ctx, stop := signal.NotifyContext(
        context.Background(),
        os.Interrupt,
        syscall.SIGTERM,
    )
    defer stop()

    shutdown, err := setupOTelSDK(ctx)
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        shutdownCtx, cancel := context.WithTimeout(
            context.Background(),
            5*time.Second,
        )
        defer cancel()
        if err := shutdown(shutdownCtx); err != nil {
            log.Printf("shutdown OpenTelemetry: %v", err)
        }
    }()

    meter := otel.Meter("example/order-service")
    requestCounter, err := meter.Int64Counter("app.request.count")
    if err != nil {
        log.Fatal(err)
    }

    mux := http.NewServeMux()
    mux.Handle("/hello", otelhttp.NewHandler(
        http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            requestCtx, span := otel.Tracer("example/order-service").Start(
                r.Context(),
                "prepare-response",
            )
            defer span.End()

            span.SetAttributes(attribute.String("app.route", "/hello"))
            requestCounter.Add(requestCtx, 1)
            _, _ = fmt.Fprintln(w, "hello from OpenTelemetry Go SDK")
        }),
        "GET /hello",
    ))

    server := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
    }

    go func() {
        <-ctx.Done()
        shutdownCtx, cancel := context.WithTimeout(
            context.Background(),
            5*time.Second,
        )
        defer cancel()
        if err := server.Shutdown(shutdownCtx); err != nil {
            log.Printf("shutdown HTTP server: %v", err)
        }
    }()

    log.Println("listening on http://127.0.0.1:8080")
    if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}

实际项目应为使用到的组件安装对应插桩库。例如,标准库 net/http 使用 otelhttp;其他 Web 框架、数据库或消息队列应从 OpenTelemetry Registry 选择匹配的包,并按照该包的说明包装 Handler、Transport、Client 或 Driver。

配置上报地址并启动

下面使用 OTLP/HTTP + Protobuf 上报到本机 DataKit。OTEL_EXPORTER_OTLP_ENDPOINT 是基础地址,Trace 和 Metric Exporter 会分别追加 /v1/traces/v1/metrics,最终对应 DataKit 的 /otel/v1/* 路由。

export OTEL_SERVICE_NAME="order-service"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment.name=prod,service.version=1.0.0,team=backend"

export OTEL_EXPORTER_OTLP_ENDPOINT="http://127.0.0.1:9529/otel"
export OTEL_EXPORTER_OTLP_INSECURE="true"
export OTEL_EXPORTER_OTLP_COMPRESSION="gzip"

go run .

发起请求以生成 Trace 和 Metric:

curl http://127.0.0.1:8080/hello

Metric 默认每 30 秒导出一次,本示例由 sdkmetric.WithInterval(30*time.Second) 控制。等待一个导出周期后,可在观测云中按 service=order-service 查看链路,并查询 app.request.count 指标。

使用 OTLP/gRPC

Go SDK 的 OTLP 传输由代码中使用的 Exporter 包决定。本示例直接使用 otlptracehttpotlpmetrichttp,仅设置 OTEL_EXPORTER_OTLP_PROTOCOL=grpc 不会将其切换为 gRPC。

如需使用 OTLP/gRPC,安装 gRPC Exporter:

go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
go get go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc
go mod tidy

然后将代码中的 Exporter 包及初始化函数分别替换为:

traceExporter, err := otlptracegrpc.New(ctx)
metricExporter, err := otlpmetricgrpc.New(ctx)

并使用不带 /v1/traces/v1/metrics 路径的 gRPC 地址:

export OTEL_EXPORTER_OTLP_ENDPOINT="http://127.0.0.1:4317"
export OTEL_EXPORTER_OTLP_INSECURE="true"

三、数据上报参数

资源参数

本示例通过 resource.WithFromEnv() 读取以下标准环境变量:

环境变量 说明 建议值或示例
OTEL_SERVICE_NAME 服务名,对应资源属性 service.name order-service;生产环境必须显式设置。
OTEL_RESOURCE_ATTRIBUTES 资源属性,格式为逗号分隔的 key=value deployment.environment.name=prod,service.version=1.0.0,team=backend

建议至少设置 service.namedeployment.environment.nameservice.version,用于在观测云中进行服务归属、环境过滤和版本分析。自定义资源属性需要加入 DataKit customer_tags 白名单后才会作为标签保留,属性名中的 . 会转换为 _

OTLP/HTTP 参数

otlptracehttp.New()otlpmetrichttp.New() 会直接读取以下环境变量。信号专用参数优先于通用参数:

通用环境变量 信号专用环境变量 说明 DataKit 示例
OTEL_EXPORTER_OTLP_ENDPOINT OTEL_EXPORTER_OTLP_TRACES_ENDPOINTOTEL_EXPORTER_OTLP_METRICS_ENDPOINT 通用参数是基础 URL,Exporter 自动追加信号路径;信号专用参数是完整 URL,按原值使用。 通用:http://127.0.0.1:9529/otel;Trace:http://127.0.0.1:9529/otel/v1/traces;Metric:http://127.0.0.1:9529/otel/v1/metrics
OTEL_EXPORTER_OTLP_INSECURE OTEL_EXPORTER_OTLP_TRACES_INSECUREOTEL_EXPORTER_OTLP_METRICS_INSECURE 是否关闭传输层 TLS。 DataKit 使用明文 HTTP 时设为 true
OTEL_EXPORTER_OTLP_HEADERS OTEL_EXPORTER_OTLP_TRACES_HEADERSOTEL_EXPORTER_OTLP_METRICS_HEADERS 请求头,格式为逗号分隔的 key=value DataKit 配置 expected_headers 时设置对应值。
OTEL_EXPORTER_OTLP_TIMEOUT OTEL_EXPORTER_OTLP_TRACES_TIMEOUTOTEL_EXPORTER_OTLP_METRICS_TIMEOUT 单次导出超时时间,值为毫秒数。 根据网络情况设置,例如 10000
OTEL_EXPORTER_OTLP_COMPRESSION OTEL_EXPORTER_OTLP_TRACES_COMPRESSIONOTEL_EXPORTER_OTLP_METRICS_COMPRESSION OTLP 请求压缩方式。 可设为 gzip;不压缩时留空。
OTEL_EXPORTER_OTLP_CERTIFICATE OTEL_EXPORTER_OTLP_TRACES_CERTIFICATEOTEL_EXPORTER_OTLP_METRICS_CERTIFICATE 用于校验服务端证书的 PEM CA 文件路径。 通过 HTTPS 接收 OTLP 时按证书部署设置。

如果应用和 DataKit 不在同一主机,应将示例中的 127.0.0.1 替换为应用可访问的 DataKit 地址。

SDK 代码参数

以下参数由 Go SDK 初始化代码控制,不会因为设置同名的通用环境变量而自动生效:

配置项 示例代码 说明
Trace 采样 sdktrace.ParentBased(sdktrace.TraceIDRatioBased(1.0)) 1.0 表示根 Trace 全采样;生产环境可按容量调整为 0.1 等比例,并通过 ParentBased 遵循上游采样决定。
Span 批量导出 sdktrace.WithBatcher(traceExporter) 生产环境建议使用批量导出;可通过 WithMaxQueueSizeWithMaxExportBatchSizeWithBatchTimeoutWithExportTimeout 进一步调整。
Metric 导出周期 sdkmetric.WithInterval(30*time.Second) 控制周期性导出间隔;过短会增加应用、网络和存储开销。
上下文传播 TraceContext{}Baggage{} 使用 W3C traceparenttracestatebaggage。调用链上的服务应保持传播格式兼容。
资源探测 resource.WithHost()WithOS()WithProcess() 自动补充主机、操作系统和进程属性。避免在进程参数和资源属性中存放密钥等敏感信息。

本示例直接创建 OTLP Exporter,因此需要特别注意:

  • OTEL_EXPORTER_OTLP_PROTOCOL 不会改变已经由代码选定的 HTTP 或 gRPC Exporter;
  • OTEL_TRACES_EXPORTEROTEL_METRICS_EXPORTER 不会关闭本示例中直接创建的 Exporter;
  • OpenTelemetry Go 核心 SDK 当前不会自动应用所有通用 SDK 环境变量,尤其不要假设 OTEL_SDK_DISABLEDOTEL_TRACES_SAMPLEROTEL_PROPAGATORS 会在自定义初始化代码中自动生效;
  • 如需通过标准环境变量动态选择和初始化 Exporter,可评估官方 Contrib 的 autoexport 包,并在测试环境验证其行为。

验证与排查

  1. 执行 curl http://127.0.0.1:9529/v1/ping,确认应用可以访问 DataKit;
  2. 启动应用并访问 /hello,确认应用日志中没有 create trace exportercreate metric exporter 或 OTLP export error;
  3. 等待至少一个 Metric 导出周期;
  4. 在观测云的 APM 服务列表中按 order-service 查询 Trace;
  5. 查询不到数据时,检查 DataKit opentelemetry 采集器配置、应用到 DataKit 的网络、上报 URL 和 DataKit 日志;
  6. 出现重复 Span 时,检查同一 Handler、Transport、数据库 Client 或 Driver 是否被重复包装。

参考资料

文档评价

文档内容是否对您有帮助? ×