OpenTelemetry

OpenTelemetry 是一个开源项目,旨在通过统一的工具集、API和SDK,简化在多样化的技术栈中集成可观测性功能,确保一致地收集、处理及导出应用性能数据。

OpenTelemetry 项目使用介绍

希铭

本文将为你介绍 OpenTelemetry 项目中的各种插桩方案的实现原理,以及如何使用 OpenTelemetry 项目来为你的应用系统提供可观测性,并分别以 Golang 和 Java 为例手把手地演示如何在你的应用中集成 OpenTelemetry 插桩。

如果希望更详细地了解集成步骤与注意事项,欢迎参考:

前言

在阅读本文之前,建议先阅读以下文章,以了解 OpenTelemetry 项目:

如果你希望为你的应用系统带来可观测性,需要先保证这个系统中的各类组件被插桩(instrumented),并基于这些插桩产生和发送各种遥测数据(telemetry data),比如链路(trace)、指标(metrics)和日志(logs)。OpenTelemetry 项目本身提供了多样的插桩解决方案的实现,用于生成规范的遥测数据并按照特定协议上报到代理或可观测后端服务,按照插桩方式可以主要分为两类:

  • 基于代码(code-based)的插桩方案
  • 零代码(zero-code)的插桩方案

除 OpenTelemetry 社区提供的以上两种方案外,部分第三方库(library)提供了库原生的插桩方案。如果希望了解有哪些三方库提供了原生的插桩方案,可以访问 OpenTelemetry Registry

你可以根据需求自由选择任一种插桩方案,也可以多种方案共同使用。

基于代码的插桩方案

OpenTelemetry 针对各种语言提供了 API 和 SDK,并以第三方库的形式发布。在使用基于代码的插桩方案前,需要先将 OpenTelemetry 的 API 和 SDK 引入到你的代码中。对于一个独立的进程或者服务来说,OpenTelemetry 的 API 和 SDK 需要被同时引入。而如果你正在为一个库或者其他会被应用依赖的组件添加可观测性插桩,则只需要引入 API,详情可以参考“库原生的插桩方案”。本章节接下来的内容围绕“为独立的进程或服务集成可观测性”展开。

OpenTelemetry API 定义了产生和发送遥测数据的各种交互接口,SDK 则提供了这些过程的具体实现。在引入 API 和 SDK 以后,需要通过一些配置来初始化链路、指标、日志等遥测数据的提供者(provider)、数据上报的目标端点等一系列行为。

在完成配置后,你就可以通过调用这些遥测数据提供者的 API 来创建各类遥测数据。

遥测数据被创建出来后,会由 SDK 发送到配置好的目标端点。OpenTelemetry 支持两种方式来导出(export)这些遥测数据:

  • 将数据发送到一个可观测性后端服务
  • 将数据发送到 OpenTelemetry Collector 代理

遥测数据的导出过程通过一个或多个导出器来实现,你可以将这些数据按照对应的数据协议上报到遥测数据分析工具中,如 Jaeger、Prometheus 等。同时 OpenTelemetry 还提供了一个通用的 OTLP 协议,允许你将数据上报到 OpenTelemetry Collector 代理,并进一步转发到遥测数据分析工具中,以实现更加复杂或自定义的导出过程。你可以在这里找到所有原生支持 OTLP 上报协议的组织或供应商

OpenTelemetry 社区已经针对以下语言提供了 API 和 SDK:

快速实践 - 以 Go 应用为例

代码仓库:OpenTelemetry Go Example

本节以 otel-collector example 为例,该示例应用只会产生链路数据,并由 OpenTelemetry Collector 聚合出指标数据上报到 Prometheus,遥测数据流如下:

第一步:引入 API & SDK

Terminal window
go get "go.opentelemetry.io/otel" \
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric" \
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace" \
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog" \
"go.opentelemetry.io/otel/sdk/log" \
"go.opentelemetry.io/otel/log/global" \
"go.opentelemetry.io/otel/propagation" \
"go.opentelemetry.io/otel/sdk/metric" \
"go.opentelemetry.io/otel/sdk/resource" \
"go.opentelemetry.io/otel/sdk/trace" \
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"\
"go.opentelemetry.io/contrib/bridges/otelslog"

第二步:初始化 OpenTelemetry SDK

