How does tracing::Level::DEBUG compare to INFO and TRACE for verbosity control in logging?

tracing::Level defines a severity hierarchy where TRACE captures everything, DEBUG adds diagnostic details, and INFO provides high-level operational visibilityβ€”choosing between them shapes what appears in logs and how much noise your application generates. The levels form a filtering cascade: setting a maximum level excludes everything below it. Understanding this hierarchy helps you choose appropriate verbosity for different environments and messages.

The Level Hierarchy

use tracing::Level;
 
fn level_hierarchy() {
    // Levels are ordered from most verbose to least:
    // TRACE < DEBUG < INFO < WARN < ERROR
    
    // This means:
    // - TRACE is the "lowest" level (most verbose, captures everything)
    // - ERROR is the "highest" level (least verbose, captures only errors)
    
    // When you set a maximum level, you capture that level and everything above
    // Level::TRACE -> captures TRACE, DEBUG, INFO, WARN, ERROR
    // Level::DEBUG -> captures DEBUG, INFO, WARN, ERROR (not TRACE)
    // Level::INFO  -> captures INFO, WARN, ERROR (not DEBUG or TRACE)
    
    // Comparison operators work as expected:
    assert!(Level::TRACE < Level::DEBUG);
    assert!(Level::DEBUG < Level::INFO);
    assert!(Level::INFO < Level::WARN);
    assert!(Level::WARN < Level::ERROR);
    
    // Each level has semantic meaning:
    println!("TRACE: {:?}", Level::TRACE);  // "TRACE"
    println!("DEBUG: {:?}", Level::DEBUG);  // "DEBUG"
    println!("INFO: {:?}", Level::INFO);    // "INFO"
    println!("WARN: {:?}", Level::WARN);    // "WARN"
    println!("ERROR: {:?}", Level::ERROR);  // "ERROR"
}

The levels form a total order; setting a maximum level filters out everything below it.

Semantic Meaning of Each Level

use tracing::{info, debug, trace, warn, error};
 
fn semantic_meaning() {
    // ERROR: Something went wrong, needs attention
    // - Failures that affect user-visible behavior
    // - Exceptions that require investigation
    // - Critical system failures
    error!("Failed to connect to database: {}", "connection refused");
    error!("User authentication failed for user {}", "alice");
    error!("Out of memory while processing request");
    
    // WARN: Something unusual, but not necessarily wrong
    // - Deprecated API usage
    // - Approaching resource limits
    // - Recovered from an error condition
    warn!("API endpoint /v1/users is deprecated, use /v2/users");
    warn!("Memory usage at 90% of limit");
    warn!("Request took 5s, exceeding threshold of 2s");
    
    // INFO: High-level operational events
    // - Application lifecycle (startup, shutdown)
    // - Key business events
    // - Significant state changes
    info!("Application started on port {}", 8080);
    info!("User {} logged in", "alice");
    info!("Order {} placed for ${}", "ORD-123", 99.99);
    
    // DEBUG: Diagnostic information for troubleshooting
    // - Internal state during operations
    // - Decision points and branching logic
    // - Performance-relevant measurements
    debug!("Processing request with params: {:?}", vec!["a", "b"]);
    debug!("Cache miss for key {}, fetching from database", "user:123");
    debug!("Retry attempt {} of {}", 2, 3);
    
    // TRACE: Very detailed execution flow
    // - Function entry/exit
    // - Variable values at checkpoints
    // - Fine-grained timing
    trace!("Entering function process_request");
    trace!("Variable x = {}", 42);
    trace!("Loop iteration {} of {}", 5, 100);
    trace!("Exiting function process_request");
}

Each level serves a distinct purpose: ERROR for failures, WARN for concerns, INFO for events, DEBUG for diagnostics, TRACE for execution flow.

Practical Filtering

use tracing::{Level, metadata::LevelFilter};
 
