OpenTelemetry Learning
Ngôn ngữ và framework

OpenTelemetry cho Rust

Hướng dẫn instrument service Rust với Tokio, tracing-opentelemetry, OTLP và Collector theo trạng thái API hiện tại.

Maturity cần đọc trước

Core traces, metrics và logs của OpenTelemetry Rust đang ở mức Beta và các crate vẫn pre-1.0. Chúng có thể dùng sau khi đánh giá production, nhưng API vẫn có thể breaking giữa release. Mỗi bridge/framework integration có maturity riêng và một số vẫn experimental. Hãy khóa dependency, đọc changelog và kiểm thử upgrade.

Mục lục

Chọn mô hình instrumentation

Rust không có một auto-instrumentation agent bao phủ ecosystem như một số runtime managed. Cách phổ biến là compose các crate tại compile time.

Mục tiêuLựa chọnKhi dùng
Code đã dùng tracingtracing-opentelemetry layerChuyển tracing spans thành OTel spans
Library portableopentelemetry APIKhông sở hữu exporter hoặc global provider
Applicationopentelemetry_sdk + opentelemetry-otlpSở hữu resource, sampling, batching, shutdown
HTTP middlewareFramework/Tower integrationChỉ sau khi kiểm tra maturity và compatibility
Business operation nhỏ#[tracing::instrument] hoặc info_span!Tên/fields ổn định, không chứa PII

Nói ngắn gọn: application cấu hình SDK một lần. Library chỉ phát telemetry. Dùng tracing bridge nếu codebase đã chuẩn hóa trên tracing.

Phân biệt tracing span và OpenTelemetry span

tracing::Span là span của ecosystem tracing. Subscriber/layer quyết định cách xử lý nó. opentelemetry::trace::Span là span theo OpenTelemetry API và có SpanContext dùng cho propagation.

tracing-opentelemetry là bridge. Layer nhận tracing spans và tạo OTel spans trong provider. Extension trait OpenTelemetrySpanExt cho phép gán extracted OTel parent vào một tracing span và lấy OTel context để inject.

#[tracing::instrument] → tracing Span → tracing-opentelemetry Layer → OTel Span

                                              SDK batch exporter → OTLP

Không tạo một OTel span thủ công và một tracing span cho cùng boundary nếu không có chủ đích. Kết quả thường là duplicate hoặc parent hierarchy khó hiểu.

Chạy service Tokio tối thiểu

Ví dụ dùng Tokio và tracing. Nó tạo root request span, một child async span và export qua OTLP/gRPC.

Tạo project và dependency

cargo new rust-checkout
cd rust-checkout
cargo add tokio --features macros,rt-multi-thread,time
cargo add tracing tracing-subscriber tracing-opentelemetry
cargo add opentelemetry --features trace
cargo add opentelemetry_sdk --features trace,rt-tokio
cargo add opentelemetry-otlp --features trace,grpc-tonic

Các lệnh lấy release tương thích hiện tại thay vì chép version pin nhanh lỗi thời. Commit Cargo.lock cho application. Với library, khai báo range có chủ đích và kiểm tra nhiều tổ hợp phiên bản trong CI.

Khởi tạo provider exporter và resource

Tạo src/main.rs:

use std::error::Error;
use std::time::Duration;

use opentelemetry::global;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry::KeyValue;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_sdk::Resource;
use tracing::{info, info_span, Instrument};
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

fn init_telemetry() -> Result<SdkTracerProvider, Box<dyn Error + Send + Sync>> {
    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .with_endpoint(
            std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
                .unwrap_or_else(|_| "http://localhost:4317".into()),
        )
        .build()?;

    let resource = Resource::builder()
        .with_service_name("rust-checkout")
        .with_attributes([KeyValue::new(
            "deployment.environment.name",
            std::env::var("DEPLOYMENT_ENV").unwrap_or_else(|_| "local".into()),
        )])
        .build();

    let provider = SdkTracerProvider::builder()
        .with_resource(resource)
        .with_batch_exporter(exporter)
        .build();

    global::set_tracer_provider(provider.clone());
    let tracer = provider.tracer("rust-checkout");

    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer())
        .with(OpenTelemetryLayer::new(tracer))
        .try_init()?;

    Ok(provider)
}

#[tracing::instrument(name = "payment.authorize", skip_all, fields(payment.method = "test"))]
async fn authorize() {
    tokio::time::sleep(Duration::from_millis(20)).await;
    info!(payment.result = "approved", "payment completed");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
    let provider = init_telemetry()?;

    let request = info_span!("POST /checkout", otel.kind = "server");
    async {
        authorize().await;

        let task = tokio::spawn(
            async { info!("inventory checked"); }
                .instrument(info_span!("inventory.check")),
        );
        task.await?;
    }
    .instrument(request)
    .await;

    provider.shutdown()?;
    Ok(())
}

API builder có thể đổi ở release sau vì crate chưa 1.0. Nếu compiler báo method khác, dùng docs.rs đúng version trong Cargo.lock. Đừng ghép snippets từ hai major/minor release khác nhau.

