tracing

Modern Rust library providing structured, asynchronous logging and span-based observability. In addition to traditional logging macros (info!, error!, etc.), supports distributed tracing and metrics collection. Optimized for monitoring in microservices environments.

LoggingRustStructured LoggingAsynchronousDistributed TracingObservability

Library

tracing

Overview

tracing is a modern Rust library that provides structured, asynchronous logging and span-based observability. In addition to traditional logging macros (info!, error!, etc.), it supports distributed tracing and metrics collection, optimized for monitoring in microservices environments. In 2025, rapid adoption expansion is occurring in Rust development for microservices and cloud-native environments, gaining attention as a choice for full-scale production systems due to the increasing importance of structured logging and distributed tracing.

Details

The 2025 version of tracing is becoming a standard choice for modern Rust projects emphasizing observability. Beyond traditional log output functionality, it introduces the concept of "spans" to track application execution flows, powerfully supporting request tracking, performance analysis, and debugging in distributed systems. With high affinity for asynchronous processing and integration with the Tokio ecosystem, it is highly valued for use in high-performance asynchronous applications.

Key Features

  • Structured Logging: Rich context information management in key-value format
  • Span-based Tracing: Tracking from process start to completion with nested structures
  • Asynchronous Support: Complete integration with asynchronous runtimes like Tokio
  • Distributed Tracing: Distributed system visualization through OpenTelemetry integration
  • Metrics Collection: Automatic collection and analysis of performance indicators
  • Extensible Subscriber System: Pluggable output destinations and formats

Pros and Cons

Pros

  • Excellent observability in microservices and cloud-native environments
  • Minimal performance degradation through complete integration with asynchronous processing
  • Industry-standard distributed tracing support through OpenTelemetry
  • Easy machine analysis and querying through structured logs
  • Complex processing flow visualization possible through span functionality
  • Rich ecosystem and active community support

Cons

  • Higher learning cost compared to traditional logging libraries
  • Features may be excessive for simple applications
  • Time required to understand configuration and Subscribers
  • Tendency for higher memory usage than traditional logging libraries
  • Complex debug output difficult for beginners to understand
  • Introduction cost may not be justified for small-scale projects

Reference Pages

Usage Examples

Installation and Basic Setup

# Add dependencies to Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = "0.3"