func initProvider() (func(context.Context) error, error) {
ctx := context.Background()
// 1. 创建 Resource
res, err := resource.New(
ctx,
resource.WithAttributes(
// 用于后端调用链路中展示使用的服务名
semconv.ServiceName("test-service"),
),
)
if err != nil {
return nil, fmt.Errorf("failed to create resource: %w", err)
}
// 2. [可选]创建 gRPC 客户端,用于连接到 OpenTelemetry Collector
conn, err := grpc.NewClient("localhost:4317",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, fmt.Errorf("failed to create gRPC connection to collector: %w", err)
}
// 3. 配置链路数据的导出器,这里使用了 OpenTelemetry 提供的 OTLP gRPC 实现
// 你也可以自己实现一个 Exporter 的扩展,从而用指定的协议上报到指定的代理或后端服务
traceExporter, err := otlptracegrpc.New(context.Background(), otlptracegrpc.WithGRPCConn(conn))
if err != nil {
return nil, fmt.Errorf("failed to create trace exporter: %w", err)
}
// 4. 将导出器注册到 Span 的处理器中,并将采样器、资源和处理器注册到链路数据的提供者中
bsp := sdktrace.NewBatchSpanProcessor(traceExporter)
tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithResource(res),
sdktrace.WithSpanProcessor(bsp),
)
// 5. 将链路数据提供者注册到全局
otel.SetTracerProvider(tracerProvider)
// 6. 将默认上下文传播器注册到全局
otel.SetTextMapPropagator(propagation.TraceContext{})
return tracerProvider.Shutdown, nil
}

第三步:在应用代码中创建链路数据

func main() {
log.Printf("Waiting for connection...")
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
// 1. 初始化 provider
shutdown, err := initProvider()
if err != nil {
log.Fatal(err)
}
defer func() {
if err := shutdown(ctx); err != nil {
log.Fatal("failed to shutdown TracerProvider: %w", err)
}
}()
// 2. 获取 tracer
tracer := otel.Tracer("test-tracer")
// 3. 记录 Span 中的属性,这些属性可以被转为指标或者其他类型的遥测数据
commonAttrs := []attribute.KeyValue{
attribute.String("attrA", "chocolate"),
attribute.String("attrB", "raspberry"),
attribute.String("attrC", "vanilla"),
}
// 4. 执行业务逻辑,使用 Start 方法创建 Span,使用 End 方法关闭 Span
// Span 会被 SDK 收集起来,调用导出器上报到代理或后端服务
ctx, span := tracer.Start(
ctx,
"CollectorExporter-Example",
trace.WithAttributes(commonAttrs...))
defer span.End()
for i := 0; i < 10; i++ {
_, iSpan := tracer.Start(ctx, fmt.Sprintf("Sample-%d", i))
log.Printf("Doing really hard work (%d / 10)\n", i+1)
<-time.After(time.Second)
iSpan.End()
}
log.Printf("Done!")
}

第四步:启动应用

效果展示:

Jaeger 的链路数据。

Prometheus 的指标数据(OpenTelemetry Collector 中后聚合得到)。

零代码的插桩方案

OpenTelemetry 项目提供的零代码插桩方案允许你为系统快速集成可观测性,而不需要对代码进行改造。零代码插桩方案会通过字节码增强、猴子补丁(Monkey Patching)或 eBPF 等方式,将 OpenTelemetry 的 API 和 SDK 注入到你的应用中,并自动完成可观测数据的产生和发送过程。

通常来说,零代码插桩方案会为你的所应用引入的三方库添加自动插桩,比如 gRPC、kafka-clients 等,并不会对你的应用的代码进行自动插桩。如果希望为应用中的代码进行插桩,需要使用“基于代码的插桩方案”。

OpenTelemetry 社区已经针对以下语言提供了零代码自动插桩的支持:

快速实践 - 以 Java 应用为例

代码仓库:OpenTelemetry Java Agent Example

本节以 javaagent example 为例,由于 OpenTelemetry 社区提供的 example 只会上报给 OpenTelemetry Collector,并以日志的形式展示出来,这里我们继续采用上一节 example 中使用的 OpenTelemetry Collector 配置,将链路和指标分别上报到 Jaeger 和 Prometheus,忽略日志。

第一步:下载 OpenTelemetry Java Agent(或者编译 Java Agent)

Terminal window
wget https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar

第二步:在启动命令中添加 -javaagent 指令,挂载探针

FROM eclipse-temurin:11-jre
ADD build/libs/app.jar /app.jar
ADD build/agent/opentelemetry-javaagent.jar /opentelemetry-javaagent.jar
ENTRYPOINT java -jar -javaagent:/opentelemetry-javaagent.jar /app.jar

第三步:修改 docker-compose.yaml 及 collector-config.yaml,添加 prometheus.yaml

docker-compose.yaml 的修改中,添加了 prometheus 和 jaeger 作为 OpenTelemetry Collector 的目标服务,并将 Agent 中的上报协议改为了 gRPC 协议。

