How does tracing::subscriber::set_global_default differ from per-thread subscribers for logging configuration?

set_global_default installs a single subscriber that receives all spans and events across all threads in the application, while per-thread subscribers allow different threads to have independent logging configurations. The global default is simpler for most applications but per-thread subscribers enable isolated logging in testing, benchmarks, and specialized threading models.

The Global Default Subscriber

use tracing_subscriber::{fmt, EnvFilter};
 
fn global_subscriber_example() {
    // Create a subscriber
    let subscriber = fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .finish();
    
    // Set as global default - affects all threads
    tracing::subscriber::set_global_default(subscriber)
        .expect("Failed to set global default subscriber");
    
    // Now all spans and events go through this subscriber
    tracing::info!("This is logged globally");
    
    // Cannot set another global default - can only be called once
    // let another = fmt().finish();
    // tracing::subscriber::set_global_default(another);  // Panics!
}

set_global_default installs one subscriber for the entire process, affecting all threads.

Per-Thread Subscribers with set_default

use tracing_subscriber::fmt;
use std::thread;
 
fn per_thread_example() {
    // No global default set
    
    thread::spawn(|| {
        // Each thread can have its own subscriber
        let subscriber = fmt()
            .with_target(false)
            .finish();
        
        // set_default is per-thread, scoped to the current thread
        let _guard = tracing::subscriber::set_default(subscriber);
        
        tracing::info!("Only visible in this thread's subscriber");
        // Guard drops here, subscriber no longer active
    });
    
    thread::spawn(|| {
        // This thread has no subscriber
        // Events go nowhere (or to a previously set default)
        tracing::info!("No subscriber in this thread");
    });
    
    // Main thread - can have its own subscriber
    let main_subscriber = fmt()
        .with_target(true)
        .finish();
    let _guard = tracing::subscriber::set_default(main_subscriber);
    
    tracing::info!("Main thread logging");
}

set_default creates a thread-local subscriber that only affects the current thread.

Key Differences in Scope

use tracing_subscriber::fmt;
use std::thread;
 
fn scope_differences() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Aspect               β”‚ set_global_default      β”‚ set_default          β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ Scope                 β”‚ Process-wide            β”‚ Thread-local         β”‚
    // β”‚ Threads affected      β”‚ All threads             β”‚ Current thread only  β”‚
    // β”‚ Can be called         β”‚ Once per process        β”‚ Multiple times       β”‚
    // β”‚ Lifetime               β”‚ Until process ends      β”‚ Until guard drops    β”‚
    // β”‚ Thread safety          β”‚ Must be Sync            β”‚ No Sync required     β”‚
    // β”‚ Use case               β”‚ Production logging      β”‚ Testing, isolation   β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    
    // Global default: all threads share
    let global = fmt().finish();
    tracing::subscriber::set_global_default(global).unwrap();
    
    // Every thread now uses the same subscriber
    // Cannot customize per-thread
    
    // Per-thread: each thread has its own
    thread::spawn(|| {
        let local = fmt().with_target(false).finish();
        let _guard = tracing::subscriber::set_default(local);
        // This subscriber only affects this thread
    });
}

Global affects all threads; thread-local only affects the current thread.

The Guard Pattern

use tracing_subscriber::fmt;
 
fn guard_pattern() {
    // set_default returns a guard
    let subscriber = fmt().finish();
    let _guard = tracing::subscriber::set_default(subscriber);
    
    // Events are captured while guard is alive
    tracing::info!("This is captured");
    
    // Guard can be explicitly dropped
    drop(_guard);
    
    // Now no subscriber is active in this thread
    tracing::info!("This goes nowhere");
}
 
fn nested_guards() {
    let outer = fmt().with_target(true).finish();
    let _outer_guard = tracing::subscriber::set_default(outer);
    
    tracing::info!("Goes to outer subscriber");
    
    {
        let inner = fmt().with_target(false).finish();
        let _inner_guard = tracing::subscriber::set_default(inner);
        
        // Inner subscriber shadows outer for this scope
        tracing::info!("Goes to inner subscriber");
    }
    
    // Back to outer subscriber
    tracing::info!("Goes to outer subscriber again");
}

