opentelementry入门-1-核心概念与demo

1 基本介绍

OpenTelemetry 是一个帮助开发者 监控追踪应用程序 的开源工具。它能收集应用程序的各种数据,比如 日志(应用程序的输出信息)、指标(比如请求数量、CPU 使用率)和 追踪信息(记录请求的整个过程)。这些数据可以用来了解应用的运行状况和性能问题。

OpenTelemetry 对程序员有什么用?

  • 发现问题:通过追踪应用中的请求流程,程序员可以快速找到性能瓶颈或异常。
  • 监控应用健康状态:通过收集应用的日志和性能指标,帮助程序员实时监控应用的运行情况。
  • 优化性能:根据收集的数据,程序员可以发现慢的操作或服务,进行针对性的优化。

2 核心概念

在 OpenTelemetry 中,有几个核心概念对于实现分布式追踪、监控和日志记录至关重要,分别是 Context & PropagationSpan,以及 Signals(包括 Traces、Metrics、Logs、Baggage)。以下是对这些概念的介绍:

2.1 Context & Propagation

  • Context:在分布式系统中,Context 代表一个请求的上下文信息,用于在各个服务、进程或线程之间携带追踪信息。它允许不同服务在处理请求时共享同一条追踪线索。Context 中可以包含追踪的标识信息(如 Trace IDSpan ID),以及一些额外的数据(如 Baggage)。
  • PropagationPropagation 是上下文的传播机制,用于在系统的不同部分之间传递追踪信息。它的主要作用是确保在请求跨越多个服务时,追踪信息能够继续传播,从而实现端到端的分布式追踪。OpenTelemetry 支持多种 Propagation 格式,例如 W3C Trace Context 和 Baggage 规范。

2.2 Span

  • Span 是 OpenTelemetry 中最基本的追踪单位,它表示系统中操作或事件的一个时段。每个 Span 都会记录以下关键信息:
    • 操作名称:描述当前操作或事件的名称。
    • 开始时间和持续时间:记录操作从何时开始、进行了多长时间。
    • Span IDTrace ID:唯一标识这个 Span 及其所属的整个追踪链(Trace)。
    • 父级 SpanSpan 可以是其他 Span 的子级,这样可以形成一个 Span 树,表示请求在各个服务或模块中的调用关系。
    • Attributes:描述 Span 的一些键值对属性,如 HTTP 状态码、请求路径等。
    • Events:记录在 Span 的生命周期中发生的特定事件。
    • Links:可以将当前 Span 与其他 Span 关联。

2.3 Signals (Traces, Metrics, Logs, Baggage)

在 OpenTelemetry 中,Signals(包括 TracesMetricsLogsBaggage)是核心的观测数据类型。每种 Signal 的采集、处理和导出都依赖于 ProviderExporters 等组件,而 SpanTraces 的基础单元。这些组件协作,确保系统的可观测性数据能够被正确采集和传输。以下是对 SignalsProviderExportersSpan 之间关系的详细说明:

2.3.1 Traces (追踪)

Traces 是 OpenTelemetry 中用于跟踪单个请求或事务如何在分布式系统中流动的信号。Trace 由多个 Span 组成,每个 Span 代表一次操作的时间段。

  • Provider: 对于 Traces 来说,TracerProvider 是负责创建 Tracer 对象的组件。Tracer 用于创建和管理 Span。通常在应用程序的初始化过程中会设置一个全局的 TracerProvider
  • Span: Span 是构成 Trace 的基础单位,包含该操作的开始时间、持续时间、父子关系等信息。开发者可以通过 Tracer 创建 Span,标识代码中某一段操作的开始和结束。
  • Exporters: Span 结束后,采集的数据会传递给 ExporterExporter 是将追踪数据(Span)发送到外部系统(如 Jaeger、Zipkin 或其他追踪平台)的组件。通过 Exporter,可以选择如何持久化或分析这些追踪信息。

举例来说,当一个 HTTP 请求到达服务器时,应用会生成一个 Span 来记录这个请求的处理过程。每当请求跨服务时,就会创建新的 Span,并将其与前一个 Span 链接形成完整的 Trace。最终,通过 Exporter,这些 Trace 会被发送到外部平台进行分析。

2.3.2 Metrics (指标)

