Skip to content

使用 OTel SDK 增强 Go 应用程序

Golang 无侵入式接入链路请参考 通过 Operator 实现应用程序无侵入增强 文档,通过注解实现自动接入链路。

OpenTelemetry 也简称为 OTel,是一个开源的可观测性框架,可以帮助在 Go 应用程序中生成和收集遥测数据:链路、指标和日志。

本文主要讲解如何在 Go 应用程序中通过 OpenTelemetry Go SDK 增强并接入链路监控。

使用 OTel SDK 增强 Go 应用

安装相关依赖

必须先安装与 OpenTelemetry exporter 和 SDK 相关的依赖项。如果您正在使用其他请求路由器,请参考请求路由。 切换/进入到应用程序源文件夹后运行以下命令:

go get go.opentelemetry.io/otel@v1.19.0 \
  go.opentelemetry.io/otel/trace@v1.19.0 \
  go.opentelemetry.io/otel/sdk@v1.19.0 \
  go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin@v0.46.1 \
  go.opentelemetry.io/otel/exporters/otlp/otlptrace@v1.19.0 \
  go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc@v1.19.0

使用 OTel SDK 创建初始化函数

为了让应用程序能够发送数据,需要一个函数来初始化 OpenTelemetry。在 main.go 文件中添加以下代码片段:

import (
    "context"
    "os"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
    "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"
    semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
    "go.uber.org/zap"
    "google.golang.org/grpc"
)

var tracerExp *otlptrace.Exporter

func retryInitTracer() func() {
    var shutdown func()
    go func() {
        for {
            // otel will reconnected and re-send spans when otel col recover. so, we don't need to re-init tracer exporter.
            if tracerExp == nil {
                shutdown = initTracer()
            } else {
                break
            }
            time.Sleep(time.Minute * 5)
        }
    }()
    return shutdown
}

func initTracer() func() {
    // temporarily set timeout to 10s
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    serviceName, ok := os.LookupEnv("OTEL_SERVICE_NAME")
    if !ok {
        serviceName = "server_name"
        os.Setenv("OTEL_SERVICE_NAME", serviceName)
    }
    otelAgentAddr, ok := os.LookupEnv("OTEL_EXPORTER_OTLP_ENDPOINT")
    if !ok {
        otelAgentAddr = "http://localhost:4317"
        os.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", otelAgentAddr)
    }
    zap.S().Infof("OTLP Trace connect to: %s with service name: %s", otelAgentAddr, serviceName)

    traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithInsecure(), otlptracegrpc.WithDialOption(grpc.WithBlock()))
    if err != nil {
        handleErr(err, "OTLP Trace gRPC Creation")
        return nil
    }

    tracerProvider := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(traceExporter),
        sdktrace.WithSampler(sdktrace.AlwaysSample()),
    sdktrace.WithResource(resource.NewWithAttributes(semconv.SchemaURL)))

    otel.SetTracerProvider(tracerProvider)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))

    tracerExp = traceExporter
    return func() {
        // Shutdown will flush any remaining spans and shut down the exporter.
        handleErr(tracerProvider.Shutdown(ctx), "failed to shutdown TracerProvider")
    }
}

func handleErr(err error, message string) {
    if err != nil {
        zap.S().Errorf("%s: %v", message, err)
    }
}

在 main.go 中初始化跟踪器

修改 main 函数以在 main.go 中初始化跟踪器。另外当您的服务关闭时,应该调用 TracerProvider.Shutdown() 确保导出所有 Span。该服务将该调用作为主函数中的延迟函数:

func main() {
    // start otel tracing
    if shutdown := retryInitTracer(); shutdown != nil {
            defer shutdown()
        }
    ......
}

为应用添加 OTel Gin 中间件

通过在 main.go 中添加以下行来配置 Gin 以使用中间件:

import (
    ....
  "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)

func main() {
    ......
    r := gin.Default()
    r.Use(otelgin.Middleware("my-app"))
    ......
}

运行应用程序

  • 本地调试运行

    注意: 此步骤仅用于本地开发调试,生产环境中 Operator 会自动完成以下环境变量的注入。

    以上步骤已经完成了初始化 SDK 的工作,现在如果需要在本地开发进行调试,需要提前获取到 insight-system 命名空间下 insight-agent-opentelemerty-collector 的地址,假设为: insight-agent-opentelemetry-collector.insight-system.svc.cluster.local:4317

    因此,可以在你本地启动应用程序的时候添加如下环境变量:

    OTEL_SERVICE_NAME=my-golang-app OTEL_EXPORTER_OTLP_ENDPOINT=http://insight-agent-opentelemetry-collector.insight-system.svc.cluster.local:4317 go run main.go...
    
  • 生产环境运行

    请参考通过 Operator 实现应用程序无侵入增强只注入环境变量注解 相关介绍,为 deployment yaml 添加注解:

    instrumentation.opentelemetry.io/inject-sdk: "insight-system/insight-opentelemetry-autoinstrumentation"
    

    如果无法使用注解的方式,您可以手动在 deployment yaml 添加如下环境变量:

······
env:
  - name: OTEL_EXPORTER_OTLP_ENDPOINT
    value: 'http://insight-agent-opentelemetry-collector.insight-system.svc.cluster.local:4317'
  - name: OTEL_SERVICE_NAME
    value: "your depolyment name" # (1)!
  - name: OTEL_K8S_NAMESPACE
    valueFrom:
      fieldRef:
        apiVersion: v1
        fieldPath: metadata.namespace
  - name: OTEL_RESOURCE_ATTRIBUTES_NODE_NAME
    valueFrom:
      fieldRef:
        apiVersion: v1
        fieldPath: spec.nodeName
  - name: OTEL_RESOURCE_ATTRIBUTES_POD_NAME
    valueFrom:
      fieldRef:
        apiVersion: v1
        fieldPath: metadata.name
  - name: OTEL_RESOURCE_ATTRIBUTES
    value: 'k8s.namespace.name=$(OTEL_K8S_NAMESPACE),k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)'
······
  1. 修改此值

请求路由

OpenTelemetry gin/gonic 增强

# Add one line to your import() stanza depending upon your request router:
middleware "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"

然后注入 OpenTelemetry 中间件:

router.Use(middleware.Middleware("my-app"))

OpenTelemetry gorillamux 增强

# Add one line to your import() stanza depending upon your request router:
middleware "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux"

然后注入 OpenTelemetry 中间件:

router.Use(middleware.Middleware("my-app"))

gRPC 增强

同样,OpenTelemetry 也可以帮助您自动检测 gRPC 请求。要检测您拥有的任何 gRPC 服务器,请将拦截器添加到服务器的实例化中。

import (
  grpcotel "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
func main() {
  [...]

    s := grpc.NewServer(
        grpc.UnaryInterceptor(grpcotel.UnaryServerInterceptor()),
        grpc.StreamInterceptor(grpcotel.StreamServerInterceptor()),
    )
}

需要注意的是,如果你的程序里面使用到了 Grpc Client 调用第三方服务,你还需要对 Grpc Client 添加拦截器:

    [...]

    conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
        grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
    )

如果不使用请求路由

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

在将 http.Handler 传递给 ServeMux 的每个地方,您都将包装处理程序函数。例如,将进行以下替换:

- mux.Handle("/path", h)
+ mux.Handle("/path", otelhttp.NewHandler(h, "description of path"))
---
- mux.Handle("/path", http.HandlerFunc(f))
+ mux.Handle("/path", otelhttp.NewHandler(http.HandlerFunc(f), "description of path"))

通过这种方式,您可以确保使用 othttp 包装的每个函数都会自动收集其元数据并启动相应的跟踪。

数据库访问增强

Golang Gorm

OpenTelemetry 社区也开发了数据库访问库的中间件,比如 Gorm:

import (
    "github.com/uptrace/opentelemetry-go-extra/otelgorm"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
    panic(err)
}

otelPlugin := otelgorm.NewPlugin(otelgorm.WithDBName("mydb"), # 缺失会导致数据库相关拓扑展示不完整
    otelgorm.WithAttributes(semconv.ServerAddress("memory"))) # 缺失会导致数据库相关拓扑展示不完整
if err := db.Use(otelPlugin); err != nil {
    panic(err)
}

自定义 Span

很多时候,OpenTelemetry 提供的中间件不能帮助我们记录更多内部调用的函数,需要我们自定义 Span 来记录

 ······
    _, span := otel.Tracer("GetServiceDetail").Start(ctx,
        "spanMetricDao.GetServiceDetail",
        trace.WithSpanKind(trace.SpanKindInternal))
    defer span.End()
  ······

向 span 添加自定义属性和事件

也可以将自定义属性或标签设置为 Span。要添加自定义属性和事件,请按照以下步骤操作:

导入跟踪和属性库

import (
    ...
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/trace"
)

从上下文中获取当前 Span

span := trace.SpanFromContext(c.Request.Context())

在当前 Span 中设置属性

span.SetAttributes(attribute.String("controller", "books"))

为当前 Span 添加 Event

添加 span 事件是使用 span 对象上的 AddEvent 完成的。

span.AddEvent(msg)

记录错误和异常

import "go.opentelemetry.io/otel/codes"

// 获取当前 span
span := trace.SpanFromContext(ctx)

// RecordError 会自动将一个错误转换成 span even
span.RecordError(err)

// 标记这个 span 错误
span.SetStatus(codes.Error, "internal error")

参考

有关 Demo 演示请参考: - opentelemetry-demo/productcatalogservice - opentelemetry-collector-contrib/demo