如何在 Rust 中添加手动代码插桩
手动埋点让你能够精细控制追踪:捕获特定的业务操作、附加自定义属性,或确保失败发生时能在 SigNoz 中带着正确的上下文呈现出来。
适用于 SigNoz Cloud 和自托管部署手动埋点的步骤在各种部署方式下完全一致——区别只在于 OTLP endpoint 和 ingestion key。
前提条件
- 先按照 Rust OpenTelemetry 埋点指南 配置好 tracer provider 和 exporter。
- 本文代码在 Rust 1.75+ 和 OpenTelemetry Rust SDK v0.31.0 下测试通过。
第 1 步:获取 tracer
创建 span 需要先拿到一个 tracer 实例。应该复用 tracer,而不是每次请求都新建:
src/main.rsuse opentelemetry::global;
use opentelemetry::trace::Tracer;
fn get_tracer() -> impl Tracer {
global::tracer("my-service")
}
提示:
- tracer 的命名通常与你的服务或模块名保持一致。
- 可以把 tracer 存到 lazy static 里,或通过应用上下文传递,以便复用。
第 2 步:创建手动 span
使用 in_span 把重要操作包进 span 里:
use opentelemetry::trace::{Tracer, TraceContextExt};
use opentelemetry::{global, KeyValue};
pub fn process_order(order_id: &str) {
let tracer = global::tracer("order-service");
tracer.in_span("process-order", |cx| {
let span = cx.span();
span.set_attribute(KeyValue::new("order.id", order_id.to_string()));
span.set_attribute(KeyValue::new("order.status", "processing"));
// 在这里编写订单处理逻辑
validate_inventory(order_id);
charge_payment(order_id);
});
}
代码块执行完毕时,in_span 闭包会自动结束 span,即使中途发生错误也不例外。
第 3 步:创建嵌套 span
对于包含子步骤的操作,可以创建子 span,它们会自动关联到父 span:
src/checkout_service.rsuse opentelemetry::trace::{Tracer, TraceContextExt};
use opentelemetry::{global, KeyValue};
pub fn checkout(cart_id: &str) {
let tracer = global::tracer("checkout-service");
tracer.in_span("checkout", |cx| {
let span = cx.span();
span.set_attribute(KeyValue::new("cart.id", cart_id.to_string()));
// 验证的子 Span
tracer.in_span("validate-cart", |cx| {
let span = cx.span();
span.set_attribute(KeyValue::new("cart.item_count", 5i64));
// 验证逻辑
});
// 支付的子 Span
tracer.in_span("process-payment", |cx| {
let span = cx.span();
span.set_attribute(KeyValue::new("payment.method", "credit_card"));
// 支付逻辑
});
});
}
在父级 in_span 闭包内创建的子 Span 会自动继承 trace 上下文。
第 4 步:传播上下文
Rust 将当前活跃 Span 存储在每个线程的 Context 中。in_span 会在闭包执行期间将对应 Span 设为当前上下文,因此闭包内部的调用都能正确嵌套。但有两类情况会中断这条传播链:新启动的任务(spawned task)和跨服务调用。关于本步骤涉及的概念,请参阅 上下文传播。
进程内部
在 in_span 闭包外部,需要手动构建并附加上下文。attach 会消费上下文对象并返回一个 ContextGuard,当该守卫被丢弃时会自动恢复之前的上下文:
use opentelemetry::trace::{Span, TraceContextExt, Tracer};
use opentelemetry::{global, Context};
let tracer = global::tracer("order-service");
let span = tracer.start("process-order");
let cx = Context::current_with_span(span);
let _guard = cx.clone().attach();
// process-order 的子 Span,因为 guard 仍然有效
tracer.in_span("validate-cart", |_cx| validate_cart(cart_id));
// 或者显式指定父 Span,无需 guard
let child = tracer.start_with_context("charge-card", &cx);
drop(_guard);
cx.span().end(); // 参见下文说明
Warning释放 ContextGuard 只会恢复之前的上下文,不会结束 span。交给 Context::current_with_span 的 span,其生命周期由持有它的 Context 决定,直到该值被 drop 才结束,这往往发生在 provider 关闭之后。在关闭后结束 span,永远无法送达 SigNoz。
在关闭前自行调用 cx.span().end() 来结束。对 BoxedSpan 调用 .end() 还需要作用域内存在 Span trait,因此要导入 opentelemetry::trace::Span。in_span 闭包形式不存在这两个问题:闭包返回时自动结束 span。
spawn 出来的任务可能运行在不同线程上,上下文不会跟随传递。应在 spawn 前捕获上下文,再用 FutureExt::with_context 包裹 future:
use opentelemetry::trace::FutureExt;
use opentelemetry::Context;
let cx = Context::current();
tokio::spawn(
async move {
// 这里创建的 span 会挂到调用者的 span 下
settle_order().await;
}
.with_context(cx),
);
Warning不要在包含 await 的 spawn 任务内部调用 cx.attach()。attach 返回的 ContextGuard 是刻意设计为 !Send 的。把它跨越 await 持有,会让 future 变成 !Send,导致 tokio::spawn 在编译期报 "future cannot be sent between threads safely"。而 with_context 是在每次 poll 时附加上下文,因此没有 guard 被跨越 await 持有。
跨越服务边界
HashMap<String, String> 同时实现了 Injector 和 Extractor,可以直接作为 carrier 使用:
在两端都设置 span kind。SigNoz 依据 Client 和 Server 构建 service map 及 APM metrics。
use opentelemetry::trace::{SpanKind, TraceContextExt, Tracer};
use opentelemetry::{global, Context};
use std::collections::HashMap;
// 出站:inject 将 traceparent 写入载体
let mut carrier = HashMap::<String, String>::new();
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&Context::current(), &mut carrier);
});
// 此时 carrier 的内容为 {"traceparent": "00-<trace-id>-<span-id>-01"}
// 入站:extract 返回一个 context,用于设置入口 span 的父级
let parent_cx = global::get_text_map_propagator(|propagator| propagator.extract(&carrier));
let span = tracer
.span_builder("handle-charge")
.with_kind(SpanKind::Server)
.start_with_context(&tracer, &parent_cx);
let _guard = Context::current_with_span(span).attach();
opentelemetry-http 库提供了 HeaderInjector 和 HeaderExtractor。在 reqwest 和其他 http 库的客户端中,可直接透传 HeaderMap,无需手动复制到 map 中。
除非通过 global::set_text_map_propagator 注册了传播器,否则 inject_context 和 extract 将不会执行任何操作。请在启动阶段,与 tracer provider 一起设置 opentelemetry_sdk::propagation::TraceContextPropagator::new()。
第 5 步:添加属性
属性是键值对,用于在 SigNoz 中提供过滤和分析的上下文:
src/payment_service.rsuse opentelemetry::trace::TraceContextExt;
use opentelemetry::KeyValue;
pub fn handle_payment(cx: &opentelemetry::Context, amount: f64, currency: &str) {
let span = cx.span();
span.set_attribute(KeyValue::new("payment.amount", amount));
span.set_attribute(KeyValue::new("payment.currency", currency.to_string()));
span.set_attribute(KeyValue::new("payment.method", "credit_card"));
span.set_attribute(KeyValue::new("payment.success", true));
}
支持的属性值类型:
String/&stri64、f64bool- 上述类型的数组
第 6 步:添加事件
事件用于标记 span 内的重要时刻,例如重试、缓存命中或状态变更:
src/cache_service.rsuse opentelemetry::trace::TraceContextExt;
use opentelemetry::KeyValue;
pub fn fetch_from_cache(cx: &opentelemetry::Context, key: &str) -> Option<String> {
let span = cx.span();
// 检查缓存
if let Some(value) = cache_lookup(key) {
span.add_event("cache.hit", vec![
KeyValue::new("cache.key", key.to_string()),
]);
return Some(value);
}
span.add_event("cache.miss", vec![
KeyValue::new("cache.key", key.to_string()),
]);
// 从数据库获取数据并写入缓存
let value = fetch_from_db(key)?;
span.add_event("cache.populated", vec![
KeyValue::new("cache.key", key.to_string()),
KeyValue::new("cache.ttl_seconds", 3600i64),
]);
Some(value)
}
第 7 步:记录错误
在 span 上标记失败,让错误能够在 SigNoz 的错误视图和告警中呈现出来:
src/risky_service.rsuse opentelemetry::trace::{Tracer, TraceContextExt, Status};
use opentelemetry::{global, KeyValue};
pub fn risky_operation() -> Result<(), Box<dyn std::error::Error>> {
let tracer = global::tracer("risky-service");
tracer.in_span("risky-operation", |cx| {
let span = cx.span();
match do_something_risky() {
Ok(result) => {
span.set_status(Status::Ok);
Ok(result)
}
Err(e) => {
// 在 span 上记录错误
span.set_status(Status::error(e.to_string()));
span.record_error(&*e);
span.set_attribute(KeyValue::new("error.type", "RiskyOperationError"));
Err(e)
}
}
})
}
如果需要记录完整的异常详情:
Copyuse opentelemetry::trace::TraceContextExt;
pub fn record_exception(cx: &opentelemetry::Context, error: &dyn std::error::Error) {
let span = cx.span();
span.record_error(error);
span.set_status(opentelemetry::trace::Status::error(error.to_string()));
}
第 8 步:Span links(可选)
使用 span links 来关联存在因果关系的 trace,比如批量处理场景中,一个 span 触发了多个相互独立的操作:
src/batch_processor.rsuse opentelemetry::trace::{Tracer, TraceContextExt, Link, SpanContext};
use opentelemetry::global;
pub fn process_batch(batch_id: &str, source_contexts: Vec<SpanContext>) {
let tracer = global::tracer("batch-processor");
// 创建指向所有源 Span 的 Link
let links: Vec<Link> = source_contexts
.into_iter()
.map(|ctx| Link::new(ctx, vec![], 0))
.collect();
let span = tracer
.span_builder("process-batch")
.with_links(links)
.start(&tracer);
// 在 Span 激活状态下处理批次
// ...
}
验证
- 触发创建手动 Span 的代码路径。
- 在 SigNoz 的 Traces 中,按
service.name过滤