上一个博文中,我提到如何使用 OpenTelemery 的特定语言 API 来收集遥测数据,包含手动和自动的埋点技术,这很重要!但是,收集遥测数据只是解决方案的第一步。
你需要把遥测数据路由转发到其他地方,同时添加额外的元数据信息。这时就轮到 SDK 发挥作用了。
本文讨论 OpenTelemetry SDK 导出链路追踪数据的关键环节:
- TracerProvider:连接 API、SpanProcessor、Resource 和 Exporter。
- Resource:给遥测数据补充服务名、版本等资源元数据。
- Exporter:决定遥测数据发送到哪里,例如 OpenTelemetry Collector。
- Propagator 和 Shutdown:保证跨服务上下文传递,并在进程结束前可靠清理。
链路追踪生产者( Tracer Provider )
链路追踪生产者是 SDK 中一个关键概念。用于将通过 API 收集的遥测数据与其他组件联系起来。在 Go 语言中,TracerProvider 对象只有一个 Tracer 方法的接口,方法签名如下:
Tracer(instrumentationName string, opts ...TracerOption) Tracer
Tracer 方法返回一个实现 Tracer 接口的对象,这个接口也只有一个方法 Start,其方法签名如下:
Start(ctx context.Context, spanName string, opts ...spanStartOption) (context.Context, Span)
样例项目中通过链路追踪生产者创建了跨度( span ):
import "go.opentelemetry.io/otel"
// ...
ctx, span := otel.Tracer(telemetry.TelemetryLibrary).Start(ctx, "get_product_price")
可以发现通过otel.Tracer 查找并创建全局的链路追踪生产者最终返回 Tracer 对象,需要注意要使用链路追踪生产者,其初始化设置是不可缺少的。
Note: 在文中提及是获取‘全局’链路追踪生产者的方法。使用全局链路追踪最简单的一种方式就是调用 otel.Tracer 的 API 。不过实际使用中如果上面方案不满足,还可以通过链路追踪生产者传递给消费者以替代全局查找的方法。
Note: trace 代表整个请求的路径信息、span 代表链路中的具体节点信息
可以把 TracerProvider 理解为 OTel SDK 里的组装点:业务代码通过 API 创建 span,TracerProvider 决定这些 span 如何被采样、处理、添加资源信息,并最终交给 Exporter 发出去。
资源( Resource )
链路追踪生产者还需要配置‘资源’对象,它是元数据信息的一部分。资源是遥测数据产生描述过程或者服务的信息,描述了服务本身的元数据,有助于解析遥测数据。
这是样例项目中购物车服务的‘资源’对象定义:
import (
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
)
// ...
res, err := resource.New(
ctx,
resource.WithAttributes(
semconv.ServiceNameKey.String("cart"),
semconv.ServiceVersionKey.String("v1.0.0"),
),
)
资源对象定义的关键是设置属性参数,OpenTelemetry 已经定义了一些资源属性的键值对,可以参考这篇文档 OTel’s 资源语义约定。 例如,你可以通过上面例子看到,如何定义服务名称和版本号信息。但是可能还有更多信息你需要配置,比如服务自身依赖的资源有哪些;服务运行在云上吗?需要约定不同的属性给不同的云服务供应商;服务运行在 Kubernetes 吗?是的话,这里有份指导手册 Kubernetes 的资源语义约定。
最终样例项目中, 链路追踪数据中 span 都包含这样的‘资源’数据:
Resource labels:
-> service.name: STRING(cart)
-> service.version: STRING(v1.0.0)
Resource 的价值在排障时会非常明显。没有 service.name、service.version 这类信息,后端只能看到一堆 span;有了资源信息,就可以按服务、版本、实例、环境或 Kubernetes 资源维度过滤和聚合。
导出器( Exporter )
既然我们已经创建了资源对象,我们接下来定义一下遥测数据的目的地。
导出器的选择范围很广,可以根据自己的需求选择不同的导出器,不过在当前项目例子中我使用 OpenTelemetry 控制器(会在下一篇细聊),它支持 HTTP 和 gRPC 协议。我选择使用 gRPC 协议和 OTLP 导出器:
import (
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"google.golang.org/grpc"
)
// ...
hostIP := os.Getenv("HOST_IP")
if hostIP == "" {
return nil, fmt.Errorf("unexpected no host IP address for receiver")
}
receiverAddress := fmt.Sprintf("%s:%d", hostIP, 4317)
conn, err := grpc.DialContext(
ctx,
receiverAddress,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
return nil, fmt.Errorf("error creating client connection to collector: %w", err)
}
otlpTraceExporter, err := otlptracegrpc.New(
ctx,
otlptracegrpc.WithGRPCConn(conn),
)
Note: 文中例子是演示的程序,使用的非安全的连接方式来获取数据,不过生产环境中你最起码应该要使用带鉴权的连接方式。
就导出器而言,有多种方式输出结果渠道供你选择,例如:控制台输出(输出到 stdout ), Jaeger (直接发送数据给它), Prometheus 等。使用 OTLP 导出器并将数据发送到 OTel Collector 的好处是,您可以创建数据副本、并行处理数据,并拥有更多控制权(将在下一篇文章中介绍)。
由于使用 OTLP 导出器非常灵活,我们可以根据需要在 Collector 中使用遥测数据(输出到 stdout、发送到 Jaeger 等)。下一篇文章将详细介绍这一点!
导出器选型时可以先回答两个问题:
| 问题 | 影响 |
|---|---|
| 数据是否要直接发送到后端,还是先发送到 Collector? | 决定应用是否和后端系统强绑定 |
| 是否需要复制、加工、过滤或发送到多个目的地? | 如果需要,Collector 通常更合适 |
整合( Tying it all together )
现在我们有了资源(生成遥测数据)和导出器(遥测数据的目的地),我们将它们放在一起形成链路追踪生产者:
tp := trace.NewTracerProvider(
trace.WithSampler(trace.AlwaysSample()),
trace.WithResource(res),
trace.WithSpanProcessor(trace.NewBatchSpanProcessor(otlpTraceExporter)),
)
当链路追踪生产者创建后,我们需要将其设置为全局链路追踪生产者:
import (
"go.opentelemetry.io/otel"
)
// ...
otel.SetTracerProvider(tp)
接下来我们需要设置‘传播’。在后续博文中,将深入讨论传播和附加数据( baggage ,整个链路中传递业务自定义 KV 属性),但现在只需要知道‘传播’可以将 OTel 链路追踪的上下文信息跨多个服务进行传递。让‘分布式’概念在‘分布式链路追踪’中实现。
import (
"go.opentelemetry.io/otel/propagation"
)
// ...
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{}),
)
最后,我们需要调用 TracerProvider.Shutdown 来清理并关闭跨度处理器(在例子中,我们使用批量 span 处理器,按批次将 span 数据进行聚合和批量处理,然后将完整的批处理结果发送给导出器):
defer func() {
if err := tp.Shutdown(context.Background()); err != nil {
fmt.Printf("Error shutting down tracer provider: %v", err)
os.Exit(1)
}
}()
Note: 为了可靠性和可读性,仅通过调用 defer tp.Shutdown(context.Background()) 是不够的,需要处理函数返回的一些错误。
这一段代码把导出链路串起来了:创建资源,创建 OTLP 导出器,把导出器交给批量 SpanProcessor,再把 Resource 和 SpanProcessor 放进 TracerProvider,最后设置为全局 TracerProvider。对生产代码来说,还要关注鉴权、网络超时、重试、关闭时 flush 等可靠性问题。
链路追踪生产者 Python 版( Python tracer provider )
样例项目中大部分服务都用 Go 语言来编写,用 Python 写了一个服务(定价服务)。为了完整起见,以下是如何在 Python 中创建和设置类似的链路追踪生产者的例子:
from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
resource = Resource(attributes={
SERVICE_NAME: "price",
SERVICE_VERSION: "v1.0.0"
})
tracer_provider = TracerProvider(resource=resource)
host_ip = os.environ.get("HOST_IP")
if host_ip is None:
print("Must pass in environment var HOST_IP")
sys.exit(1)
tracer_provider.add_span_processor(span_processor=BatchSpanProcessor(
OTLPSpanExporter(endpoint=f"{host_ip}:4317", insecure=True)
))
trace.set_tracer_provider(tracer_provider)
其中资源、span 处理器和设置全局链路追踪生产者的实现与 Go 描述相同。
导出链路排查清单
如果 OpenTelemetry 埋点后在后端看不到 Trace,可以按顺序检查:
- TracerProvider 是否正确初始化,并设置为全局或传入使用方。
- Resource 中的
service.name是否配置,避免数据进了后端却不好检索。 - Exporter endpoint、协议和端口是否正确,例如 OTLP gRPC 常见端口是示例中的
4317。 - Collector 或后端是否可达,网络、DNS、TLS、鉴权是否正确。
- SpanProcessor 是否在进程退出前完成 flush,
Shutdown错误是否被处理。 - 如果跨服务 trace 断开,再检查 propagator 是否配置。
总结
很棒前进了一步!按照上面步骤实现了,通过 API 获取了遥测数据,并将其从当前组件中被发送到一个导出器,并向其中添加了一些元数据(资源)!接下来我们将了解如何使用 OpenTelemetry 收集器来处理这来数据。
FAQ:OpenTelemetry 导出常见问题
Q1:Exporter 一定要直接连 Jaeger 或 Prometheus 吗?
不一定。本文示例使用 OTLP Exporter 把数据送到 OpenTelemetry Collector,再由 Collector 决定后续目的地。这样能降低应用和后端系统的耦合。
Q2:Resource 和 Span 属性有什么区别?
Resource 描述产生遥测数据的服务或运行环境,例如服务名和版本;Span 属性描述某个具体操作的上下文,例如用户、请求、数据库查询等。
Q3:为什么要显式调用 TracerProvider.Shutdown?
因为批量 SpanProcessor 可能还缓存着未发送的数据。进程退出前调用 Shutdown 可以让 SDK 尝试完成发送并释放资源。
本文翻译自:https://trstringer.com/otel-part3-export/
扩展阅读:
