karawaci.kode

← Semua snippet

Rust Lanjut Performance

Rust tracing + OpenTelemetry export

Setup tracing-subscriber dengan OpenTelemetry export ke OTLP collector. Span otomatis, structured log, distributed trace di Rust.

Dipublikasikan 20 Juli 2026

Rust ekosistem tracing powerful tapi setup-nya nightmare buat pemula. Compose subscriber + layer + filter + OTel exporter butuh hapal banyak crate. Snippet ini setup end-to-end: log JSON ke stdout, span ke OTLP collector (Tempo/Jaeger), dengan filter env-controllable.

Kode

# Cargo.toml
# [dependencies]
# tokio = { version = "1.40", features = ["full"] }
# tracing = "0.1"
# tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "fmt"] }
# tracing-opentelemetry = "0.27"
# opentelemetry = "0.26"
# opentelemetry_sdk = { version = "0.26", features = ["rt-tokio"] }
# opentelemetry-otlp = { version = "0.26", features = ["grpc-tonic", "trace"] }
# opentelemetry-semantic-conventions = "0.26"
# tower-http = { version = "0.5", features = ["trace"] }
# axum = "0.7"
# serde_json = "1"
// observability.rs
use std::time::Duration;

use opentelemetry::{global, trace::TracerProvider as _, KeyValue};
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::{
    propagation::TraceContextPropagator,
    runtime::Tokio,
    trace::{BatchConfig, Config, RandomIdGenerator, Sampler, TracerProvider},
    Resource,
};
use opentelemetry_semantic_conventions::resource;
use tracing::Subscriber;
use tracing_subscriber::{
    fmt,
    layer::SubscriberExt,
    registry::LookupSpan,
    util::SubscriberInitExt,
    EnvFilter, Layer,
};

pub struct ObservabilityConfig {
    pub service_name: String,
    pub service_version: String,
    pub deployment_env: String,
    pub otlp_endpoint: Option<String>,  // None = no export
    pub sampling_ratio: f64,             // 0.0 - 1.0
}

impl Default for ObservabilityConfig {
    fn default() -> Self {
        Self {
            service_name: std::env::var("SERVICE_NAME").unwrap_or_else(|_| "api-rust".to_string()),
            service_version: std::env::var("APP_VERSION").unwrap_or_else(|_| "dev".to_string()),
            deployment_env: std::env::var("ENV").unwrap_or_else(|_| "local".to_string()),
            otlp_endpoint: std::env::var("OTEL_ENDPOINT").ok(),
            sampling_ratio: std::env::var("OTEL_SAMPLE_RATIO")
                .ok()
                .and_then(|s| s.parse().ok())
                .unwrap_or(0.1),
        }
    }
}

pub struct ObservabilityGuard {
    tracer_provider: Option<TracerProvider>,
}

impl Drop for ObservabilityGuard {
    fn drop(&mut self) {
        if let Some(tp) = self.tracer_provider.take() {
            let _ = tp.shutdown();
        }
        global::shutdown_tracer_provider();
    }
}

pub fn init(config: ObservabilityConfig) -> ObservabilityGuard {
    global::set_text_map_propagator(TraceContextPropagator::new());

    let resource = Resource::new(vec![
        KeyValue::new(resource::SERVICE_NAME, config.service_name.clone()),
        KeyValue::new(resource::SERVICE_VERSION, config.service_version.clone()),
        KeyValue::new(resource::DEPLOYMENT_ENVIRONMENT, config.deployment_env.clone()),
    ]);

    // Filter — RUST_LOG override
    let filter = EnvFilter::try_from_default_env()
        .or_else(|_| EnvFilter::try_new("info,hyper=warn,tower=warn"))
        .unwrap();

    // Layer 1: stdout JSON formatter
    let fmt_layer = fmt::layer()
        .json()
        .with_target(true)
        .with_thread_ids(false)
        .with_current_span(true)
        .with_span_list(false);

    // Layer 2 (optional): OpenTelemetry export
    let tracer_provider = if let Some(endpoint) = &config.otlp_endpoint {
        let exporter = opentelemetry_otlp::new_exporter()
            .tonic()
            .with_endpoint(endpoint)
            .with_timeout(Duration::from_secs(5));

        let tp = opentelemetry_otlp::new_pipeline()
            .tracing()
            .with_exporter(exporter)
            .with_trace_config(
                Config::default()
                    .with_resource(resource.clone())
                    .with_sampler(Sampler::TraceIdRatioBased(config.sampling_ratio))
                    .with_id_generator(RandomIdGenerator::default()),
            )
            .with_batch_config(
                BatchConfig::default()
                    .with_max_queue_size(4096)
                    .with_scheduled_delay(Duration::from_secs(5)),
            )
            .install_batch(Tokio)
            .expect("init OTLP tracer");

        global::set_tracer_provider(tp.clone());
        Some(tp)
    } else {
        None
    };

    let otel_layer = tracer_provider.as_ref().map(|tp| {
        tracing_opentelemetry::layer()
            .with_tracer(tp.tracer("rust-tracing-otel"))
    });

    // Compose & init
    let subscriber = tracing_subscriber::registry()
        .with(filter)
        .with(fmt_layer);

    match otel_layer {
        Some(layer) => subscriber.with(layer).init(),
        None => subscriber.init(),
    }

    tracing::info!(
        service = %config.service_name,
        version = %config.service_version,
        env = %config.deployment_env,
        otlp = ?config.otlp_endpoint,
        sample = %config.sampling_ratio,
        "observability initialized"
    );

    ObservabilityGuard { tracer_provider }
}
// main.rs — pemakaian
use std::time::Duration;