fn filtering_levels() {
    // In production, typically use INFO or WARN
    // In development, typically use DEBUG or TRACE
    
    // Setting max level at compile time (most efficient):
    // RUST_LOG=info cargo run  -> INFO and above
    // RUST_LOG=debug cargo run  -> DEBUG and above
    // RUST_LOG=trace cargo run   -> Everything
    
    // LevelFilter mirrors Level but for configuration:
    let filter = LevelFilter::INFO;
    
    assert_eq!(filter, LevelFilter::INFO);
    assert!(filter < LevelFilter::DEBUG);  // INFO excludes DEBUG and TRACE
    
    // Common configurations:
    
    // Production: INFO (operational events, warnings, errors)
    // - Shows application lifecycle and business events
    // - Captures warnings and errors
    // - Hides diagnostic noise
    
    // Staging: DEBUG (adds diagnostic information)
    // - Everything in INFO
    // - Plus diagnostic information for troubleshooting
    // - Still hides extremely detailed trace messages
    
    // Development: TRACE (everything)
    // - All messages
    // - Full execution flow visibility
    // - Maximum debugging capability
}
 
fn programmatic_filtering() {
    use tracing_subscriber::EnvFilter;
    
    // Environment-based configuration:
    // RUST_LOG=my_app=debug,other_lib=info
    
    // This sets:
    // - my_app crate at DEBUG level
    // - other_lib crate at INFO level
    
    // Module-specific filtering:
    // RUST_LOG=my_app::database=trace,my_app::api=info
    // - database module at TRACE
    // - api module at INFO
}

Level filtering determines what gets logged; the RUST_LOG environment variable controls this at runtime.

Comparing DEBUG vs INFO vs TRACE

use tracing::{info, debug, trace, span, Level};
 
fn comparison_example() {
    // Consider an HTTP request handler:
    
    // INFO: High-level summary (business event)
    info!("Processing order request for user {}", "alice");
    // "What happened" - suitable for production logs
    
    // DEBUG: Internal details for troubleshooting
    debug!(
        user_id = %123,
        items = ?vec!["item1", "item2"],
        "Order contains {} items",
        2
    );
    // "How it happened" - useful when investigating issues
    
    // TRACE: Execution flow, extremely detailed
    trace!("validate_user: entered");
    trace!("validate_user: checking database");
    trace!("validate_user: user found, checking permissions");
    trace!("validate_user: exited with Ok(true)");
    // "Step by step" - for deep debugging sessions
    
    // Example showing the same operation at different levels:
    
    // INFO level - just the result:
    info!("User {} authenticated successfully", "alice");
    
    // DEBUG level - key decision points:
    debug!("Auth attempt for user {}", "alice");
    debug!("Password hash comparison: match={}", true);
    debug!("Session created with ID {}", "sess_123");
    
    // TRACE level - every step:
    trace!("enter authenticate()");
    trace!("lookup_user(\"alice\") -> Ok(user)");
    trace!("verify_password(input, stored) -> Ok(())");
    trace!("create_session(user.id) -> sess_123");
    trace!("exit authenticate() -> Ok(session)");
}
 
fn when_to_use_which() {
    // Use INFO for:
    // - Application started/stopped
    // - User actions (login, logout, signup)
    // - Business events (order placed, payment processed)
    // - Scheduled job execution
    // - Configuration loaded
    // - External service connections
    
    // Use DEBUG for:
    // - Cache hits/misses
    // - Retry attempts
    // - Request/response details (but not full bodies)
    // - Database query summaries (not full queries unless needed)
    // - Branching decisions in code
    // - Performance timing of individual operations
    
    // Use TRACE for:
    // - Function entry/exit
    // - Variable values at checkpoints
    // - Loop iterations
    // - Internal state transitions
    // - Full request/response bodies
    // - Timing of every operation
}

INFO captures "what happened", DEBUG captures "how it happened", TRACE captures "every step".

Spans and Levels

use tracing::{span, info, debug, trace, Level};
 
fn spans_and_levels() {
    // Spans group related events and inherit level filtering
    
    // Create span at INFO level
    let span = span!(Level::INFO, "request", id = %123);
    let _enter = span.enter();
    
    // Events within the span respect level filtering
    info!("Processing request");      // Shown at INFO+
    debug!("Validating input");        // Shown at DEBUG+
    trace!("Checking field x");        // Shown at TRACE+
    
    // Span levels affect visibility:
    // - If max level is INFO, span is visible
    // - Only info!("...") appears inside the span
    // - debug! and trace! are filtered out
    
    // Create span at DEBUG level
    let debug_span = span!(Level::DEBUG, "database_query", query = "SELECT *");
    let _enter = debug_span.enter();
    
    debug!("Executing query");
    trace!("Connection acquired");  // Only shown at TRACE level
    
    // Create span at TRACE level
    let trace_span = span!(Level::TRACE, "function", name = "process_item");
    let _enter = trace_span.enter();
    
    trace!("Entered function");
    trace!("x = {}", 42);
    trace!("Exiting function");
    // Entire span only visible at TRACE level
}
 
