Opentelemetry 介绍

2 阅读2分钟

Opentelemetry 介绍

OpenTelemetry 提供了一套 API、库、代理和采集器,帮助开发者收集、处理和导出分布式系统的可观测性数据。

腾讯云可观测APM服务,就接受OTLP协议的数据上报,可以实现链路跟踪,应用性能,接口耗时等颗粒度很细的可观测数据。

项目地址:github.com/open-teleme…

Opentelemetry SDK 初始化

func setupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
   var shutdownFuncs []func(context.Context) error

   shutdown = func(ctx context.Context) error {
      var err error
      for _, fn := range shutdownFuncs {
         err = errors.Join(err, fn(ctx))
      }
      shutdownFuncs = nil
      return 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>替换为业务系统Token
         attribute.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
}

http Server 接入

实现 Handler中间件

  1. 从request header 中获取trace信息,和request context 合并成最新的一个context ctx
  2. 使用最新的context ctx 启动 trace,获得一个带trace信息的contextspanCtx
  3. spanCtx 重新注入到下一个request header中,串联起整个链路。
func TraceHandler(serviceName, path string, opts ...TraceOption) func(http.Handler) http.Handler {
   var options traceOptions
   for _, opt := range opts {
      opt(&options)
   }

   ignorePaths := collection.NewSet()
   ignorePaths.AddStr(options.traceIgnorePaths...)

   return func(next http.Handler) http.Handler {
      tracer := otel.Tracer(TraceName)
      propagator := otel.GetTextMapPropagator()

      return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
         spanName = path
         if len(spanName) == 0 {
            spanName = r.URL.Path
         }

         if ignorePaths.Contains(spanName) {
            next.ServeHTTP(w, r)
            return
         }

         ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
         spanCtx, span := tracer.Start(
            ctx,
            spanName,
            oteltrace.WithSpanKind(oteltrace.SpanKindServer),
            oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(
               serviceName, spanName, r)...),
         )
         defer span.End()

         propagator.Inject(spanCtx, propagation.HeaderCarrier(w.Header()))

         next.ServeHTTP(w, r.WithContext(spanCtx))

         span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(http.StatusOK)...)
         span.SetStatus(semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(
            http.StatusOK, oteltrace.SpanKindServer))
      })
   }
}

http request 接入

将request context中的trace信息写入 propagator header 中传播。

func Request(r *http.Request) (*http.Response, error) {
   ctx := r.Context()
   tracer := TracerFromContext(ctx)
   propagator := otel.GetTextMapPropagator()

   spanName = r.URL.Path
   ctx, span := tracer.Start(
      ctx,
      spanName,
      oteltrace.WithSpanKind(oteltrace.SpanKindClient),
      oteltrace.WithAttributes(semconv.HTTPClientAttributesFromHTTPRequest(r)...),
   )
   defer span.End()

   r = r.WithContext(ctx)
   propagator.Inject(ctx, propagation.HeaderCarrier(r.Header))

   resp, err := http.DefaultClient.Do(r)

   if err != nil {
      span.RecordError(err)
      span.SetStatus(codes.Error, err.Error())
      return resp, err
   }

   span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...)
   span.SetStatus(semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(resp.StatusCode, oteltrace.SpanKindClient))

   return resp, err
}

Kong网关接入

腾讯云 kong网关目前暂不支持,已经提交安灯需求单(andon.woa.com/micro/base/…

服务接入

因为项目是使用go-zero 框架,只需要在项目中增加配置文件,服务启动时候配置对应的 opentelemetry endpoint和 token就行,也可以使用

  • 服务配置文件开启opentelemetry endpoint
Telemetry:
  Name: urp-api
  Endpoint: pl.ap-shanghai.apm.tencentcs.com:4317
  Batcher: otlpgrpc
  • 服务deployment 文件配置 opentelemetry token, 以configmap 方式挂载到container的环境变量中即可。
apiVersion: v1
data:
  OTEL_RESOURCE_ATTRIBUTES: token=xxxxx
kind: ConfigMap
metadata:
  name: apm-resource-config
  namespace: {{Namespace}}

MySQL 接入

trace定义:

var sqlAttributeKey = attribute.Key("sql.method")

func startSpan(ctx context.Context, method string) (context.Context, oteltrace.Span) {
   tracer := trace.TracerFromContext(ctx)
   start, span := tracer.Start(ctx, spanName, oteltrace.WithSpanKind(oteltrace.SpanKindClient))
   span.SetAttributes(sqlAttributeKey.String(method))

   return start, span
}

func endSpan(span oteltrace.Span, err error) {
   defer span.End()

   if err == nil || errors.Is(err, sql.ErrNoRows) {
      span.SetStatus(codes.Ok, "")
      return
   }

   span.SetStatus(codes.Error, err.Error())
   span.RecordError(err)
}

trace接入:

    ctx, span := startSpan(ctx, "Exec")
    defer func() {
    endSpan(span, err)
    }()
    //  your work code here

MongoDB 接入

trace 定义:

var mongoCmdAttributeKey = attribute.Key("mongo.cmd")

func StartSpan(ctx context.Context, cmd string) (context.Context, oteltrace.Span) {
   tracer := trace.TracerFromContext(ctx)
   ctx, span := tracer.Start(ctx, spanName, oteltrace.WithSpanKind(oteltrace.SpanKindClient))
   span.SetAttributes(mongoCmdAttributeKey.String(cmd))

   return ctx, span
}

func EndSpan(span oteltrace.Span, err error) {
   defer span.End()

   if err == nil || errorsx.In(err, mongo.ErrNoDocuments, mongo.ErrNilValue, mongo.ErrNilDocument) {
      span.SetStatus(codes.Ok, "")
      return
   }

   span.SetStatus(codes.Error, err.Error())
   span.RecordError(err)
}

trace 使用:

   ctx, span := trace.StartSpan(p.ctx, "GetProductOneSpec")
   defer func() {
      trace.EndSpan(span, err)
   }()
   // your work code

Redis 接入

trace定义:

var redisCmdsAttributeKey = attribute.Key("redis.cmds")

func startSpan(ctx context.Context, cmdStrs string) (context.Context, oteltrace.Span) {
   tracer := trace.TracerFromContext(ctx)
   start, span := tracer.Start(ctx, spanName, oteltrace.WithSpanKind(oteltrace.SpanKindClient))
   span.SetAttributes(redisCmdsAttributeKey.StringSlice(cmdStrs))
   return start, span
}

func endSpan(span oteltrace.Span, err error) {
   defer span.End()

   if err == nil || errors.Is(err, red.Nil) {
      span.SetStatus(codes.Ok, "")
      return
   }

   span.SetStatus(codes.Error, err.Error())
   span.RecordError(err)
}

trace接入:

    ctx, span := startSpan(ctx, "set")
    defer func() {
    endSpan(span, err)
    }()
    //  your work code here