use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::get,
    Json, Router,
};
use serde_json::{json, Value};
use tower_http::trace::TraceLayer;
use tracing::{info, instrument, warn};

mod observability;

#[tokio::main]
async fn main() {
    // Guard di drop saat program exit → flush trace pending
    let _guard = observability::init(observability::ObservabilityConfig::default());

    let app = Router::new()
        .route("/api/produk/:id", get(get_produk))
        .route("/api/order", get(create_order))
        .layer(TraceLayer::new_for_http());  // tower-http auto instrument

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    info!(addr = "0.0.0.0:8080", "server started");
    axum::serve(listener, app).await.unwrap();
}

#[instrument(skip_all, fields(produk_id = %id))]
async fn get_produk(Path(id): Path<u64>) -> Result<Json<Value>, StatusCode> {
    info!("fetching produk");

    let produk = fetch_produk_from_db(id).await?;
    let rekomendasi = fetch_rekomendasi(id).await.unwrap_or_default();

    Ok(Json(json!({
        "produk": produk,
        "rekomendasi": rekomendasi
    })))
}

#[instrument(skip_all, fields(db.system = "postgresql"))]
async fn fetch_produk_from_db(id: u64) -> Result<Value, StatusCode> {
    info!(id, "query db");

    // Simulasi query
    tokio::time::sleep(Duration::from_millis(35)).await;

    if id == 0 {
        warn!(id, "produk tidak valid");
        return Err(StatusCode::NOT_FOUND);
    }

    Ok(json!({
        "id": id,
        "nama": "Indomie Goreng",
        "harga": 3500
    }))
}

#[instrument(skip_all, fields(otel.kind = "client", http.url = "https://ml-service/rekomendasi"))]
async fn fetch_rekomendasi(produk_id: u64) -> Result<Vec<u64>, StatusCode> {
    info!(produk_id, "call ml-service");
    tokio::time::sleep(Duration::from_millis(110)).await;
    Ok(vec![42, 17, 89])
}

#[instrument]
async fn create_order() -> Json<Value> {
    info!("creating order");
    Json(json!({"order_id": 12345}))
}

Pemakaian

# Local dev — log ke stdout, tanpa export OTel
RUST_LOG=info cargo run

# Output JSON:
# {"timestamp":"2026-07-20T10:00:00Z","level":"INFO","fields":{"message":"observability initialized","service":"api-rust"},"target":"my_app"}
# {"timestamp":"2026-07-20T10:00:01Z","level":"INFO","fields":{"message":"fetching produk"},"target":"my_app","span":{"name":"get_produk","produk_id":"123"}}

# Production — export ke Tempo / Jaeger
OTEL_ENDPOINT=http://tempo.observability.svc:4317 \
OTEL_SAMPLE_RATIO=0.05 \
SERVICE_NAME=api-tokopedia \
APP_VERSION=v1.2.3 \
ENV=production \
RUST_LOG=info \
./target/release/api

# Trigger request
curl http://localhost:8080/api/produk/123
# Cek di Tempo / Jaeger
# Trace timeline:
# - HTTP GET /api/produk/:id (parent, 175ms)
#   - get_produk[produk_id=123] (165ms)
#     - fetch_produk_from_db (35ms)
#       - "query db" event
#     - fetch_rekomendasi (110ms)
#       - "call ml-service" event
// Pattern: span attribute custom + event
use tracing::field;

#[instrument(skip_all, fields(
    transaction.amount = field::Empty,
    transaction.status = field::Empty,
))]
async fn process_payment(amount: u64) -> Result<String, String> {
    // Set field setelah computed
    let current_span = tracing::Span::current();
    current_span.record("transaction.amount", &amount);

    let status = call_gateway(amount).await?;
    current_span.record("transaction.status", &status.as_str());

    // Event di tengah span — appears di timeline trace
    tracing::info!(
        gateway = "qris",
        latency_ms = 124,
        "payment confirmed"
    );

    Ok(status)
}

async fn call_gateway(_amount: u64) -> Result<String, String> {
    Ok("paid".to_string())
}

Kapan dipakai

  • Production Rust HTTP / gRPC service.
  • Microservice yang call multiple downstream — distributed trace.
  • Service yang harus SLO compliance (p95, error rate).
  • App dengan async complex — span context propagation otomatis di .await.

Catatan

  • TraceContextPropagator untuk W3C trace context — standard yang dipakai semua vendor. Tanpa ini, trace gak nyambung antar service.
  • install_batch(Tokio) — batch span sebelum kirim ke collector. Hemat network roundtrip.
  • sampling_ratio — production 1-10%, staging 100%. TraceIdRatioBased = consistent across services (parent trace di-sample, semua child ikut).
  • #[instrument] macro otomatis bikin span dengan parameter function. skip_all untuk hide param sensitif (password, token).
  • tracing-opentelemetry bridge crate — convert tracing span ke OTel span. Tracing pakai event, OTel pakai attribute.
  • ObservabilityGuard Drop wajib di-keep di main — flush span pending saat shutdown. Tanpa ini, span terakhir hilang.
  • TraceLayer tower-http — auto-create span per HTTP request. Pair dengan #[instrument] di handler.

Hati-hati .with_target(true) — kalau path crate panjang (my_app::module::submodule), log jadi noisy. Set false di production, atau filter dengan EnvFilter.

# tags

rusttracingopentelemetryobservabilityasync

Ditulis oleh Asti Larasati · 20 Juli 2026