fn nested_spans() {
    let outer = span!(Level::INFO, "outer");
    let _outer = outer.enter();
    
    info!("In outer span");  // Visible at INFO+
    
    {
        let inner = span!(Level::DEBUG, "inner");
        let _inner = inner.enter();
        
        debug!("In inner span");  // Visible at DEBUG+
        trace!("Very detailed");   // Only at TRACE
        
        // Inner span provides additional context for DEBUG events
    }
    
    debug!("Back in outer");  // Still in outer span context
}

Spans inherit level filtering; a span at DEBUG level only appears when maximum level includes DEBUG.

Real-World Examples

use tracing::{info, debug, trace, warn, error, span, Level};
 
// Example: HTTP handler
mod http_handler {
    use super::*;
    
    pub async fn handle_request(user_id: u32, action: &str) {
        // INFO: Business-level event
        info!(user_id, action, "Handling request");
        
        let span = span!(Level::DEBUG, "request_processing", user_id);
        let _enter = span.enter();
        
        // DEBUG: Internal processing details
        debug!("Validating user permissions");
        
        // TRACE: Detailed execution
        trace!("Checking user {} in cache", user_id);
        trace!("Cache miss, querying database");
        
        match process_action(action).await {
            Ok(result) => {
                debug!("Action completed successfully");
                info!(result, "Request completed");
            }
            Err(e) => {
                error!(error = %e, "Request failed");
            }
        }
    }
    
    async fn process_action(action: &str) -> Result<String, String> {
        trace!("enter process_action");
        let result = format!("processed: {}", action);
        trace!("exit process_action");
        Ok(result)
    }
}
 
// Example: Database operations
mod database {
    use super::*;
    
    pub fn query(sql: &str) -> Vec<String> {
        // INFO: High-level operation
        info!("Executing database query");
        
        // DEBUG: Query details
        debug!(sql, "Query text");
        
        // TRACE: Full execution flow
        trace!("Acquiring connection from pool");
        trace!("Preparing statement");
        trace!("Executing: {}", sql);
        trace!("Fetching results");
        
        let results = vec!["result1".to_string(), "result2".to_string()];
        
        debug!("Query returned {} results", results.len());
        results
    }
}
 
// Example: Background job processor
mod job_processor {
    use super::*;
    
    pub fn process_job(job_id: u64) {
        // INFO: Job lifecycle
        info!(job_id, "Starting job processing");
        
        let span = span!(Level::DEBUG, "job", id = job_id);
        let _enter = span.enter();
        
        // DEBUG: Processing stages
        debug!("Stage 1: Validation");
        process_stage(1);
        
        debug!("Stage 2: Processing");
        process_stage(2);
        
        debug!("Stage 3: Cleanup");
        process_stage(3);
        
        // TRACE: Stage details
        trace!("All stages completed");
        
        info!(job_id, "Job completed successfully");
    }
    
    fn process_stage(stage: u32) {
        trace!("Entering stage {}", stage);
        // ... processing ...
        trace!("Exiting stage {}", stage);
    }
}

Real code uses each level purposefully: INFO for business events, DEBUG for troubleshooting, TRACE for deep debugging.

Level Selection Guidelines

use tracing::{info, debug, trace};
 
