暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Grafana全家桶(四)链路跟踪Demo-Go代码集成opentelemetry SDK

栋总侃技术 2023-10-22
207

上一章节通过部署Tempo、Grafana Agent和Grafana完成链路跟踪整体服务的搭建。同时实现了一个Go程序Demo集成opentelemetry SDK完成链路的接入和验证,这一节将向大家介绍如何用Go语言集成opentelemetry SDK。

OpenTelemetry-Go

在opentelemetry官网中的接入指导中有Go语言集成一步步介绍如何使用一个基本的net/http应用程序集成openteletry。

代码中需要集成以下库,可以事先通过go get命令拉取。

  "go.opentelemetry.io/otel" 
  "go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" 
  "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" 
  "go.opentelemetry.io/otel/sdk/metric" 
  "go.opentelemetry.io/otel/sdk/resource" 
  "go.opentelemetry.io/otel/sdk/trace" 
  "go.opentelemetry.io/otel/semconv/v1.21.0" 
  "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"

在OpenTelemetry-Go Githhub仓库有着更多的集成示例,大家也可以进行参考GO集成示例:https://github.com/open-telemetry/opentelemetry-go/tree/main/example

与官网使用基本的net/http应用程序集成openteletry指导不同,接下来讲解的Go Demo将会涉及到Web服务常用的基础库,如Gin、Gorm、go-redis如何集成opentelemetry,而有关可以用于扩展OpenTelemetry的库、插件、集成和其他有用工具大家可以在当前链接查找:https://opentelemetry.io/ecosystem/registry/?component=instrumentation&language=go

大家可以先花3到5分钟阅读opentelemetry官网的Go语言集成示例,可以更好的理解接下来讲解的Go Demo接入过程。

如何构建Trace和Span关系

在开始介绍Demo前大家有必要了解如何构建Trace和Span关系,一个完整的Trace由若干个Span构成,一个Span可以是一条调用链路中的函数的调用、数据库的操作或者微服务之间的调用等等。

创建TracerProvider

TracerProvider用于创建Trace,在TracerProvider中定义Trace数据如何输出,按照怎样的采样策略输出。在理解如何构建Trace和Span的关系时不考虑将Trace数据发送至后端服务,可以先默认使用控制台输出Trace和Span。采样策略也采用最常见的全采样方式

# 输出到控制台的Exporter
function DisplayWindowSize(){
  exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
}

#全采样
tp := trace.NewTracerProvider(
  trace.WithBatcher(exporter),
  trace.WithSampler(trace.AlwaysSample()),
)

otel.SetTracerProvider(tp)

创建Trace和Span

Trace由若干Span构成,同时每个Span也可以拥有自己的子Span,一直嵌套下去。每个pan除了可以设置名称以外,还可以设置一些例如参数调用等标识性属性。

func doWork(ctx context.Context) {
  tracer := otel.Tracer("my-package-name")

  ctx, span := tracer.Start(ctx, "doWork")
  span.SetAttributes(attribute.String("key""value"))
  defer span.End()

  doSubWork(ctx)
}

func doSubWork(ctx context.Context, p1, p2 string) {
  ctx, span := tracer.Start(ctx, "doSubWork")
  span.SetAttributes(attribute.String("p1", p1))
  span.SetAttributes(attribute.String("p2", p2 ))
  defer span.End()
}

接下来我们将开始Go 程序集成Opentelemetry SDK的完整过程讲解。

初始化Opentelemetry

初始化Opentelemetry与上述与创建TracerProvider一样,只是Demo中会将Trace发送至链路后端,且需要设置应用的资源配置,例如服务名称、运行的Namespace、运行的示例名称(一般服务都会以多实例运行,标识是哪个实例)等信息。

构造Exporter

我们可以构造一个GRPC Exporter用于将指标数据发送至指定的后端服务,在服务的配置文件中需要对后端服务的GRPC地址进行配置。

func newGrpcExporterAndSpanProcessor(ctx context.Context) (*otlptrace.Exporter, sdktrace.SpanProcessor) {
  traceExporter, err := otlptrace.New(
    ctx,
    otlptracegrpc.NewClient(
      otlptracegrpc.WithInsecure(),
      otlptracegrpc.WithEndpoint(GRPC_ENDPOINT),

      otlptracegrpc.WithDialOption(grpc.WithBlock()),
      otlptracegrpc.WithCompressor(gzip.Name)),
  )

  if err != nil {
    log.Fatalf("%s: %v""Failed to create the OpenTelemetry trace exporter", err)
  }

  batchSpanProcessor := sdktrace.NewBatchSpanProcessor(traceExporter)

  return traceExporter, batchSpanProcessor
}

构造TracerProvider

在上面已经介绍了TracerProvider用于定义Trace数据如何输出,按照怎样的采样策略输出,同时也需要设置服务的资源信息,例如服务名称、命名空间等标识信息。

首先我们设置资源信息

// 设置应用资源
func newResource(ctx context.Context, serviceName string) *resource.Resource {
  // hostname默认值为本机主机名
  hostName, _ := os.Hostname()
  namespace := os.Getenv("NAMESPACE")

  r, err := resource.New(
    ctx,
    resource.WithFromEnv(),
    resource.WithProcess(), // runtime信息 process.runtime.name: go/gc, process.runtime.version: go1.20.1s
    resource.WithTelemetrySDK(),
    resource.WithHost(),
    resource.WithAttributes(
      semconv.ServiceNameKey.String(serviceName),
      semconv.HostNameKey.String(hostName),
      semconv.K8SNamespaceNameKey.String(namespace),
    ),
    resource.WithContainer(),
  )

  if err != nil {
    log.Fatalf("%s: %v""Failed to create OpenTelemetry resource", err)
  }
  return r
}