Metrics 用于监控系统的性能和行为。它们通过定量数据(如请求数、延迟、内存使用量等)来帮助开发人员理解系统的健康状况。

  • Provider: MeterProviderMetrics 数据的核心提供者,它负责创建 Meter 对象。Meter 则是用于创建和记录 Metrics 的接口。
  • Metrics Instruments: Meter 允许开发者定义和操作不同类型的指标工具,例如 Counter(计数器),Histogram(直方图)和 Gauge(测量仪)。这些工具用于采集和聚合系统的运行数据。
  • Exporters: 采集到的 Metrics 需要通过 MetricsExporter 导出到外部监控平台(如 Prometheus、Datadog)。Exporter 可以定期将指标数据推送到外部系统,以便进行持续监控和报警。

例如,应用可以定义一个 Counter 来跟踪总请求数,使用 Histogram 来记录请求的延迟分布。这些数据通过 Exporter 发送到 Prometheus 等平台,可以实时监控应用的运行状态。

2.3.3 Logs (日志)

Logs 是时间序列事件的记录,通常用于捕获应用程序中的具体操作或错误。与 SpanMetrics 结构化的数据不同,日志通常是非结构化的,但在调试和故障排查中非常有价值。

  • Provider: 日志的管理由 LoggerProvider 负责,LoggerProvider 会生成 Logger 对象。开发者可以通过 Logger 记录应用的运行状态、异常信息等。
  • Logs Data Model: OpenTelemetry 的日志数据模型包括时间戳、事件描述和上下文信息,类似于 Span 的一些属性。Logger 可以将这些信息捕获并存储。
  • Exporters: 日志数据通过 LogsExporter 导出到外部系统(如 Elasticsearch、Splunk),从而实现集中化的日志存储与查询。

开发者可以在特定时间点记录日志信息,例如在发生错误时生成一条日志。这些日志可以与 TracesMetrics 一起使用,以帮助更快速地定位系统问题。

2.3.4 Baggage (上下文信息)

BaggageOpenTelemetry 中的一种特殊机制,用于跨服务传播键值对信息。与 SpanMetrics 不同,Baggage 通常不用于直接观测,而是携带与请求相关的元数据。

  • Propagation: Baggage 的数据随着 Trace 在分布式系统中传播。每个服务都可以在处理请求时读取或修改这些数据。Baggage 可以帮助服务之间共享一些全局上下文信息,例如用户 ID、地理位置等。
  • Provider: Baggage 并没有专门的 Provider,它作为 Context 机制的一部分,通常与 Trace 一起传播。
  • Exporters: Baggage 数据通常不会直接导出到外部系统,但可以作为 Span 的一部分包含在 Trace 数据中,通过 Trace Exporter 进行传递和分析。

Provider、Exporters 与 Span 的关系:

  • Provider: 在每种信号的上下文中,Provider 是核心管理器,负责生成适当的 Tracer(用于 Traces)、Meter(用于 Metrics)或 Logger(用于 Logs)。Provider 控制整个信号的采集流程,定义了如何创建、记录和传递这些数据。
  • Exporters: 无论是 SpanTraces)、Metrics 还是 Logs,所有的观测数据最终都需要通过 Exporter 发送到外部的观测平台。Exporter 决定了数据的出口方向,比如将 Span 数据发送到 Jaeger,将 Metrics 发送到 Prometheus。
  • Span 的作用: 对于 Traces 而言,Span 是核心单位。它用于记录和表示请求的生命周期和调用链。通过 Span 的聚合,形成完整的 Trace,从而帮助开发者追踪和分析请求在分布式系统中的路径与延迟。

这三者共同组成了 OpenTelemetry 的观测数据流程:**Provider 负责生成观测信号实例,Span 等信号用于记录和标识系统中的操作,Exporter 最终将这些数据发送到外部系统以供分析和监控。**

3 实战

这里我使用Rust语言,使用opentelemetry-rust库,来实现一个简单的demo。官方的Rustdemo太简陋了, 所以这里我基于官方的代码写了一个稍微复杂一点的demo

demo主要演示作用:

  1. 初始化providerexporter
  2. http/同线程的函数之间传递context
  3. eventmetric的记录
  4. 导出数据

demo完整代码: dice_server

3.1 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[package]
name = "dice_server"
version = "0.1.0"
edition = "2021"
publish = false

[[bin]]
name = "dice_server"
path = "src/dice_server.rs"
doc = false

[dependencies]
tokio = { version = "1.40", features = ["full"] } # 异步运行时,提供全功能支持
actix-web = "4" # Web 框架
rand = { version = "0.8" } # 随机数生成库

