← 文章 / 编程开发
signoz 7小时前 · 2026-09-23 09:02:09 · 0 阅读

如何在 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.rs
use opentelemetry::global;
use opentelemetry::trace::Tracer;
 
fn get_tracer() -> impl Tracer {
    global::tracer("my-service")
}

提示:

  • tracer 的命名通常与你的服务或模块名保持一致。
  • 可以把 tracer 存到 lazy static 里,或通过应用上下文传递,以便复用。

第 2 步:创建手动 span

使用 in_span 把重要操作包进 span 里:

src/order_service.rs
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.rs
use 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,当该守卫被丢弃时会自动恢复之前的上下文:

src/in_process.rs
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::Spanin_span 闭包形式不存在这两个问题:闭包返回时自动结束 span。

spawn 出来的任务可能运行在不同线程上,上下文不会跟随传递。应在 spawn 前捕获上下文,再用 FutureExt::with_context 包裹 future:

src/spawn.rs
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> 同时实现了 InjectorExtractor,可以直接作为 carrier 使用:

在两端都设置 span kind。SigNoz 依据 ClientServer 构建 service map 及 APM metrics。

src/propagate.rs
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 库提供了 HeaderInjectorHeaderExtractor。在 reqwest 和其他 http 库的客户端中,可直接透传 HeaderMap,无需手动复制到 map 中。

Info

除非通过 global::set_text_map_propagator 注册了传播器,否则 inject_contextextract 将不会执行任何操作。请在启动阶段,与 tracer provider 一起设置 opentelemetry_sdk::propagation::TraceContextPropagator::new()

第 5 步:添加属性

属性是键值对,用于在 SigNoz 中提供过滤和分析的上下文:

src/payment_service.rs
use 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 / &str
  • i64f64
  • bool
  • 上述类型的数组

第 6 步:添加事件

事件用于标记 span 内的重要时刻,例如重试、缓存命中或状态变更:

src/cache_service.rs
use 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.rs
use 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)
            }
        }
    })
}

如果需要记录完整的异常详情:

Copy
use 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.rs
use 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 激活状态下处理批次
    // ...
}

验证

  1. 触发创建手动 Span 的代码路径。
  2. 在 SigNoz 的 Traces 中,按 service.name 过滤
原始来源: signoz

评论 (0)