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_alluntuk 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. Setfalsedi production, atau filter dengan EnvFilter.
# tags
rusttracingopentelemetryobservabilityasync
Ditulis oleh Asti Larasati · 20 Juli 2026