# Recommended setup for asynchronous applications
tokio = { version = "1.0", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-opentelemetry = "0.21"  # For OpenTelemetry integration
tracing-appender = "0.2"        # For file output

Basic Logging (Compatible with Traditional log Crate)

use tracing::{info, warn, error, debug, trace};
use tracing_subscriber;

fn main() {
    // Initialize basic Subscriber
    tracing_subscriber::init();

    // Usage similar to traditional logging libraries
    trace!("Trace level detailed information");
    debug!("Debug info: Application starting");
    info!("Info: Configuration file loaded");
    warn!("Warning: Using temporary configuration");
    error!("Error: Database connection failed");

    // Structured logging (key-value format)
    info!(
        user_id = 12345,
        action = "login",
        ip_address = "192.168.1.100",
        "User logged in"
    );

    // Variable expansion
    let request_id = "req_abcd1234";
    let response_time_ms = 150;
    
    info!(
        request_id,
        response_time_ms,
        "Request processing completed"
    );
}

Span-based Tracing

use tracing::{info, warn, error, span, Level, instrument};
use tracing_subscriber;

#[tokio::main]
async fn main() {
    tracing_subscriber::init();

    info!("Application starting");

    // Manual span creation
    let span = span!(Level::INFO, "user_registration", user_id = 12345);
    let _enter = span.enter();

    info!("User registration process started");
    
    // Nested span
    register_user("[email protected]").await;
    
    info!("User registration process completed");
}

// Automatic span generation (using instrument attribute)
#[instrument]
async fn register_user(email: &str) -> Result<u64, String> {
    info!("User registration started");
    
    // Database operation span
    let user_id = create_user_in_database(email).await?;
    
    // Email sending span
    send_welcome_email(email, user_id).await?;
    
    info!(user_id, "User registration completed successfully");
    Ok(user_id)
}

#[instrument(skip(email), fields(email = %email))]
async fn create_user_in_database(email: &str) -> Result<u64, String> {
    info!("Creating user in database");
    
    // Database operation simulation
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    
    let user_id = 12345; // Generated user ID
    info!(user_id, "User created in database");
    
    Ok(user_id)
}

#[instrument]
async fn send_welcome_email(email: &str, user_id: u64) -> Result<(), String> {
    info!("Sending welcome email");
    
    // Email sending simulation
    tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
    
    info!("Welcome email sent successfully");
    Ok(())
}

Advanced Subscriber Configuration and Filtering

use tracing::{info, warn, error, Level};
use tracing_subscriber::{
    fmt,
    layer::SubscriberExt,
    util::SubscriberInitExt,
    EnvFilter,
    Registry,
};
use tracing_appender::{rolling, non_blocking};

fn main() {
    // Subscriber configuration with multiple output destinations
    let file_appender = rolling::daily("logs", "app.log");
    let (non_blocking, _guard) = non_blocking(file_appender);

    let subscriber = Registry::default()
        .with(EnvFilter::from_default_env()
            .add_directive("myapp=debug".parse().unwrap())
            .add_directive("myapp::database=trace".parse().unwrap()))
        .with(fmt::Layer::new()
            .with_writer(std::io::stdout)
            .with_ansi(true)
            .with_level(true)
            .with_target(true)
            .with_thread_ids(true)
            .with_thread_names(true))
        .with(fmt::Layer::new()
            .with_writer(non_blocking)
            .with_ansi(false)
            .json());

    subscriber.init();

    info!("Application started with configured Subscriber");

    // Logs from various modules
    database_operations();
    network_operations();
    business_logic();
}

mod database {
    use tracing::{info, debug, error, instrument, span, Level};

    #[instrument]
    pub fn connect() -> Result<(), &'static str> {
        debug!("Loading database connection configuration");
        info!("Connected to database");
        Ok(())
    }

    #[instrument(skip(query), fields(query_type = "SELECT"))]
    pub fn execute_query(query: &str) -> Result<Vec<String>, &'static str> {
        let span = span!(Level::DEBUG, "query_execution", query_length = query.len());
        let _enter = span.enter();
        
        debug!("Executing SQL query");
        
        // Query execution simulation
        std::thread::sleep(std::time::Duration::from_millis(50));
        
        info!(rows_affected = 3, "Query executed successfully");
        Ok(vec!["row1".to_string(), "row2".to_string(), "row3".to_string()])
    }
}

mod network {
    use tracing::{info, warn, error, instrument};

    #[instrument]
    pub fn send_http_request(url: &str) -> Result<String, &'static str> {
        info!("HTTP request sending started");
        
        // Network processing simulation
        std::thread::sleep(std::time::Duration::from_millis(300));
        
        info!(status_code = 200, "HTTP request successful");
        Ok("response_data".to_string())
    }

    #[instrument]
    pub fn handle_timeout() {
        warn!("Network timeout occurred");
        error!("Connection timed out");
    }
}

fn database_operations() {
    let _ = database::connect();
    let _ = database::execute_query("SELECT * FROM users WHERE active = true");
}

fn network_operations() {
    let _ = network::send_http_request("https://api.example.com/data");
    network::handle_timeout();
}

fn business_logic() {
    info!("Business logic processing started");
    info!("Business logic processing completed");
}

Asynchronous Processing and OpenTelemetry Integration

use tracing::{info, instrument, Span};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use tracing_opentelemetry::OpenTelemetryLayer;
use opentelemetry::{global, KeyValue};
use opentelemetry_sdk::{
    trace::{self, TracerProvider},
    Resource,
};
use opentelemetry_jaeger::JaegerPipeline;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    // OpenTelemetry tracer configuration
    global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new());
    
    let tracer = opentelemetry_jaeger::new_agent_pipeline()
        .with_service_name("myapp")
        .install_simple()?;

    // Subscriber configuration
    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::new("myapp=debug"))
        .with(tracing_subscriber::fmt::layer())
        .with(OpenTelemetryLayer::new(tracer))
        .init();

    info!("Distributed tracing enabled application started");

    // Distributed tracing enabled processing
    process_order("order_12345").await?;

    // OpenTelemetry shutdown
    global::shutdown_tracer_provider();
    Ok(())
}

#[instrument]
async fn process_order(order_id: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    info!("Order processing started");
    
    // Add custom attributes
    Span::current().set_attribute(KeyValue::new("order.priority", "high"));
    
    // Process each step
    validate_order(order_id).await?;
    reserve_inventory(order_id).await?;
    process_payment(order_id).await?;
    ship_order(order_id).await?;
    
    info!("Order processing completed");
    Ok(())
}