set_default uses RAII guards; when the guard drops, the subscriber is removed.

Thread Safety Requirements

use tracing_subscriber::fmt;
use tracing::Subscriber;
 
// Global subscriber must implement Sync
// because multiple threads access it concurrently
 
fn thread_safety() {
    // This subscriber must be Sync + Send
    let global = fmt().finish();
    
    // Compile-time check: fmt::Subscriber implements Sync
    tracing::subscriber::set_global_default(global).unwrap();
    
    // Multiple threads can emit events simultaneously
    // The subscriber must handle concurrent access safely
}
 
// Per-thread subscribers don't need Sync
fn thread_local_no_sync() {
    // This subscriber doesn't need to be Sync
    // Only one thread accesses it
    let local = fmt()
        .with_target(false)
        .finish();
    
    let _guard = tracing::subscriber::set_default(local);
    // No Sync requirement because only this thread uses it
}

Global subscribers must be Sync; per-thread subscribers don't have this requirement.

Testing with Per-Thread Subscribers

use tracing_subscriber::fmt;
 
#[cfg(test)]
mod tests {
    use super::*;
    use tracing::info_span;
    
    // Per-thread subscribers are ideal for tests
    // Each test can have isolated logging
    
    #[test]
    fn test_isolated_logging() {
        // Each test sets up its own subscriber
        let subscriber = fmt()
            .with_test_writer()  // Writes to test output
            .finish();
        
        let _guard = tracing::subscriber::set_default(subscriber);
        
        // This test's logging is isolated
        let _span = info_span!("test_operation").entered();
        tracing::info!("Test message");
        
        // Guard drops at end, cleaning up
    }
    
    #[test]
    fn another_isolated_test() {
        // Different subscriber for different test
        let subscriber = fmt()
            .with_target(true)
            .with_level(false)
            .finish();
        
        let _guard = tracing::subscriber::set_default(subscriber);
        
        // This test's logging goes to its own subscriber
        tracing::info!("Another test message");
    }
}

Tests can use per-thread subscribers for isolation without interfering with each other.

Benchmark Isolation

use tracing_subscriber::fmt;
use criterion::{Criterion, black_box};
 
fn benchmark_example(c: &mut Criterion) {
    // For benchmarks, you often want to disable logging
    // or use a no-op subscriber
    
    c.bench_function("operation", |b| {
        b.iter(|| {
            // Set up minimal/no-op subscriber for benchmark
            let no_op = tracing_subscriber::NoSubscriber::default();
            let _guard = tracing::subscriber::set_default(no_op);
            
            // Benchmark code without logging overhead
            black_box(do_work());
        });
    });
}
 
fn do_work() -> i32 {
    tracing::info!("This won't be logged");  // No-op subscriber
    42
}

Benchmarks can use per-thread subscribers to eliminate logging overhead.

Combining Global Default with Thread-Local Overrides

use tracing_subscriber::fmt;
use std::thread;
 
fn combined_approach() {
    // Set up global default for production
    let global = fmt()
        .with_env_filter("info")
        .finish();
    tracing::subscriber::set_global_default(global).unwrap();
    
    // Most threads use the global default
    thread::spawn(|| {
        tracing::info!("Uses global subscriber");
    });
    
    // Specific thread can override with its own
    thread::spawn(|| {
        // This thread-local subscriber shadows the global
        let thread_local = fmt()
            .with_env_filter("debug")
            .finish();
        let _guard = tracing::subscriber::set_default(thread_local);
        
        tracing::debug!("Uses thread-local subscriber");
        tracing::info!("Also thread-local subscriber");
        
        // When guard drops, back to global default
    });
    
    // Note: Once global is set, you cannot set it again
    // But you can still use set_default per-thread
}

Per-thread subscribers can shadow the global default for specific threads.

Subscriber Priority and Dispatch