fn level_guidelines() {
    // PRODUCTION (LevelFilter::INFO):
    // - Logs are for operations, not debugging
    // - Capture business events, warnings, errors
    // - Minimize volume and cost
    
    // Production appropriate:
    info!("Server started on port 8080");
    info!("User {} logged in", "alice");
    warn!("Memory usage at 90%");
    error!("Database connection failed");
    
    // Too verbose for production:
    // debug!("Cache hit for user:123");    // Skip in production
    // trace!("Function enter: process");    // Skip in production
    
    // DEVELOPMENT (LevelFilter::DEBUG):
    // - Troubleshooting in progress
    // - Understanding application behavior
    // - Performance investigation
    
    // Development appropriate:
    debug!("Cache hit for user:123");
    debug!("Query took 15ms");
    debug!("Retry 2 of 3 for external API");
    
    // Still too verbose for regular development:
    // trace!("Loop iteration 5");           // Skip normally
    // trace!("Variable x = 42");            // Skip normally
    
    // DEBUGGING SESSION (LevelFilter::TRACE):
    // - Active bug investigation
    // - Understanding execution flow
    // - Finding where things go wrong
    
    // TRACE appropriate:
    trace!("Function enter: process_request");
    trace!("Validating input: {:?}", input);
    trace!("State after step 1: {:?}", state);
    trace!("Function exit: process_request -> Ok");
    
    // Level selection questions:
    // - Would I want to see this in production? -> INFO
    // - Would this help diagnose an issue? -> DEBUG
    // - Do I need to see every step? -> TRACE
}
 
fn module_specific_levels() {
    // Different modules can have different appropriate levels:
    
    // High-level API handlers: INFO
    // - Focus on request/response lifecycle
    // - Log business events
    
    // Database layer: DEBUG
    // - Query execution details
    // - Connection pool status
    
    // External service clients: DEBUG
    // - Retry logic
    // - Rate limiting
    
    // Authentication: DEBUG
    // - Login attempts
    // - Token validation
    
    // Configuration: INFO
    // - Settings loaded
    // - Environment detection
    
    // Performance-critical code: DEBUG or TRACE only during investigation
    // - Hot paths should minimize logging overhead
    // - Use conditional compilation if needed
}

Choose levels based on audience: operations teams see INFO+, developers debugging see DEBUG+, deep debugging uses TRACE.

Performance Considerations

use tracing::{info, debug, trace, Level};
 
fn performance_considerations() {
    // Levels have performance implications:
    
    // 1. Compiled-out when disabled:
    // With max level INFO, debug! and trace! calls have zero overhead
    // The macro checks level at compile time
    
    // 2. Still evaluated if disabled at runtime:
    // If RUST_LOG=info, debug! arguments are NOT evaluated
    // tracing handles this efficiently
    
    // 3. Expensive operations:
    // BAD: Always computes expensive value
    debug!("Result: {}", expensive_computation());
    
    // GOOD: Only computes if level enabled (tracing does this automatically)
    debug!("Result: {}", expensive_computation());
    // tracing macros are lazy - argument only evaluated if level passes
    
    // 4. Structured fields:
    // Efficient even at lower levels
    debug!(
        user_id = %123,
        items = ?get_items(),  // Only evaluated if DEBUG enabled
        "Processing items"
    );
    
    // 5. Span overhead:
    // Creating spans has minimal overhead when disabled
    let span = span!(Level::TRACE, "expensive_operation");
    // Span is created but events inside are filtered
}
 
fn expensive_computation() -> String {
    // Expensive operation
    std::thread::sleep(std::time::Duration::from_millis(100));
    "result".to_string()
}
 
fn get_items() -> Vec<String> {
    vec!["item1".to_string(), "item2".to_string()]
}
 
// Performance summary:
// - INFO in production: minimal overhead, essential events
// - DEBUG in staging: moderate overhead, diagnostic value
// - TRACE in development: significant overhead, full visibility
// - Compile-time filtering eliminates disabled levels

The tracing macros are lazyβ€”arguments are only evaluated if the level is enabled.

Filtering Examples

use tracing_subscriber::{EnvFilter, fmt, Registry};
use tracing::Level;
 