#[instrument]
async fn validate_order(order_id: &str) -> Result<(), &'static str> {
    info!("Validating order");
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
    
    // Validation result
    Span::current().set_attribute(KeyValue::new("validation.result", "success"));
    info!("Order validation completed");
    Ok(())
}

#[instrument]
async fn reserve_inventory(order_id: &str) -> Result<(), &'static str> {
    info!("Reserving inventory");
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    
    let reserved_items = 3;
    Span::current().set_attribute(KeyValue::new("inventory.reserved_items", reserved_items));
    info!(reserved_items, "Inventory reservation completed");
    Ok(())
}

#[instrument]
async fn process_payment(order_id: &str) -> Result<(), &'static str> {
    info!("Processing payment");
    tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
    
    let amount = 9999;
    Span::current().set_attribute(KeyValue::new("payment.amount", amount));
    Span::current().set_attribute(KeyValue::new("payment.method", "credit_card"));
    
    info!(amount, "Payment processing completed");
    Ok(())
}

#[instrument]
async fn ship_order(order_id: &str) -> Result<(), &'static str> {
    info!("Arranging shipment");
    tokio::time::sleep(tokio::time::Duration::from_millis(80)).await;
    
    let tracking_number = "TRK123456789";
    Span::current().set_attribute(KeyValue::new("shipping.tracking_number", tracking_number));
    
    info!(tracking_number, "Shipment arrangement completed");
    Ok(())
}

Custom Subscriber and Metrics Collection

use tracing::{info, warn, error, Subscriber, Event, Id, span};
use tracing_subscriber::{
    layer::{Context, SubscriberExt},
    Layer, Registry,
};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

// Custom metrics collection Layer
#[derive(Debug, Clone)]
struct MetricsLayer {
    metrics: Arc<Mutex<Metrics>>,
}

#[derive(Debug, Default)]
struct Metrics {
    log_counts: HashMap<String, u64>,
    error_counts: HashMap<String, u64>,
    span_durations: HashMap<String, Vec<Duration>>,
}

impl MetricsLayer {
    fn new() -> Self {
        Self {
            metrics: Arc::new(Mutex::new(Metrics::default())),
        }
    }

    fn print_metrics(&self) {
        let metrics = self.metrics.lock().unwrap();
        
        println!("\n=== Metrics Report ===");
        
        println!("Log Counts:");
        for (level, count) in &metrics.log_counts {
            println!("  {}: {}", level, count);
        }
        
        println!("Error Counts:");
        for (target, count) in &metrics.error_counts {
            println!("  {}: {}", target, count);
        }
        
        println!("Span Execution Times (Average):");
        for (name, durations) in &metrics.span_durations {
            if !durations.is_empty() {
                let avg = durations.iter().sum::<Duration>() / durations.len() as u32;
                println!("  {}: {:?}", name, avg);
            }
        }
    }
}

impl<S> Layer<S> for MetricsLayer
where
    S: Subscriber,
{
    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
        let mut metrics = self.metrics.lock().unwrap();
        
        // Count by log level
        let level = event.metadata().level().to_string();
        *metrics.log_counts.entry(level.clone()).or_insert(0) += 1;
        
        // For error level, count by target
        if *event.metadata().level() == tracing::Level::ERROR {
            let target = event.metadata().target().to_string();
            *metrics.error_counts.entry(target).or_insert(0) += 1;
        }
    }

    fn on_enter(&self, id: &Id, ctx: Context<'_, S>) {
        // Record span start time
        if let Some(span) = ctx.span(id) {
            span.extensions_mut().insert(Instant::now());
        }
    }

    fn on_exit(&self, id: &Id, ctx: Context<'_, S>) {
        // Calculate span execution time
        if let Some(span) = ctx.span(id) {
            if let Some(start_time) = span.extensions().get::<Instant>() {
                let duration = start_time.elapsed();
                let span_name = span.name().to_string();
                
                let mut metrics = self.metrics.lock().unwrap();
                metrics.span_durations
                    .entry(span_name)
                    .or_insert_with(Vec::new)
                    .push(duration);
            }
        }
    }
}