with_batch_exporter dùng processor nền để giảm latency trên application path. Resource gắn danh tính process vào mọi span. try_init trả lỗi nếu global subscriber đã được cài, thay vì panic âm thầm.

Tạo async work và propagation

#[tracing::instrument] tạo span cho mỗi lần gọi authorize. .instrument(...) gắn span vào future. Đây là điểm quan trọng: future có thể được poll trên nhiều worker threads, nên thread-local guard giữ qua .await là lựa chọn dễ sai.

Task inventory.check có parent đúng vì span được tạo khi request span đang active. Với detached task sống lâu hơn request, cân nhắc tạo trace mới và dùng link thay vì giữ request trace mở về mặt nhân quả.

Chạy chương trình

OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 cargo run

Kỳ vọng Collector nhận POST /checkout, payment.authorizeinventory.check trong cùng trace. Log local vẫn xuất qua fmt layer.

Context qua async task và HTTP

Context propagation truyền SpanContext qua boundary. HTTP dùng W3C traceparent. Tokio task cần instrument future hoặc gán parent rõ ràng.

Task có quan hệ parent child

Ưu tiên .instrument(span) hoặc #[instrument]:

use tracing::{info_span, Instrument};

let span = info_span!("job.enrich");
let handle = tokio::spawn(async move {
    enrich().await;
}.instrument(span));

Tránh giữ let _guard = span.enter(); qua .await. Guard dựa trên lexical scope, trong khi future có thể yield và thread chạy việc khác. Điều này có thể gán sai active span.

Nếu span được tạo ngoài active parent, đặt OTel parent rõ ràng:

use tracing_opentelemetry::OpenTelemetrySpanExt;

let span = tracing::info_span!("consumer.process");
span.set_parent(extracted_context);
async { process_message().await }.instrument(span).await;

Inject và extract HTTP headers

Tạo adapter cho HeaderMap vì propagator dùng trait Injector/Extractor:

use http::HeaderMap;
use opentelemetry::propagation::{Extractor, Injector};

struct HeaderInjector<'a>(&'a mut HeaderMap);
impl Injector for HeaderInjector<'_> {
    fn set(&mut self, key: &str, value: String) {
        if let (Ok(name), Ok(value)) = (key.parse::<http::header::HeaderName>(), value.parse()) {
            self.0.insert(name, value);
        }
    }
}

struct HeaderExtractor<'a>(&'a HeaderMap);
impl Extractor for HeaderExtractor<'_> {
    fn get(&self, key: &str) -> Option<&str> {
        self.0.get(key).and_then(|v| v.to_str().ok())
    }
    fn keys(&self) -> Vec<&str> {
        self.0.keys().map(|k| k.as_str()).collect()
    }
}

Cài propagator một lần ở startup:

use opentelemetry_sdk::propagation::TraceContextPropagator;
opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());

Inject context của tracing span hiện tại:

use tracing_opentelemetry::OpenTelemetrySpanExt;

let context = tracing::Span::current().context();
opentelemetry::global::get_text_map_propagator(|p| {
    p.inject_context(&context, &mut HeaderInjector(&mut headers));
});

Ở server, extract headers rồi gọi server_span.set_parent(context) trước khi instrument handler. Invalid header phải tạo root context an toàn, không làm request panic.

Dependency bổ sung cho adapter

Snippet dùng crate http. Chạy cargo add http nếu framework không re-export đúng type. Trong service thật, ưu tiên middleware đã kiểm thử của framework để tránh inject sau khi request đã được gửi.

Framework và tower ecosystem

Axum, Hyper, Tonic và các framework dựa trên Tower thường dùng TraceLayer hoặc integration ecosystem. Một tracing middleware chỉ tạo tracing span. Bạn vẫn cần tracing-opentelemetry layer để export nó và propagation middleware để extract/inject headers.

tower-otel và các crate tương tự có thể hữu ích, nhưng không phải mọi crate đều thuộc OpenTelemetry project hoặc có cùng maturity. Trước khi chọn:

  1. Kiểm tra owner, release cadence và phiên bản Axum/Tower tương thích.
  2. Xác nhận server span có đúng parent remote.
  3. Xác nhận client inject traceparent trước khi gửi request.
  4. Kiểm tra semantic conventions và route template. Không dùng raw URL có ID làm span name.
  5. Đảm bảo middleware không tạo duplicate với TraceLayer hiện có.

Manual middleware nhỏ, có test propagation rõ ràng, tốt hơn một integration không còn maintain. Nói ngắn gọn: đánh giá crate ecosystem như dependency ứng dụng, không coi nó là stable chỉ vì tên có “otel”.

Metrics và logs

Core metrics và logs của OpenTelemetry Rust đang ở mức Beta, đồng thời crate API vẫn pre-1.0. OTLP metrics cần feature phù hợp, SdkMeterProvider, periodic reader và shutdown riêng. Hãy triển khai sau traces với một counter/histogram ít cardinality. Ví dụ, checkout.durationresult, không có user_id.

