通过 OpenTelemetry-Go 接入 Go 应用(推荐)

最近更新时间: 2026-06-30 15:06:00

说明:

  • OpenTelemetry 是工具、API 和 SDK 的集合,用于检测、生成、收集和导出遥测数据(指标、日志和跟踪),帮助用户分析软件的性能和行为。关于 OpenTelemetry 的更多信息请参考 OpenTelemetry 官方网站
  • OpenTelemetry 社区活跃,技术更迭迅速,广泛兼容主流编程语言、组件与框架,为云原生微服务以及容器架构的链路追踪能力广受欢迎。

本文将通过相关操作介绍如何通过社区的 OpenTelemetry-Go 方案接入 Go 应用。OpenTelemetry-Go 提供了一系列 API,用户可以通过 SDK 将性能数据并发送到可观测平台的服务端。本文通过最常见的应用行为,例如 HTTP 服务、访问数据库等,介绍如何基于 OpenTelemetry-Go 接入应用性能监控 APM,对于 OpenTelemetry-Go 的更多用法,请参考 项目主页

前提条件

此方案支持 Go 的官方支持版本,目前为1.21和1.22,对于更低的版本,理论上可以接入,但社区不保持完整的兼容性,具体信息请参考社区 兼容说明

前置步骤:获取接入点和 Token

  1. 在左侧菜单栏中选择应用性能监控 > 应用监控,单击应用列表 > 接入应用
  2. 在右侧弹出的数据接入抽屉框中,单击 Go 语言。
  3. 接入 Go 应用页面,选择您所要接入的地域以及业务系统
  4. 选择接入协议类型OpenTelemetry
  5. 获取接入点Token

接入 Go 应用

步骤1:引入 OpenTelemetry 相关依赖,实现 SDK 初始化逻辑

package main

import (
    "context"
    "errors"
    "time"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
    "go.opentelemetry.io/otel/propagation"
    "go.opentelemetry.io/otel/sdk/metric"
    "go.opentelemetry.io/otel/sdk/resource"
    "go.opentelemetry.io/otel/sdk/trace"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    "log"
)

func setupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
    var shutdownFuncs []func(context.Context) error

    shutdown = func(ctx context.Context) error {
        var err error
        for _, fn := range shutdownFuncs {
            err = errors.Join(err, fn(ctx))
        }
        shutdownFuncs = nil
        return err
    }

    handleErr := func(inErr error) {
        err = errors.Join(inErr, shutdown(ctx))
    }

    tracerProvider, err := newTraceProvider(ctx)
    if err != nil {
        handleErr(err)
        return
    }
    shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
    otel.SetTracerProvider(tracerProvider)

    meterProvider, err := newMeterProvider()
    if err != nil {
        handleErr(err)
        return
    }
    shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
    otel.SetMeterProvider(meterProvider)
    return
}

func newTraceProvider(ctx context.Context) (*trace.TracerProvider, error) {
    opts := []otlptracegrpc.Option{
        otlptracegrpc.WithEndpoint("<endpoint>"), // <endpoint>替换为上报地址
        otlptracegrpc.WithInsecure(),
    }
    exporter, err := otlptracegrpc.New(ctx, opts...)
    if err != nil {
        log.Fatal(err)
    }

    r, err := resource.New(ctx, []resource.Option{
        resource.WithAttributes(
            attribute.KeyValue{Key: "token", Value: attribute.StringValue("<token>")}, // <token>替换为业务系统Token
            attribute.KeyValue{Key: "service.name", Value: attribute.StringValue("<serviceName>")}, // <serviceName>替换为应用名
            attribute.KeyValue{Key: "host.name", Value: attribute.StringValue("<hostName>")}, // <hostName>替换为IP地址
        ),
    }...)
    if err != nil {
        log.Fatal(err)
    }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithSampler(sdktrace.AlwaysSample()),
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(r),
    )
    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
    return tp, nil
}

func newMeterProvider() (*metric.MeterProvider, error) {
    metricExporter, err := stdoutmetric.New()
    if err != nil {
        return nil, err
    }

    meterProvider := metric.NewMeterProvider(
        metric.WithReader(metric.NewPeriodicReader(metricExporter,
            // Default is 1m. Set to 3s for demonstrative purposes.
            metric.WithInterval(3*time.Second))),
    )
    return meterProvider, nil
}

对应的字段说明如下,请根据实际情况进行替换。

  • <serviceName> :应用名,多个使用相同 serviceName 接入的应用进程,在 APM 中会表现为相同应用下的多个实例。应用名最长63个字符,只能包含小写字母、数字及分隔符“ - ”,且必须以小写字母开头,数字或小写字母结尾。
  • <token> :前置步骤中拿到业务系统 Token。
  • <hostName>:该实例的主机名,是应用实例的唯一标识,通常情况下可以设置为应用实例的 IP 地址。
  • <endpoint> :前置步骤中拿到的接入点。

步骤2:SDK 初始化,启动 HTTP 服务

package main

import (
   "context"
   "errors"
   "fmt"
   "log"
   "net"
   "net/http"
   "os"
   "os/signal"
   "time"
   "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func main() {
   if err := run(); err != nil {
      log.Fatalln(err)
   }
}