#[tokio::main]
async fn main() {
    let metrics_layer = MetricsLayer::new();
    
    // Custom Subscriber configuration
    let subscriber = Registry::default()
        .with(tracing_subscriber::fmt::layer())
        .with(metrics_layer.clone());

    tracing::subscriber::set_global_default(subscriber)
        .expect("Failed to set subscriber");

    info!("Custom metrics collection started");

    // Execute various processes
    for i in 0..5 {
        process_request(i).await;
    }

    // Intentional error generation
    error!("Test error 1");
    error!("Test error 2");
    warn!("Warning message");

    // Display metrics
    metrics_layer.print_metrics();
}

#[tracing::instrument]
async fn process_request(request_id: usize) {
    info!(request_id, "Request processing started");
    
    // Random delay to create processing time variations
    let delay = 50 + (request_id * 25);
    tokio::time::sleep(tokio::time::Duration::from_millis(delay as u64)).await;
    
    if request_id % 3 == 0 {
        error!("Request processing error");
    } else {
        info!(request_id, "Request processing completed");
    }
}

Practical Web Application Integration Example

use tracing::{info, warn, error, instrument, Span};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use uuid::Uuid;
use std::collections::HashMap;

// Request ID generation and context management
#[derive(Debug, Clone)]
struct RequestContext {
    request_id: String,
    user_id: Option<u64>,
    ip_address: String,
}

impl RequestContext {
    fn new(user_id: Option<u64>, ip_address: String) -> Self {
        Self {
            request_id: Uuid::new_v4().to_string(),
            user_id,
            ip_address,
        }
    }

    fn attach_to_span(&self) {
        let span = Span::current();
        span.record("request_id", &self.request_id);
        span.record("user_id", &self.user_id.unwrap_or(0));
        span.record("ip_address", &self.ip_address);
    }
}

#[tokio::main]
async fn main() {
    // Full-featured Subscriber configuration for web applications
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "myapp=debug,tower_http=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer()
            .with_target(false)
            .with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339())
            .with_level(true))
        .init();

    info!("Web application started");

    // Simulate multiple request processing
    let requests = vec![
        (Some(12345), "192.168.1.100"),
        (None, "10.0.0.50"),
        (Some(67890), "203.0.113.25"),
    ];

    for (user_id, ip) in requests {
        let ctx = RequestContext::new(user_id, ip.to_string());
        handle_http_request(ctx).await;
    }
}

#[instrument(
    skip(ctx),
    fields(
        request_id = %ctx.request_id,
        user_id = ctx.user_id,
        ip_address = %ctx.ip_address
    )
)]
async fn handle_http_request(ctx: RequestContext) {
    info!("HTTP request received");
    
    // Request processing
    match process_business_logic(&ctx).await {
        Ok(result) => {
            info!(result = %result, "Request processing successful");
        }
        Err(e) => {
            error!(error = %e, "Request processing failed");
        }
    }
}

#[instrument(skip(ctx))]
async fn process_business_logic(ctx: &RequestContext) -> Result<String, &'static str> {
    ctx.attach_to_span();
    
    info!("Business logic processing started");
    
    // Authentication check
    if let Some(user_id) = ctx.user_id {
        authenticate_user(user_id).await?;
    } else {
        warn!("Request from unauthenticated user");
    }
    
    // Data processing
    let data = fetch_data().await?;
    let processed_data = transform_data(data).await?;
    
    info!("Business logic processing completed");
    Ok(processed_data)
}

#[instrument]
async fn authenticate_user(user_id: u64) -> Result<(), &'static str> {
    info!("User authentication started");
    
    // Authentication processing simulation
    tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
    
    // Record user information
    Span::current().record("user_role", "premium");
    
    info!("User authentication successful");
    Ok(())
}

#[instrument]
async fn fetch_data() -> Result<HashMap<String, String>, &'static str> {
    info!("Data fetching started");
    
    // Database query simulation
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    
    let mut data = HashMap::new();
    data.insert("key1".to_string(), "value1".to_string());
    data.insert("key2".to_string(), "value2".to_string());
    
    info!(records_count = data.len(), "Data fetching completed");
    Ok(data)
}

#[instrument(skip(data))]
async fn transform_data(data: HashMap<String, String>) -> Result<String, &'static str> {
    info!(input_size = data.len(), "Data transformation started");
    
    // Data transformation processing simulation
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
    
    let result = serde_json::to_string(&data).unwrap_or_else(|_| "{}".to_string());
    
    info!(output_size = result.len(), "Data transformation completed");
    Ok(result)
}