use tracing_subscriber::fmt;
use tracing::subscriber::Interest;
 
fn dispatch_order() {
    // When looking for a subscriber, tracing checks:
    // 1. Thread-local subscriber (set_default) - highest priority
    // 2. Global default subscriber (set_global_default)
    // 3. No subscriber (events go nowhere)
    
    // If a thread-local subscriber is set, it shadows global
    let global = fmt().with_target(true).finish();
    tracing::subscriber::set_global_default(global).unwrap();
    
    // Thread has its own subscriber
    let local = fmt().with_target(false).finish();
    let _guard = tracing::subscriber::set_default(local);
    
    tracing::info!("Goes to local subscriber");
    // Global subscriber is NOT called for this thread
    
    // Without thread-local, would go to global
}
 
fn no_subscriber() {
    // If neither is set, events go nowhere
    tracing::info!("This is lost");  // No subscriber
}

Thread-local subscribers take precedence over global default.

The Default Subscriber Pattern

use tracing_subscriber::fmt;
use std::thread;
 
fn default_fallback() {
    // Common pattern: try global first, fall back to thread-local
    
    // Check if global is already set
    if tracing::dispatcher::has_global_default() {
        tracing::info!("Global subscriber already set");
    } else {
        // No global, set thread-local
        let subscriber = fmt().finish();
        let _guard = tracing::subscriber::set_default(subscriber);
        
        tracing::info!("Using thread-local subscriber");
    }
}

Check for global default before setting thread-local.

Multiple Threads, Multiple Subscribers

use tracing_subscriber::fmt;
use std::thread;
use std::sync::Arc;
 
fn multiple_thread_configurations() {
    // Thread A: verbose logging
    let handle_a = thread::spawn(|| {
        let verbose = fmt()
            .with_max_level(tracing::Level::TRACE)
            .finish();
        let _guard = tracing::subscriber::set_default(verbose);
        
        tracing::trace!("Thread A trace message");
        tracing::info!("Thread A info message");
    });
    
    // Thread B: warnings only
    let handle_b = thread::spawn(|| {
        let warnings = fmt()
            .with_max_level(tracing::Level::WARN)
            .finish();
        let _guard = tracing::subscriber::set_default(warnings);
        
        tracing::trace!("Thread B trace - not shown");
        tracing::warn!("Thread B warning");
    });
    
    // Thread C: no logging
    let handle_c = thread::spawn(|| {
        let no_op = tracing_subscriber::NoSubscriber::default();
        let _guard = tracing::subscriber::set_default(no_op);
        
        tracing::info!("Thread C - not logged");
    });
    
    handle_a.join().unwrap();
    handle_b.join().unwrap();
    handle_c.join().unwrap();
}

Each thread can have completely different logging configurations.

Error Handling for set_global_default

use tracing_subscriber::fmt;
 
fn error_handling() {
    // First call succeeds
    let first = fmt().finish();
    tracing::subscriber::set_global_default(first)
        .expect("Failed to set global default");
    
    // Second call would panic
    let second = fmt().finish();
    let result = tracing::subscriber::set_global_default(second);
    
    match result {
        Ok(()) => println!("Global subscriber set"),
        Err(_) => println!("Global subscriber already set"),
    }
    
    // Common pattern: set once at application start
    // If called again, just ignore the error
}
 
fn safe_global_setup() {
    let subscriber = fmt()
        .with_env_filter("info")
        .finish();
    
    // Ignore error if already set (e.g., in tests)
    let _ = tracing::subscriber::set_global_default(subscriber);
}

set_global_default returns Result because it can only be called once.

Real-World Use Cases

use tracing_subscriber::{fmt, EnvFilter};
use std::thread;
 
fn production_example() {
    // Production: use global default
    let subscriber = fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .json()  // Structured logging for production
        .finish();
    
    tracing::subscriber::set_global_default(subscriber)
        .expect("Failed to set subscriber");
    
    // All threads use the same structured logging
}
 