Logs Beta không có nghĩa mọi bridge đều cùng mức trưởng thành. tracing events cũng không tự động trở thành OTel log records chỉ vì OTel tracing layer đã được cài. Kiểm tra status của bridge/exporter trong release đã khóa. Có ba lựa chọn rõ ràng:

  • giữ tracing-subscriber fmt/JSON layer cho log pipeline hiện có;
  • thêm trace/span IDs vào structured logs bằng layer hỗ trợ correlation;
  • thử OTel logs bridge trong một rollout riêng và chấp nhận API churn.

Không quảng bá một logs bridge Beta hoặc experimental thành contract stable. Không gửi cùng log qua hai layer nếu điều đó tạo duplicate.

Xác minh bằng Collector

Dùng cấu hình local:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317

processors:
  batch: {}

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

Chạy Collector theo local stack, rồi chạy cargo run. Kiểm tra:

  • resource có service.name=rust-checkout;
  • ba spans dùng cùng trace ID;
  • child spans trỏ tới request span;
  • instrumentation scope là rust-checkout;
  • process shutdown nhưng spans cuối vẫn xuất hiện.

OTLP/gRPC thường dùng 4317. OTLP/HTTP thường dùng 4318 và signal path. Feature grpc-tonic cùng .with_tonic() phải khớp Collector gRPC receiver.

Flush shutdown và lỗi lifecycle

SdkTracerProvider sở hữu batch processor và exporter. Giữ provider sống tới cuối process. Gọi shutdown() khi server đã ngừng nhận request và task đang chạy đã hoàn tất.

Không gọi shutdown trong request handler. Không chỉ dựa vào drop ở process exit. Batch queue có thể còn spans. Trong server thật:

  1. Nhận SIGTERM bằng tokio::signal.
  2. Dừng accept request mới.
  3. Chờ in-flight requests trong deadline.
  4. Shutdown provider ngoài async hot path nếu implementation có thể block.
  5. Để orchestrator grace period lớn hơn drain và export timeout.

Một số release cung cấp force_flush() và trả danh sách kết quả. Kiểm tra mọi kết quả, ghi lỗi ra stderr/health telemetry, rồi shutdown. API cụ thể phải theo version lockfile.

Testing

Unit test không cần Collector. Dùng in-memory exporter từ opentelemetry_sdk::testing hoặc exporter test phù hợp với release đang khóa. Cấu hình simple processor để spans có ngay sau end, hoặc flush batch processor trước assert.

Kiểm tra contract sau:

  • payment.authorize xuất hiện đúng một lần;
  • parent span ID đúng;
  • error được record với status/event mong đợi;
  • fields cardinality cao hoặc PII không được export;
  • remote traceparent hợp lệ tạo đúng parent;
  • invalid traceparent không panic;
  • spawned future vẫn có parent khi chuyển worker thread.

Integration test nên chạy Collector container và assert debug output hoặc dùng backend test. Không assert trace ID cố định. Với tests chạy song song, tránh cài global subscriber/provider nhiều lần. Tạo subscriber scoped bằng tracing::subscriber::with_default, hoặc serialize test thật sự cần global.

Troubleshooting

Triệu chứngNguyên nhân thường gặpHành động
Compile lỗi ở builderSnippet và crates khác releaseĐồng bộ crate versions; đọc docs.rs theo Cargo.lock
Có log nhưng không có traceChỉ cài fmt layerThêm OTel layer và giữ provider sống
Span là rootFuture không instrument hoặc chưa extractDùng .instrument; set_parent trước handler
Parent sai ngẫu nhiênGiữ enter guard qua .awaitDùng Instrument, không giữ guard qua yield
Không thấy spans cuốiBatch provider chưa shutdownDrain tasks rồi shutdown()
UNIMPLEMENTED/connection errorSai protocol hoặc receiverKhớp gRPC 4317 với grpc-tonic
Duplicate spansMiddleware và manual span cùng boundaryChọn một owner; bỏ layer/wrapper trùng
Cardinality tăng mạnhURL/ID nằm trong span name hoặc fieldsDùng route template; allowlist attributes
Test panic subscriber đã tồn tạiGọi global init nhiều lầnScoped subscriber hoặc one-time fixture

Bật internal diagnostics bằng log filter phù hợp cho các crate telemetry trong staging. Không bật verbose toàn hệ thống lâu dài vì có thể tăng chi phí và lộ metadata.

Checklist production

  • Commit Cargo.lock và dùng dependency audit/update có kiểm soát.
  • Ghi rõ crate nào stable về signal và crate nào experimental/incubating.
  • Đặt resource service name, version và environment nhất quán.
  • Chỉ một owner tạo span cho mỗi HTTP/RPC boundary.
  • Dùng route template, không dùng raw path có ID làm span name.
  • Test propagation qua HTTP và tokio::spawn trên multi-thread runtime.
  • Dùng batch exporter với queue/timeout phù hợp memory budget.
  • Cấu hình parent-based sampling và đo overhead bằng load test.
  • Bảo vệ OTLP bằng TLS/auth; không để Collector receiver public ngoài ý muốn.
  • Allowlist fields; cấm secret, body và identifier cardinality cao.
  • Graceful shutdown flush được spans trong thời hạn orchestrator.
  • Giám sát dropped spans, export failures và Collector backpressure.

Nguồn chính thức và bài liên quan

On this page