说明:
OpenTelemetry 是工具、API 和 SDK 的集合,用于检测、生成、收集和导出遥测数据(指标、日志和跟踪),帮助用户分析软件的性能和行为。关于 OpenTelemetry 的更多信息请参考 OpenTelemetry 官方网站。
OpenTelemetry 社区活跃,技术更迭迅速,广泛兼容主流编程语言、组件与框架,为云原生微服务以及容器架构的链路追踪能力广受欢迎。
本文将通过相关操作介绍如何通过社区的 OpenTelemetry-Go 方案接入 Go 应用。OpenTelemetry-Go 提供了一系列 API,用户可以通过 SDK 将性能数据并发送到可观测平台的服务端。本文通过最常见的应用行为,例如 HTTP 服务、访问数据库等,介绍如何基于 OpenTelemetry-Go 接入腾讯云应用性能监控 APM,对于 OpenTelemetry-Go 的更多用法,请参考 项目主页。
前提条件
前置步骤:获取接入点和 Token
1. 登录 腾讯云可观测平台 控制台。
2. 在左侧菜单栏中选择应用性能监控,单击应用列表 > 接入应用。
3. 在右侧弹出的数据接入抽屉框中,单击 Go 语言。
4. 在接入 Go 应用页面,选择您所要接入的地域以及业务系统。
5. 选择接入协议类型为 OpenTelemetry。
6. 选择您所想要的上报方式,获取您的接入点和 Token。
说明:
内网上报:使用此上报方式,您的服务需运行在腾讯云 VPC。通过 VPC 直接联通,在避免外网通信的安全风险同时,可以节省上报流量开销。
外网上报:当您的服务部署在本地或非腾讯云 VPC 内,可以通过此方式上报数据。请注意外网通信存在安全风险,同时也会造成一定上报流量费用。
接入 Go 应用
步骤1:引入 OpenTelemetry 相关依赖,实现 SDK 初始化逻辑
package mainimport ("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")var (tracer = otel.Tracer("otel-demo") // 这里初始化了一个全局的链路对象,用户可以自定义链路名)func setupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {var shutdownFuncs []func(context.Context) errorshutdown = func(ctx context.Context) error {var err errorfor _, fn := range shutdownFuncs {err = errors.Join(err, fn(ctx))}shutdownFuncs = nilreturn 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>替换为业务系统Tokenattribute.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 mainimport ( "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 }
步骤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 请求相关的链路数据。在有正常流量的情况下,应用性能监控 > 应用列表 中将展示接入的应用,单击应用名称/ID 进入应用详情页,再选择实例监控,即可看到接入的应用实例。由于可观测数据的处理存在一定延时,如果接入后在控制台没有查询到应用或实例,请等待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.DBtype TableDemo struct {ID int `gorm:"column:id"`Value string `gorm:"column:value"`}func InitGorm() {var err errordsn := "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 stringif 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,以提升链路数据的丰富度。进行自定义埋点的时候,需要通过
tracer.WithSpanKind()
设置 Span Kind。Span Kind 包括server
、client
、internal
、consumer
和 producer
五种类型,请根据具体的业务场景进行设置。本文将提供内部方法埋点,以及访问外部资源埋点的代码编写示例。内部方法
这里演示在一个
server
类型 Span 里面,追加一个类型为internal
类型的 Span。func entryFunc(w http.ResponseWriter, r *http.Request) {_, span := tracer.Start(r.Context(), "entryFunc", trace.WithSpanKind(trace.SpanKindServer)) // server spandefer span.End()internalInvoke(r)io.WriteString(w, "ok")}func internalInvoke(r *http.Request) {// 创建一个 Internal Span_, span := tracer.Start(r.Context(), "internalInvoke", trace.WithSpanKind(trace.SpanKindInternal)) // internal spandefer span.End()// 业务逻辑省略}
访问外部资源
访问外部资源,一般需要在当前的链路上下文中追加一个类型为
client
的 Span。func clientInvoke(ctx context.Context) {_, span := tracer.Start(ctx, "child", trace.WithSpanKind(trace.SpanKindClient))defer span.End()_, err := http.Get("https://www.example.com")if err != nil {fmt.Println("err occurred when call extrnal api: ", err)return}}
获取当前 Span 上下文
在代码中,可以获取当前 Span 的上下文信息,从而添加或修改 Span 属性,或者将获取到的 TraceID/SpanID 输出到日志中。
获取 TraceID/SpanID
func internalInvoke(ctx context.Context) {spanCtx := trace.SpanContextFromContext(ctx) // 获取当前 span 的上下文if spanCtx.HasTraceID() {traceID := spanCtx.TraceID()fmt.Println(traceID.String())}if spanCtx.HasSpanID() {spanID := spanCtx.SpanID()fmt.Println(spanID.String())}// 业务逻辑省略}
设置 Span 属性
先获取 span 对象,再设置属性。
func gormRequest(ctx context.Context) {span := trace.SpanFromContext(ctx) // 获取当前的 span 对象if span == nil {fmt.Println("no active span detected")return}span.SetAttributes(attribute.String("key1", "value1"),attribute.Int64("key2", 123),)var val stringif 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)}