OpenTelemetry 标准详解

2023-10-21 language golang

OpenTelemetry, OTel 是一个中立开源的可观测性框架,用于仪表化、生成、收集和导出诸如跟踪、度量、日志等遥测数据。

简介

平台、厂商无关的协议标准,使得开发更容易添加或替换底层 APM 实现,而且不只是 Trace 相关内容,还包括了指标、日志相关标准,详细可参考 OpenTelemetry Specification 中的内容,以及不同语言的实现 Contrib

如下简单介绍其基本概念。

Span

Span 指的是一个服务调用的跨度,通过 SpanId 标识,假设有多个 span 存在依赖关系如下。

       [Span A]  ←←←(the root span)
           |
    +------+------+
    |             |
[Span B]      [Span C] ←←←(Span C is a `child` of Span A)
    |             |
[Span D]      +---+-------+
              |           |
          [Span E]    [Span F]

大部分的可视化工具都是以时间线的方式进行展示。

––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––––|–––––> time

 [Span A···················································]
   [Span B··········································]
      [Span D······································]
    [Span C····················································]
         [Span E·······]        [Span F··]

这些和 Dapper 中描述的概念没有本质区别,也就是上述的展示,这是构建分布式跟踪的基本单元,详细可以参考 Signals Trace 内容,除了包含名称 (Name)、Parent SpanID (根为空)、开始结束时间等基本内容外,还包含了如下的基本内容:

  • Context 不可变对象,包含了 TraceIDSpanIDFlagsState 等信息。
  • Attribute 用来添加 Span 关联的属性信息,会通过 Semantic Conventions 对命名进行约束。
  • Event 标识 Span 期间某个时间点发生的事件信息,用于提供相关的额外信息。
  • Links 用于跨 Span 关联,常见场景是队列缓存的异步执行,因为不确定任务何时执行但两者存在关联。
  • Status 状态信息,支持 Unset Error Ok 等。

如下是一个简单的示例。

{
  "name": "hello",
  "context": {
    "trace_id": "5b8aa5a2d2c872e8321cf37308d69df2",
    "span_id": "051581bf3cb55c13"
  },
  "parent_id": null,
  "start_time": "2022-04-29T18:52:58.114201Z",
  "end_time": "2022-04-29T18:52:58.114687Z",
  "attributes": {
    "http.route": "some_route1"
  },
  "events": [
    {
      "name": "Guten Tag!",
      "timestamp": "2022-04-29T18:52:58.114561Z",
      "attributes": {
        "event_attributes": 1
      }
    }
  ]
}

跨进程传播

这里实际上包含了两类,一类是如何将进程间的 Span 串联起来,还有就是用户相关的数据,分别通过如下方式实现:

  • Context 基础 Span 信息,对于 HTTP 采用的是 Trace Context 标准。
  • Baggage 用户自定义数据,例如用户ID、会话ID等,可以参考 Docs 中的介绍。

更多可以参考 Propagators API 中的介绍。

SDK

默认提供了多种语言的 SDK 实现,这里以 GoLang 为例介绍相关的概念,SDK 相关的标准可以参考 Trace SDK 中的内容。

TracerProvider

用来采集数据并上传到相应的 Registry (例如 Stdout、Jaeger 等),通常有如下的配置参数:

  • Exporter 用来设置数据上报的目的地,可通过 WithSyncer() WithBatcher() 封装同步或者批量方式,前者适用于调试。
  • Resrouce 指定非临时的底层元数据信息,例如主机名;另外,通过 Attribute 定义与 Span 相关的属性。
  • Sampler 设置采样频率,防止数据量过大。
  • SpanLimit 设置上报时的硬性限制,例如 AttributeEventLink 数量限制,同样是为了防止突发大量数据,。

上述的前两者是核心配置,通常 TracerProvider 只会初始化一次,与应用采用相同的生命周期。上述的 WithSyncer/Batcher 实际是定义不同的 SpanProcessor 实现,前者同步上传,有性能问题但是方便调试;后者则会赞批异步上传,适用于生产环境。

除此之外,还可以通过 WithIDGenerator() 修改 SpanID 的生成方式等。

GoLang

使用时需要提供 TracerProvider 对象,用来定义 exporter 以及相关的属性,如下是一个 GoLang 的示例。

package main

import (
	"context"
	"log"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/sdk/resource"
	sdk "go.opentelemetry.io/otel/sdk/trace"
	"go.opentelemetry.io/otel/semconv/v1.26.0"
	"go.opentelemetry.io/otel/trace"
)

var tracer = otel.Tracer("echo")

func getUser(ctx context.Context, id string) string {
	_, span := tracer.Start(ctx, "getUser", trace.WithAttributes(attribute.String("id", "100")))
	defer span.End()

	if id == "123" {
		return "otelecho tester"
	}
	return "hello"
}

func main() {
	ctx := context.TODO()

	// 创建 exporter 作为测试使用
	exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
	// 可以通过 gRPC/HTTP 上报到 Jaeger,可以添加不同的配置参数
	//exporter, err := otlptracegrpc.New(ctx,
	//	otlptracegrpc.WithEndpoint("localhost:4317"),
	//	otlptracegrpc.WithInsecure(),
	//)
	if err != nil {
		return
	}

	res, err := resource.New(ctx, resource.WithAttributes(semconv.ServiceNameKey.String("test")))
	if err != nil {
		log.Fatal(err)
	}

	tp := sdk.NewTracerProvider(
		sdk.WithSampler(sdk.AlwaysSample()),
		sdk.WithResource(res),
		sdk.WithBatcher(exporter),
	)
	otel.SetTracerProvider(tp)
	otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
		propagation.TraceContext{}, propagation.Baggage{},
	))
	defer func() {
		if err := tp.Shutdown(context.Background()); err != nil {
			log.Printf("Error shutting down tracer provider: %v", err)
		}
	}()

	log.Printf("Name %v\n", getUser(ctx, "123"))
}