fn library_example() {
    // Libraries should NOT set global default
    // Let the application decide
    
    // Instead, libraries can:
    // 1. Emit events without setting subscriber
    // 2. Document required subscriber setup
    
    tracing::info!("Library emits events");
    // Application is responsible for subscriber
}
 
fn test_example() {
    // Tests: use per-thread subscribers
    let subscriber = fmt()
        .with_test_writer()
        .with_max_level(tracing::Level::DEBUG)
        .finish();
    
    let _guard = tracing::subscriber::set_default(subscriber);
    
    // Isolated test logging
}
 
fn background_worker_example() {
    // Background workers with different log levels
    let worker = thread::spawn(|| {
        let worker_subscriber = fmt()
            .with_target(true)
            .with_max_level(tracing::Level::INFO)
            .finish();
        let _guard = tracing::subscriber::set_default(worker_subscriber);
        
        // Worker threads have their own logging
    });
}

Use global for production apps, per-thread for tests and specialized threads.

Comparison Table

fn comparison_table() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Aspect               β”‚ set_global_default      β”‚ set_default          β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ Scope                 β”‚ Process-wide            β”‚ Thread-local         β”‚
    // β”‚ Can set multiple timesβ”‚ No (returns Err)         β”‚ Yes (with guards)    β”‚
    // β”‚ Thread safety         β”‚ Requires Sync           β”‚ No Sync needed       β”‚
    // β”‚ Lifetime               β”‚ Permanent               β”‚ Guard-scoped         β”‚
    // β”‚ Use in libraries      β”‚ Avoid                    β”‚ Okay for scoped      β”‚
    // β”‚ Testing               β”‚ Awkward (can't reset)   β”‚ Ideal (isolated)     β”‚
    // β”‚ Production apps       β”‚ Recommended             β”‚ Unnecessary          β”‚
    // β”‚ Benchmark isolation   β”‚ Cannot isolate          β”‚ Perfect use case     β”‚
    // β”‚ Guard returned        β”‚ No (void return)        β”‚ Yes (RAII guard)     β”‚
    // β”‚ Overhead              β”‚ Shared across threads   β”‚ Per-thread alloc     β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
}

Complete Summary

use tracing_subscriber::fmt;
 
fn complete_summary() {
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Global Default (set_global_default):                                    β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ - Sets subscriber for entire process                                   β”‚
    // β”‚ - All threads use the same subscriber                                  β”‚
    // β”‚ - Can only be called once (subsequent calls fail)                      β”‚
    // β”‚ - Requires subscriber to be Sync                                      β”‚
    // β”‚ - Best for production applications                                     β”‚
    // β”‚ - Set at application startup                                           β”‚
    // β”‚ - Libraries should NOT call this                                      β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    
    // β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    // β”‚ Per-Thread (set_default):                                              β”‚
    // β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
    // β”‚ - Sets subscriber for current thread only                              β”‚
    // β”‚ - Each thread can have different subscriber                           β”‚
    // β”‚ - Can be called multiple times (with guards)                           β”‚
    // β”‚ - Subscriber doesn't need Sync                                        β”‚
    // β”‚ - Guard controls lifetime (RAII)                                      β”‚
    // β”‚ - Shadows global default for current thread                           β”‚
    // β”‚ - Best for testing, benchmarks, isolated threads                     β”‚
    // β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    
    // Choosing between them:
    // - Production apps: set_global_default
    // - Tests: set_default (isolated per test)
    // - Benchmarks: set_default (or NoSubscriber)
    // - Libraries: neither (let app decide)
    // - Worker threads needing different config: set_default
}

Key insight: set_global_default establishes a process-wide subscriber that all threads share, making it ideal for production applications where consistent logging is needed. set_default creates thread-local subscribers scoped to guard lifetimes, perfect for test isolation, benchmark configurations, or threads with specialized logging needs. The guard-based RAII pattern of set_default allows clean setup and teardown, while set_global_default's once-only restriction ensures stable logging configuration throughout the process lifetime. Choose global default for application-wide logging; choose per-thread subscribers for isolated or specialized logging contexts.