version: '3'
services:
app:
build: ./
environment:
OTEL_SERVICE_NAME: "agent-example-app"
OTEL_EXPORTER_OTLP_ENDPOINT: "http://localhost:4317"
OTEL_LOGS_EXPORTER: "otlp"
OTEL_CONFIG_FILE: "/sdk-config.yaml"
OTEL_EXPORTER_OTLP_PROTOCOL: "grpc"
ports:
- "8080:8080"
volumes:
- ./sdk-config.yaml:/sdk-config.yaml
depends_on:
- otel-collector
# 添加了 prometheus 和 jaeger 作为可观测后端服务
otel-collector:
image: otel/opentelemetry-collector-contrib:0.91.0
command: ["--config=/etc/collector-config.yaml"]
volumes:
- ./collector-config.yaml:/etc/collector-config.yaml
ports:
- 4317:4317
depends_on:
- prometheus
- jaeger
prometheus:
image: prom/prometheus:v2.45.2
volumes:
- ./prometheus.yaml:/etc/prometheus/prometheus.yml
ports:
- 9090:9090
jaeger:
image: jaegertracing/all-in-one:1.52
ports:
- 16686:16686

collector-config.yaml 的修改中,添加了 prometheus 和 jaeger 的上报端点,并声明在 pipelines 中。

receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
extensions:
health_check: {}
exporters:
otlp:
endpoint: jaeger:4317
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:9090
namespace: testapp
debug:
service:
extensions: [health_check]
pipelines:
traces:
receivers: [otlp]
processors: []
exporters: [otlp, debug]
metrics:
receivers: [otlp]
processors: []
exporters: [prometheus, debug]
logs:
receivers: [otlp]
processors: []
exporters: [debug]

prometheus.yaml 配置数据拉取来源为 OpenTelemetry Collector。

scrape_configs:
- job_name: 'otel-collector'
scrape_interval: 5s
static_configs:
- targets: ['otel-collector:9090']

第四步:启动服务并向服务发起请求

Terminal window
curl --location 'http://localhost:8080/ping'

效果展示:

Jaeger 的链路数据。

Prometheus 的指标数据。Java Agent 中预实现了丰富的指标采集能力,如下图即统计了类加载总数的指标信息。

库原生的插桩方案

OpenTelemetry 社区为许多依赖库提供了原生的插桩方案,一般通过调用库中提供的钩子(hook)或者利用动态补丁机制来实现插桩,实现这样机制的代码库可以成为插桩库(instrumentation libraries)。当然,插桩库也可能是依赖库本身的一部分。插桩库可以为依赖库的使用者带来开箱即用的可观测性,并允许依赖库的维护者基于 OpenTelemetry 的 API 自由定制、微调、聚合和扩展可观测数据。

对于依赖库的开发者而言,需要引入 OpenTelemetry API 依赖,并遵循 OpenTelemetry 提供的语义规约(semantic conventions)完成遥测数据的创建,OpenTelemetry 社区为插桩库的开发提供了非常丰富的指引。在不引入 OpenTelemetry SDK 的情况下,API 中所有对象都为默认的空实现,开发者不需要担心 API 对依赖库产生任何影响。

对于依赖库的使用者而言,只需要将插桩库添加到依赖中,并正常初始化 OpenTelemetry SDK(通过手动或者零代码方案),经过简单的配置,就能启用插桩库带来的可观测能力。不同语言使用插桩库的方式也有所差异,你可以在 OpenTelemetry Registry 中找到所有的插桩库并了解他们的使用方式。

OpenTelemetry 的其他优势

OpenTelemetry 并不仅仅带来了各种插桩的实现方案,还具备一系列的扩展能力,帮助你更灵活地建设可观测的系统。

  • 每种遥测数据都可以采用多种方法来创建、处理和导出,这些过程具有高度的扩展性。
  • OpenTelemetry 的进程内上下文传播是内建的,开发者和使用者无需关注上下文的传播过程,只需要专注于各种遥测数据的创建。
  • OpenTelemetry 中的各种遥测数据都可以依照资源和插桩作用域来分组,从而带来更强的聚合分析、对比分析和下钻能力。
  • OpenTelemetry 给每个语言的 API 和 SDK 都定义了统一的规范要求,并为各类遥测数据定义了标准化的命名方式和实现细节,这可以用于跨代码库和跨平台的可观测数据标准化。

参考文章

  1. Getting started for Developers https://opentelemetry.io/docs/getting-started/dev/
  2. Getting started for Ops https://opentelemetry.io/docs/getting-started/dev/
  3. Instrumentation https://opentelemetry.io/docs/concepts/instrumentation/
  4. Zero-code https://opentelemetry.io/docs/concepts/instrumentation/zero-code/
  5. Code-based https://opentelemetry.io/docs/concepts/instrumentation/code-based/
  6. Libraries https://opentelemetry.io/docs/concepts/instrumentation/libraries/
  7. Components https://opentelemetry.io/docs/concepts/components/
  8. OTLP Specification https://opentelemetry.io/docs/specs/otlp/
  9. OpenTelemetry Go API & SDK https://opentelemetry.io/docs/languages/go/

observability.cn Authors 2024 | Documentation Distributed under CC-BY-4.0
Copyright © 2017-2024, Alibaba. All rights reserved. Alibaba has registered trademarks and uses trademarks.
浙ICP备2021005855号-32