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 状态信息,支持
UnsetErrorOk等。
如下是一个简单的示例。
{
"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"))
}