腾讯云可观测平台 通过 OpenTelemetry SDK 接入 Go 应用(推荐)

如何基于 OpenTelemetry SDK 将 Go 应用接入腾讯云应用性能监控 APM,把应用的可观测数据上报到服务端。文中以最常见的应用行为(如 HTTP 服务、访问数据库等)为例进行说明。关于 OpenTelemetry SDK 的更多用法,请参考 项目主页。

相关产品
腾讯云可观测平台 产品页,含版本规格、价格与购买入口。

注意:说明:

注意:OpenTelemetry 是工具、API 和 SDK 的集合,用于检测、生成、收集和导出遥测数据(指标、日志和链路),帮助用户分析软件的性能和行为。关于 OpenTelemetry 的更多信息请参考 OpenTelemetry 网站。

一、前提条件

请使用 Go 1.18及以上版本。

二、步骤1:获取接入点和 Token

1. 登录 腾讯云可观测平台 控制台。

2. 在左侧菜单栏中选择应用性能监控 > 应用列表,单击接入应用。

3. 在右侧弹出的接入应用抽屉框中,单击 Go 语言。

4. 在接入 Go 应用页面,选择你所要接入的地域以及业务系统。

5. 选择接入协议类型为 OpenTelemetry。

6. 选择你所需要的上报方式,获取你的接入点和 Token。

注意:说明:

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

请参考 SDK 版本选择,找到与你 Go 版本相匹配的 SDK 版本。若匹配到的 SDK 版本不是最新版,可参考如下命令锁定 SDK 版本(以 v1.35.0为例):

go mod edit -require=go.opentelemetry.io/otel@v1.35.0go mod edit -require=go.opentelemetry.io/otel/sdk@v1.35.0go mod edit -require=go.opentelemetry.io/otel/trace@v1.35.0go 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 mainimport ( "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 mainimport ( "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 mainimport ( "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.Clientfunc 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.DBtype 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 包括 server、client、internal、consumer 和 producer 五种类型,请根据具体业务场景进行设置。下面提供内部方法埋点,以及访问外部资源埋点的代码示例。

十二、内部方法

下面演示在一个 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)}

赞 (0)
上一篇 4天前
下一篇 4天前