# OpenTelemetry API,用于分布式追踪和度量
opentelemetry = "0.26.0"
# OpenTelemetry SDK,包含 tokio 运行时支持
opentelemetry_sdk = { version = "0.26", features = ["rt-tokio"] }
# OpenTelemetry OTLP 导出器,使用 tonic 作为 gRPC 客户端
opentelemetry-otlp = { version = "0.26", features = ["tonic"] }
# OpenTelemetry 语义约定,定义标准属性和资源
opentelemetry-semantic-conventions = { version = "0.26" }
# OpenTelemetry HTTP 集成
opentelemetry-http = "0.26"

# Actix Web 客户端,用于发送 HTTP 请求
awc = "3.0"
# HTTP 类型和定义
http = "1.1"
lazy_static = "1.4"

3.2 初始化providerexporter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
fn init_meter_provider() -> Result<opentelemetry_sdk::metrics::SdkMeterProvider, MetricsError> {
opentelemetry_otlp::new_pipeline()
// 使用 Tokio 运行时来处理异步操作
.metrics(runtime::Tokio)
// 设置指标收集和导出的周期为5秒
.with_period(Duration::from_secs(5))
// 配置 OTLP 导出器
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic() // 使用 Tonic 作为 gRPC 客户端
.with_endpoint("http://localhost:4317"), // 设置 OTLP 接收器的地址
)
// 构建并返回 MeterProvider
.build()
}

// 初始化追踪提供者 (Tracer Provider),该函数返回一个全局的 `TracerProvider`
fn init_tracer_provider() -> Result<opentelemetry_sdk::trace::TracerProvider, TraceError> {
opentelemetry_otlp::new_pipeline()
.tracing()
// 配置一个 OTLP 导出器,用于将追踪数据发送到指定的后端(在这里是 Jaeger 或 OpenTelemetry Collector)
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic() // 使用 Tonic 作为 gRPC 客户端
.with_endpoint("http://localhost:4317"), // 指定 OTLP 接收器的地址
)
// 配置追踪器的资源信息,例如服务名称等
.with_trace_config(
sdktrace::Config::default().with_resource(Resource::new(vec![KeyValue::new(
SERVICE_NAME,
"tracing-jaeger", // 设置服务名称为 "tracing-jaeger"
)])),
)
// 使用批量处理器进行追踪数据的导出,`runtime::Tokio` 用于支持异步操作
.install_batch(runtime::Tokio)
}

// 初始化全局追踪器,将 `TracerProvider` 和 `MeterProvider` 设置为全局
fn init_tracer() {
// 初始化并设置全局 TracerProvider
let tracer_provider = init_tracer_provider().expect("Failed to initialize tracer provider.");
global::set_tracer_provider(tracer_provider);

// 初始化并设置全局 MeterProvider
let meter_provider = init_meter_provider().expect("Failed to initialize meter provider.");
global::set_meter_provider(meter_provider);

// 设置全局传播器(propagator)
// TraceContextPropagator 用于在分布式系统中传播追踪上下文
global::set_text_map_propagator(TraceContextPropagator::new());
}

3.3 http之间传递context

http之间传递context的本质是将context信息注入到httpheader中, 官方提供的demo使用的是hyper库, 其注入过程很简单, 但这里我使用日常开发中更常用的actix-web库, 所以这里需要自己实现http之间的context传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use opentelemetry::Context;
use awc::http::header::{HeaderMap, HeaderName, HeaderValue};

fn inject_context(request: &mut HeaderMap, cx: &Context) {
// 使用 OpenTelemetry 的 HTTP 传播器 (propagator) 注入追踪上下文到 HTTP 请求头

let mut r_headers = http::HeaderMap::new();

global::get_text_map_propagator(|propagator| {
propagator.inject_context(&cx, &mut HeaderInjector(&mut r_headers));
});

println!("randnum: r_headers: {:?}", &r_headers);

for (key, value) in r_headers.iter() {
let header_name = HeaderName::from_str(key.as_str()).unwrap();
let header_value = HeaderValue::from_str(value.to_str().unwrap()).unwrap();

request.insert(header_name, header_value);
}
}

这个函数的主要目的是将追踪上下文注入到 HTTP 请求头中,以便在分布式系统中传递追踪信息:

1
2
3
4
let mut request = awc::Client::default().get("http://127.0.0.1:8080/gen_num");

let req_headers = request.headers_mut();
inject_context(req_headers, &cx);

当然, 在另一个http的接收端, 需要将contexthttpheader中提取出来:

1
2
3
4
5
6
7
8
9
10
fn extract_context(req: &HttpRequest) -> Context {
global::get_text_map_propagator(|propagator| {
let mut headers: HashMap<String, String> = HashMap::new();

for (key, value) in req.headers().iter() {
headers.insert(key.to_string(), value.to_str().unwrap().to_string());
}
propagator.extract(&headers)
})
}

接收端路由函数可以如下处理:

1
2
3
4
5
6
#[get("/gen_num")]
async fn gen_num(req: HttpRequest) -> impl Responder {
// 使用 OpenTelemetry 的 HTTP 传播器 (propagator) 从 HTTP 请求头中提取追踪上下文
let parent_cx = extract_context(&req);
...
}

同线程之间传递context
直接传递Context的引用即可

3.4 父子Context关系的生成

不同函数调用之间以及http之间, 需要生成父子Context关系, 这样方便在span中查看调用关系, 这里我实现了一个简单的函数来生成父子Context:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fn get_cx_from_parent_cx<'a>(
tracer_name: String,
spam_name: String,
parent_cx: Option<&Context>, // 父`Context`为None时表示该`Context`为顶层`Context`
) -> Context {
let span;
let tracer = global::tracer(tracer_name);
match parent_cx {
Some(cx) => {
// 使用提取到的上下文作为父上下文,创建一个新的 span
span = tracer
.span_builder(spam_name)
.with_kind(SpanKind::Server)
.start_with_context(&tracer, cx); // start_with_context 方法会创建一个子`Span`
}
None => {
span = tracer
.span_builder(spam_name)
.with_kind(SpanKind::Server)
.start(&tracer);
}
}

Context::current_with_span(span) // 返回当前`Context`,其中包含新创建的`Span`
}

3.5 记录eventmetric

3.5.1 event记录事件

使用Context.span方法即可对event进行记录或操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#[get("/gen_num")]
async fn gen_num(req: HttpRequest) -> impl Responder {
// 使用 OpenTelemetry 的 HTTP 传播器 (propagator) 从 HTTP 请求头中提取追踪上下文
let parent_cx = extract_context(&req);

...

let cx = get_cx_from_parent_cx(
"dice_server".to_string(),
"gen_num".to_string(),
Some(&parent_cx),
);

...

cx.span().add_event(
"Generated random number",
vec![opentelemetry::KeyValue::new(
"number",
random_number.to_string(),
)],
);

...
}

3.5.2 metric记录指标

metric需要先初始化指标, 这里以Counter为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义一个结构体来保存我们的计数器
struct HttpMetrics {
success_counter: Counter<u64>,
failure_counter: Counter<u64>,
}

// 使用 lazy_static 创建一个全局的 HttpMetrics 实例
lazy_static! {
static ref HTTP_METRICS: Arc<HttpMetrics> = Arc::new({
let meter = global::meter("http_metrics");
HttpMetrics {
success_counter: meter
.u64_counter("http_requests_success")
.with_description("成功的 HTTP 请求总数")
.init(),
failure_counter: meter
.u64_counter("http_requests_failure")
.with_description("失败的 HTTP 请求总数")
.init(),
}
});
}

路由函数的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#[get("/randnum")]
async fn randnum() -> impl Responder {
...
match request.send().await {
Ok(mut response) => match response.body().await {
Ok(body) => {
HTTP_METRICS.success_counter.add(1, &[]);
cx.span().add_event("从 gen_num 收到响应", vec![]);
HttpResponse::Ok().body(body)
}
Err(_) => {
HTTP_METRICS.failure_counter.add(1, &[]);
cx.span().add_event("读取响应体失败", vec![]);
HttpResponse::InternalServerError().body("读取响应体失败")
}
},
Err(_) => {
HTTP_METRICS.failure_counter.add(1, &[]);
cx.span().add_event("发送请求失败", vec![]);
HttpResponse::InternalServerError().body("发送请求失败")
}
}
}

3.6 导出数据和可视化

这里借助jaeger来可视化数据, 可以使用docker来快速启动jaeger服务:

1
docker run -d -p16686:16686 -p4317:4317 -e COLLECTOR_OTLP_ENABLED=true jaegertracing/all-in-one:latest

访问http://127.0.0.1:16686即可看到可视化界面:
jaegertracing

4 总结

OpenTelemetry 是一个功能强大的分布式追踪和度量库,它提供了一套统一的 API 和 SDK,支持多种语言和平台。通过 OpenTelemetry,开发者可以方便地在分布式系统中进行追踪和监控,从而更好地理解和优化系统性能。

这里仅仅演示了opentelemetry的冰山一角, 更多功能可以参考官方文档以及后续的更新