帮你快速理解、总结文档立即下载
文档中心>腾讯云可观测平台>应用性能监控>接入指南>接入 Go 应用>通过 OpenTelemetry SDK 接入 Go 应用(推荐)

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

最近更新时间:2026-09-17 17:15:01
本文档已由 AI 辅助审校
我的收藏
说明:
OpenTelemetry 是工具、API 和 SDK 的集合,用于检测、生成、收集和导出遥测数据(指标、日志和链路),帮助用户分析软件的性能和行为。关于 OpenTelemetry 的更多信息请参考 OpenTelemetry 官方网站
本文介绍如何基于 OpenTelemetry SDK 将 Go 应用接入腾讯云应用性能监控 APM,把应用的可观测数据上报到服务端。文中以最常见的应用行为(如 HTTP 服务、访问数据库等)为例进行说明。关于 OpenTelemetry SDK 的更多用法,请参考 项目主页

前提条件

请使用 Go 1.18及以上版本。

操作步骤

步骤1:获取接入点和 Token

1. 登录 腾讯云可观测平台 控制台。
2. 在左侧菜单栏中选择应用性能监控 > 应用列表,单击接入应用
3. 在右侧弹出的接入应用抽屉框中,单击 Go 语言。
4. 接入 Go 应用页面,选择您所要接入的地域以及业务系统
5. 选择接入协议类型OpenTelemetry
6. 选择您所需要的上报方式,获取您的接入点Token
说明:
内网上报:使用此上报方式,您的服务需运行在腾讯云 VPC。通过 VPC 直接连通,在避免外网通信安全风险的同时,可以节省上报流量开销。
外网上报:当您的服务部署在本地或非腾讯云 VPC 内时,可以通过此方式上报数据。请注意外网通信存在安全风险,同时也会产生一定的上报流量费用。

步骤2:指定 SDK 版本(可选)

请参考 SDK 版本选择,找到与您 Go 版本相匹配的 SDK 版本。若匹配到的 SDK 版本不是最新版,可参考如下命令锁定 SDK 版本(以 v1.35.0为例):
go mod edit -require=go.opentelemetry.io/otel@v1.35.0
go mod edit -require=go.opentelemetry.io/otel/sdk@v1.35.0
go mod edit -require=go.opentelemetry.io/otel/trace@v1.35.0
go mod edit -require=go.opentelemetry.io/otel/metric@v1.35.0

步骤3:设置环境变量

请参考如下命令指定接入点、应用名和 Token:
export OTEL_EXPORTER_OTLP_ENDPOINT=<endpoint>
export OTEL_SERVICE_NAME=<serviceName>
export OTEL_RESOURCE_ATTRIBUTES=token=<token>
对应的字段说明如下:
<serviceName>:应用名。多个使用相同应用名接入的进程,在 APM 中会表现为同一应用下的多个实例。最长63个字符,只能包含小写字母、数字及分隔符 -,且必须以小写字母开头,以数字或小写字母结尾。
<token>步骤1 中获取的业务系统 Token。
<endpoint>步骤1 中获取的接入点。

步骤4:引入 OpenTelemetry 相关依赖并初始化 SDK

请参考如下代码初始化 OpenTelemetry SDK:
package main

import (
"context"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

var tracer = otel.Tracer("otel-demo") // 全局链路对象,名称可自定义

func setupOTelSDK(ctx context.Context) (func(context.Context) error, error) {
res, err := resource.New(ctx,
resource.WithTelemetrySDK(),
resource.WithHost(),
resource.WithFromEnv(), // 从环境变量获取接入点、应用名等信息
)
if err != nil {
return nil, err
}

traceExporter, err := otlptracegrpc.New(ctx)
if err != nil {
return nil, err
}

tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithBatcher(traceExporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))

return tp.Shutdown, nil
}

步骤5:启动应用

package main

import (
"context"
"errors"
"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: otelhttp.NewHandler(newHTTPHandler(), "server"),
}
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 服务,埋点方式会有所区别,具体请参考社区的 框架列表 查看其他框架的埋点方式。

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