fn filtering_examples() {
    // Environment variable configuration:
    
    // Production: RUST_LOG=info
    // Shows: INFO, WARN, ERROR
    // Hides: DEBUG, TRACE
    
    // Development: RUST_LOG=debug
    // Shows: DEBUG, INFO, WARN, ERROR
    // Hides: TRACE
    
    // Deep debugging: RUST_LOG=trace
    // Shows: Everything
    
    // Module-specific:
    // RUST_LOG=my_app=debug,sqlx=info,reqwest=warn
    // - my_app at DEBUG level
    // - sqlx (database library) at INFO level
    // - reqwest (HTTP client) at WARN level
    
    // Combination:
    // RUST_LOG=info,my_app::database=trace
    // - Everything at INFO
    // - my_app::database at TRACE
    
    // Code example for subscriber setup:
    let subscriber = fmt::Subscriber::builder()
        .with_max_level(Level::INFO)  // Hard-coded level
        .finish();
    
    // Or use environment:
    let filter = EnvFilter::from_default_env()
        .add_directive("my_app=debug".parse().unwrap());
    
    // Filtering affects what's collected:
    // - Events below max level are never constructed
    // - Spans below max level are not entered
    // - This reduces overhead significantly
}

Environment-based filtering provides runtime control; module-specific filtering reduces noise from dependencies.

Synthesis

Quick reference:

use tracing::Level;
 
// Level hierarchy (increasing severity):
// TRACE < DEBUG < INFO < WARN < ERROR
 
// When setting max level to X, you see X and everything above:
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ Max Level   β”‚ What appears in logs                β”‚
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ TRACE       β”‚ TRACE, DEBUG, INFO, WARN, ERROR     β”‚
// β”‚ DEBUG       β”‚ DEBUG, INFO, WARN, ERROR             β”‚
// β”‚ INFO        β”‚ INFO, WARN, ERROR                    β”‚
// β”‚ WARN        β”‚ WARN, ERROR                          β”‚
// β”‚ ERROR       β”‚ ERROR                                β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 
// Level semantics:
// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
// β”‚ Level   β”‚ Use for                                  β”‚
// β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
// β”‚ ERROR   β”‚ Failures requiring attention             β”‚
// β”‚ WARN    β”‚ Concerns, unusual but handled            β”‚
// β”‚ INFO    β”‚ Business events, lifecycle, state change β”‚
// β”‚ DEBUG   β”‚ Diagnostics, troubleshooting details     β”‚
// β”‚ TRACE   β”‚ Execution flow, step-by-step debugging  β”‚
// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 
// Typical configuration:
// Production:  INFO  - Essential events, warnings, errors
// Staging:     DEBUG - Add diagnostic details
// Development: TRACE - Full visibility during debugging
 
// Decision flowchart:
// Q: Is this an error or failure?         β†’ ERROR
// Q: Is this concerning but handled?      β†’ WARN
// Q: Is this a key business event?         β†’ INFO
// Q: Would this help diagnose an issue?    β†’ DEBUG
// Q: Is this step-by-step execution flow?  β†’ TRACE
 
// Common mistakes:
// ❌ Using ERROR for expected conditions (use WARN instead)
// ❌ Using INFO for debugging details (use DEBUG instead)
// ❌ Using DEBUG for business events (use INFO instead)
// ❌ Using TRACE in production (too verbose)
// ❌ Not using structured fields with events
 
// Best practices:
// βœ… Use structured fields for key data: debug!(user_id = %id, ...)
// βœ… Group related events with spans
// βœ… Use module-specific filtering for dependencies
// βœ… Let the level do the work - don't if-log-level checks
// βœ… Reserve ERROR for actual errors, WARN for concerns
// βœ… INFO should make sense in production logs
// βœ… DEBUG should help diagnose issues
// βœ… TRACE should trace execution flow

Key insight: The tracing levels represent a deliberate hierarchy where each level serves a distinct audience and purpose. INFO is your operations baselineβ€”business events, lifecycle milestones, and state changes that make sense in production logs. DEBUG adds the diagnostic layerβ€”cache behavior, retry logic, decision points that help troubleshoot issues without overwhelming the logs. TRACE is the microscopeβ€”function entries, variable values, and execution steps that reveal exactly what happened during a deep debugging session. The filtering is cumulative: setting DEBUG gives you INFO, WARN, and ERROR too, but excludes TRACE. This design means you can leave comprehensive logging in your code and control verbosity through configurationβ€”INFO for production efficiency, DEBUG for staging investigation, TRACE for development deep-dives. The choice isn't just about verbosityβ€”it's about matching the right level of detail to the right context.