注意:说明:
注意: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)}