说明:
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 版本(可选)
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:returncase <-ctx.Done():stop()}err = srv.Shutdown(context.Background())return}
步骤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 上报,需要根据实际情况填入 DBNameif err = db.Use(tracing.NewPlugin(tracing.WithoutMetrics(), tracing.WithDBName("mockdb-mysql"))); err != nil {panic(err)}GormDB = db}
数据访问:
func gormRequest(ctx context.Context) {var val stringif 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 spandefer 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 spandefer 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 stringif err := GormDB.WithContext(ctx).Model(&TableDemo{}).Where("id = ?", 1).Pluck("value", &val).Error; err != nil {panic(err)}fmt.Println("MySQL query result:", val)}