package main

import (
"io"
"net/http"

"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

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)

return mux
}

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

接入验证

启动 Go 应用后,通过8080端口访问对应的接口,例如 http://localhost:8080/simple,应用就会向 APM 上报处理 HTTP 请求相关的链路数据。在有正常流量的情况下,应用性能监控 > 应用列表 中将展示接入的应用,单击应用名称/ID 进入应用详情页。

再选择实例分析,即可看到接入的应用实例。由于可观测数据的处理存在一定延时,如果接入后在控制台没有查询到应用或实例,请等待30秒左右。

更多埋点示例

访问 Redis

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

var rdb *redis.Client

func InitRedis() {
rdb = redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "",
})
if err := redisotel.InstrumentTracing(rdb); err != nil {
panic(err)
}
}
数据访问:
func redisRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
val, err := rdb.Get(ctx, "foo").Result()
if err != nil {
log.Printf("redis get failed: %v", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
fmt.Fprintln(w, "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() {
dsn := "root:******@tcp(127.0.0.1:3306)/db_demo?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
})
if err != nil {
panic(err)
}
// 加入 Trace 上报,需要根据实际情况填入 DBName
if err = db.Use(tracing.NewPlugin(tracing.WithoutMetrics(), tracing.WithDBName("mockdb-mysql"))); err != nil {
panic(err)
}
GormDB = db
}
数据访问:
func gormRequest(ctx context.Context) {
var val string
if err := GormDB.WithContext(ctx).Model(&TableDemo{}).Where("id = ?", 1).Pluck("value", &val).Error; err != nil {
panic(err)
}
fmt.Println("MySQL query result:", val)
}

自定义埋点

用户可以在当前链路上下文中追加自定义 Span,以提升链路数据的丰富度。进行自定义埋点时,需要通过 trace.WithSpanKind() 作为 tracer.Start() 的参数来设置 Span Kind。Span Kind 包括 serverclientinternalconsumerproducer 五种类型,请根据具体业务场景进行设置。下面提供内部方法埋点,以及访问外部资源埋点的代码示例。

内部方法

下面演示在一个 server 类型 Span 里追加一个 internal 类型的 Span。
func entryFunc(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "entryFunc", trace.WithSpanKind(trace.SpanKindServer)) // server span
defer span.End()

internalInvoke(ctx)
io.WriteString(w, "ok")
}

func internalInvoke(ctx context.Context) {
// 创建一个 internal span
_, span := tracer.Start(ctx, "internalInvoke", trace.WithSpanKind(trace.SpanKindInternal)) // internal span
defer span.End()
// 业务逻辑省略
}

访问外部资源

访问外部资源时,一般需要在当前链路上下文中追加一个 client 类型的 Span。
func clientInvoke(ctx context.Context) {
ctx, span := tracer.Start(ctx, "clientInvoke", trace.WithSpanKind(trace.SpanKindClient))
defer span.End()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.example.com", nil)
if err != nil {
span.RecordError(err)
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
span.RecordError(err)
fmt.Println("err occurred when calling external api:", err)
return
}
defer resp.Body.Close()
}

获取当前 Span 上下文

在代码中可以获取当前 Span 的上下文信息,从而添加或修改 Span 属性,或者将获取到的 TraceID/SpanID 输出到日志中。

获取 TraceID/SpanID

func printSpanContext(ctx context.Context) {
spanCtx := trace.SpanContextFromContext(ctx) // 获取当前 span 的上下文
if spanCtx.HasTraceID() {
fmt.Println("traceID:", spanCtx.TraceID().String())
}
if spanCtx.HasSpanID() {
fmt.Println("spanID:", spanCtx.SpanID().String())
}
// 业务逻辑省略
}

设置 Span 属性

先获取 Span 对象,再设置属性。
func gormRequestWithAttrs(ctx context.Context) {
span := trace.SpanFromContext(ctx)
if !span.SpanContext().IsValid() {
fmt.Println("no active span detected")
return
}
span.SetAttributes(
attribute.String("key1", "value1"),
attribute.Int64("key2", 123),
)

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