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 不可变对象,包含了
TraceID
、SpanID
、Flags
、State
等信息。 - 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
设置上报时的硬性限制,例如Attribute
、Event
、Link
数量限制,同样是为了防止突发大量数据,。
上述的前两者是核心配置,通常 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"))
}