func run() (err error) {
   ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
   defer stop()

   // 初始化 SDK
   otelShutdown, err := setupOTelSDK(ctx)
   if err != nil {
      return
   }
   // 优雅关闭
   defer func() {
      err = errors.Join(err, otelShutdown(context.Background()))
   }()

   // 启动HTTP服务
   srv := &http.Server{
      Addr:         ":8080",
      BaseContext:  func(_ net.Listener) context.Context { return ctx },
      ReadTimeout:  time.Second,
      WriteTimeout: 10 * time.Second,
      Handler:      newHTTPHandler(),
   }
   srvErr := make(chan error, 1)
   go func() {
      srvErr <- srv.ListenAndServe()
   }()
 
   select {
   case err = <-srvErr:
     return
   case <-ctx.Done():
      stop()
   }

   err = srv.Shutdown(context.Background())
   return
}

如果通过 Gin 等框架实现 HTTP 服务,埋点方式会存在区别,具体请参考社区的 框架列表 查看其他框架的埋点方式。

步骤3: 对 HTTP 接口进行埋点增强

func newHTTPHandler() http.Handler {
   mux := http.NewServeMux()

   handleFunc := func(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) {
      // 对HTTP路由进行埋点
      handler := otelhttp.WithRouteTag(pattern, http.HandlerFunc(handlerFunc))
      mux.Handle(pattern, handler)
   }

   // 注册接口
   handleFunc("/simple", simpleIOHandler)

   // 对所有接口进行埋点增强
   handler := otelhttp.NewHandler(mux, "/")
   return handler
}

func simpleIOHandler(w http.ResponseWriter, r *http.Request) {
   io.WriteString(w, "ok")
}

接入验证

启动 Go 应用后,通过8080端口访问对应的接口,例如 https://localhost:8080/simple,应用就会向 APM 上报处理 HTTP 请求相关的链路数据。在有正常流量的情况下,应用性能监控 > 应用监控 > 应用列表中将展示接入的应用,应用性能监控 > 应用监控 > 应用详情 > 实例监控中将展示接入的应用实例。由于可观测数据的处理存在一定延时,如果接入后在控制台没有查询到应用或实例,请等待30秒左右。

更多埋点示例

访问 Redis®

  • 初始化
import (
    "github.com/redis/go-redis/v9"
    "github.com/redis/go-redis/extra/redisotel/v9"
)

var rdb *redis.Client

// InitRedis 初始化Redis®客户端
func InitRedis() *redis.Client {
    rdb := redis.NewClient(&redis.Options{
        Addr:     "127.0.0.1:6379",
        Password: "",  // no password
    })
    if err := redisotel.InstrumentTracing(rdb); err != nil {
        panic(err)
    }
    if err := redisotel.InstrumentMetrics(rdb); err != nil {
        panic(err)
    }
    return rdb
}
  • 数据访问
func redisRequest(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    rdb := InitRedis()
    val, err := rdb.Get(ctx, "foo").Result()
    if err != nil {
        log.Printf("redis err......")
        panic(err)
    }
    fmt.Println("redis res: ", val)
}

访问 MySQL

初始化

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/schema"
    "gorm.io/plugin/opentelemetry/tracing"
)

var GormDB *gorm.DB

type TableDemo struct {
    ID    int    `gorm:"column:id"`
    Value string `gorm:"column:value"`
}

func InitGorm() {
    var err error

    dsn := "root:4T$er3deffYuD#9Q@tcp(127.0.0.1:3306)/db_demo?charset=utf8mb4&parseTime=True&loc=Local"
    GormDB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
        NamingStrategy: schema.NamingStrategy{
            SingularTable: true, // 使用单数表名
        },
    })
    if err != nil {
        panic(err)
    }
    //加入tracing上报逻辑
    //需要根据实际情况填入DBName,在APM的拓扑图中,通过DBName字段确认节点,在本示例中使用"mockdb-mysql"
    if err = GormDB.Use(tracing.NewPlugin(tracing.WithoutMetrics(),tracing.WithDBName("mockdb-mysql"))); err != nil {
        panic(err)
    }
}

数据访问

func gormRequest(ctx context.Context) {
    var val string
    if err := gormclient.GormDB.WithContext(ctx).Model(&gormclient.TableDemo{}).Where("id = ?", 1).Pluck("value", &val).Error; err != nil {
                panic(err)
    }
    fmt.Println("MySQL query result: ", val)
}

内部方法埋点,以及设置 Span 的属性

通过以下代码对内部方法埋点,可以在当前的链路上下文中插入一个类型为 Internal 的 Span。

func internalSpanFunc(w http.ResponseWriter, r *http.Request) {
    internalInvoke(r)
    io.WriteString(w, "ok")
}

func internalInvoke(r *http.Request) {
    // 创建一个 Internal Span
    _, span := tracer.Start(r.Context(), "internalInvoke")
    defer span.End()
 
    // 业务逻辑省略
 
    // 设置Span的Attributes
    span.SetAttributes(attribute.KeyValue{