最后使用上述内容初始化Opentelemetry,同时设置采样策略为全采样方式。

func InitOpenTelemetry(serviceName string) {
  ctx := context.Background()

  traceExporter, batchSpanProcessor := newGrpcExporterAndSpanProcessor(ctx)
  otelResource := newResource(ctx, serviceName)

  traceProvider := sdktrace.NewTracerProvider(
    sdktrace.WithSampler(sdktrace.AlwaysSample()),
    sdktrace.WithResource(otelResource),
    sdktrace.WithSpanProcessor(batchSpanProcessor))

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

最后在服务关闭前需要对资源进行释放

func CloseOpenTelemetry() {
    cxt, cancel := context.WithTimeout(ctx, time.Second)
    defer cancel()
    if err := traceExporter.Shutdown(cxt); err != nil {
      otel.Handle(err)
    }
  }

Gin扩展otelgin

在初始化Opentelemetry完成后,可以通过Opentelemetry提供的库扩展现有的常见使用库,使其具备产生Trace的功能。

对gin扩展的Opentelemetry库Github地址: go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin

使用otelgin扩展gin相较于原有使用方式只是增加一行中间件的注入:

  r := gin.Default()
  r.Use(otelgin.Middleware(serverName))
  ...

  r.Run(":8080")

通过otelgin.Middleware的源码:github.com/gin-gonic/gin/otelgin/gintrace.go,我们可以看到先获取已经初始化完成的TracerProvider

cfg.TracerProvider = otel.GetTracerProvider()

然后将TracerProvider存到gin的Context中

c.Set(tracerKey, tracer)

根据一些属性构建span,属性包括请求类型、请求URL等。同时构建span返回的ctx继续会存储到Request的Context中,在完成htpp请求操作后,会关闭当前span,在context传递到接口实现时,可以一层层的传递下去,形成调用的嵌套span

ctx, span := tracer.Start(ctx, spanName, opts...)
defer span.End()
c.Request = c.Request.WithContext(ctx)

构建函数Span

在实现的Demo中,实现了一个/server1接口,该接口会调用server2,然后server2调用server3的调用拓扑。在sever1接口新建了一个以服务名称命名的span:

func server1(ctx *gin.Context) {

  tr := otel.Tracer("/server1")
  c, span := tr.Start(ctx.Request.Context(), serverName)
  defer span.End()

  log.WithContext(c).WithField("TraceID", span.SpanContext().TraceID()).Error("sending go-server2")

  body, err := utils.SendReuqst(c, "/server2""go-server2""GET""http://server2.go-optl-demo.svc.cluster.local:8080/server2")
  if err != nil {
    ctx.String(http.StatusInternalServerError, body)
    return
  }

  ctx.String(http.StatusOK, body)
}

在Grafana中可以看到这个span及其详细属性:

可以看到在这个span开头有个红色感叹号标识,这是因为我们在代码中模拟输出了Error日志,点击events按钮查看输出的异常日志信息

Gorm扩展otelgorm

与gin集成类似,只需要增加一行配置插件

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

func InitDB() {
  var err error
  DB, err = gorm.Open(mysql.Open("..."), &gorm.Config{})
  if err != nil {
    panic(err)
  }

  if err := DB.Use(otelgorm.NewPlugin()); err != nil {
    panic(err)
  }
}

在使用Gorm进行数据库操作调用时,无需任何处理,在server3中模拟对数据库的查询操作在Trace中可以看到这个span,以及对应的SQL。

在otelgorm中配置插件实现的是注册不同类型的hook,在调用不同的Gorm库函数时,触发对应的hook构造span信息。

go-redis扩展redisotel

与otelgorm类似的hook实现,在使用go-redis的函数操作时会触发对应的hook完成span的构造。而go-redis扩展redisotel也只需要一行集成代码

var client *rdb.Client
...

if err := redisotel.InstrumentTracing(client ); err != nil {
  panic(err)
}

在链路中可以看到redis的span,详情中会有具体的redis操作命令

HTTP Client

在Demo中记录各个服务之间的调用详情的span是手动启动的Span,其中属性添加了请求方法、请求URL等信息

func doSend(ctx context.Context, tr trace.Tracer, funcName, target, method, url string) (string, error) {
  ctx, span := tr.Start(ctx, funcName, trace.WithAttributes(semconv.PeerService(target)))
  defer span.End()
  req, _ := http.NewRequestWithContext(ctx, method, url, nil)

  fmt.Printf("Sending request...\n")

  client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}

  res, err := client.Do(req)
  if err != nil {
    panic(err)
  }
  body, err := io.ReadAll(res.Body)
  _ = res.Body.Close()

  log.WithContext(ctx).WithField("TraceID", span.SpanContext().TraceID()).Info(string(body))

  return string(body), err
}

详细的http调用span信息如下:


到这里就给大家介绍了使用Go集成OpentelemetrySDK的方法,同时带领大家了解了Gin、Gorm、Go-Redis库集成otel功能属性的示例,更多的代码示例可以参考Go集成OPTL示例:https://github.com/open-telemetry/opentelemetry-go/tree/main/example


往期回顾:

Grafana全家桶(三)使用Grafana Agent与Tempo实现链路追踪完整demo

Grafana全家桶(二)链路跟踪Grafana Tempo的介绍和部署

Grafana全家桶(一)Grafana Labs产品介绍

文章转载自栋总侃技术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论