How does tracing::dispatcher::set_global_default differ from with_default for subscriber lifetime management?

set_global_default installs a subscriber globally for the entire process lifetime, consuming the subscriber and preventing further changes, while with_default temporarily sets a subscriber for the duration of a closure, automatically restoring the previous subscriber when the closure completes. The fundamental difference is scope: set_global_default creates a permanent, process-wide subscriber that outlives the function call, while with_default creates a scoped subscriber that exists only within a controlled context.

The Dispatcher and Global State

use tracing::{info, dispatcher};
 
fn dispatcher_basics() {
    // tracing uses a thread-local dispatcher to route events
    // The dispatcher holds the current subscriber
    
    // When an event is emitted:
    // 1. The dispatcher is consulted
    // 2. The current subscriber handles the event
    // 3. If no subscriber is set, events are discarded
    
    // There are two levels of dispatcher state:
    // 1. Global default: process-wide fallback
    // 2. Thread-local: per-thread override
    
    info!("This goes nowhere");  // No subscriber set
    
    // The global default is set once and applies everywhere
    // The thread-local can be temporarily overridden
}

tracing uses dispatchers to route events; without a subscriber, events are silently discarded.

set_global_default: Permanent Installation

use tracing::{info, Subscriber};
use tracing_subscriber::FmtSubscriber;
use tracing::dispatcher;
 
fn set_global_default_example() {
    // Create a subscriber
    let subscriber = FmtSubscriber::builder()
        .with_max_level(tracing::Level::INFO)
        .finish();
    
    // Set as global default - CONSUMES the subscriber
    dispatcher::set_global_default(subscriber)
        .expect("global default should not be set already");
    
    // Now all events in the process go to this subscriber
    info!("This is logged globally");
    
    // Cannot set again - panics or returns error
    let another_subscriber = FmtSubscriber::new();
    let result = dispatcher::set_global_default(another_subscriber);
    assert!(result.is_err());
    
    // The subscriber is owned by the global dispatcher
    // It exists for the remainder of the process
}

set_global_default permanently installs a subscriber; it cannot be changed or unset.

with_default: Scoped Temporary Subscriber

use tracing::{info, dispatcher};
use tracing_subscriber::FmtSubscriber;
 
fn with_default_example() {
    // Create a subscriber
    let subscriber = FmtSubscriber::builder()
        .with_max_level(tracing::Level::DEBUG)
        .finish();
    
    // with_default temporarily sets the subscriber
    dispatcher::with_default(&subscriber, || {
        // Within this closure, subscriber is active
        info!("This is logged with the subscriber");
        
        // Nested calls work too
        inner_function();
    });
    
    // After closure completes, subscriber is NO LONGER active
    // Events are discarded again
    
    // Previous dispatcher state is restored
    info!("This goes nowhere again");
}
 
fn inner_function() {
    info!("Also logged - same subscriber");
}

with_default activates a subscriber only for the duration of a closure.

Lifetime and Ownership Differences

use tracing::{info, dispatcher};
use tracing_subscriber::FmtSubscriber;
 
fn ownership_comparison() {
    // set_global_default: TAKES OWNERSHIP
    let subscriber = FmtSubscriber::new();
    
    // Subscriber is MOVED into set_global_default
    dispatcher::set_global_default(subscriber).unwrap();
    
    // Cannot use subscriber anymore - it's owned globally
    // let _ = subscriber;  // ERROR: subscriber was moved
    
    // with_default: BORROWS subscriber
    let subscriber2 = FmtSubscriber::new();
    
    // Subscriber is BORROWED for the closure duration
    dispatcher::with_default(&subscriber2, || {
        info!("Logged");
    });
    
    // Subscriber is still valid here - borrow ended
    // Can use it again
    dispatcher::with_default(&subscriber2, || {
        info!("Logged again");
    });
    
    // Subscriber can be used for multiple with_default calls
}

set_global_default takes ownership; with_default borrows temporarily.

The Global Default Cannot Be Unset

use tracing::{info, dispatcher};
use tracing_subscriber::FmtSubscriber;
 
fn global_cannot_unset() {
    // Once set, global default persists for process lifetime
    
    let subscriber = FmtSubscriber::new();
    dispatcher::set_global_default(subscriber).unwrap();
    
    // No way to:
    // - Remove the global subscriber
    // - Replace it with another
    // - Disable it temporarily
    // - Check if one is set (only try_set_global_default returns Result)
    
    // This is by design:
    // - Global state should be set once at startup
    // - Changing it mid-process causes confusion
    // - Lifetime would be unclear if it could be removed
    
    // If you need temporary subscribers, use with_default
}

The global default is intentionally permanent; there's no unset_global_default.

Thread-Local vs Global

use tracing::{info, dispatcher};
use tracing_subscriber::FmtSubscriber;
use std::thread;
 
fn thread_local_behavior() {
    // set_global_default: applies to ALL threads
    let global_subscriber = FmtSubscriber::builder()
        .with_target(false)
        .finish();
    
    dispatcher::set_global_default(global_subscriber).unwrap();
    
    // All threads inherit this subscriber
    let handle = thread::spawn(|| {
        info!("Logged in spawned thread");
    });
    
    handle.join().unwrap();
    
    // with_default: applies to CURRENT thread only
    
    let thread_subscriber = FmtSubscriber::builder()
        .with_target(true)
        .finish();
    
    dispatcher::with_default(&thread_subscriber, || {
        // Only THIS thread uses thread_subscriber
        // Other threads still use global (if set)
        
        let handle = thread::spawn(|| {
            // This thread uses global subscriber
            info!("Logged in spawned thread");
        });
        
        handle.join().unwrap();
        
        info!("Logged in main thread with thread_subscriber");
    });
}

set_global_default affects all threads; with_default is thread-local.

Nested with_default Calls

use tracing::{info, dispatcher, Level};
use tracing_subscriber::FmtSubscriber;
 
fn nested_with_default() {
    let subscriber1 = FmtSubscriber::builder()
        .with_max_level(Level::INFO)
        .finish();
    
    let subscriber2 = FmtSubscriber::builder()
        .with_max_level(Level::DEBUG)
        .finish();
    
    // Each with_default overrides for its scope
    dispatcher::with_default(&subscriber1, || {
        info!("Logged with subscriber1 (INFO level)");
        
        dispatcher::with_default(&subscriber2, || {
            // subscriber2 is now active
            info!("Logged with subscriber2 (DEBUG level)");
            
            // Can see DEBUG events
            tracing::debug!("Also visible");
        });
        
        // Back to subscriber1
        tracing::debug!("Not visible - INFO level only");
        info!("Logged with subscriber1 again");
    });
    
    // Dispatcher state is properly restored through nesting
}

with_default can be nested; previous subscribers are restored when inner closures complete.

Practical Use Case: Test Isolation

use tracing::{info, dispatcher};
use tracing_subscriber::FmtSubscriber;
 
#[cfg(test)]
mod tests {
    use super::*;
    
    fn setup_test_subscriber() -> impl tracing::Subscriber {
        FmtSubscriber::builder()
            .with_max_level(tracing::Level::DEBUG)
            .with_test_writer()  // Writes to test output
            .finish()
    }
    
    #[test]
    fn test_logging_behavior() {
        let subscriber = setup_test_subscriber();
        
        // Each test gets its own isolated subscriber
        dispatcher::with_default(&subscriber, || {
            // Test code here
            info!("Test log message");
            
            // Assert on logging behavior if needed
        });
        
        // Subscriber ends with the test
        // Other tests are not affected
    }
    
    #[test]
    fn another_test() {
        let subscriber = setup_test_subscriber();
        
        // This test has its own subscriber
        dispatcher::with_default(&subscriber, || {
            info!("Another test log");
        });
    }
    
    // If we used set_global_default:
    // - First test would set global subscriber
    // - Second test would fail (already set)
    // - Tests would interfere with each other
}

with_default is ideal for test isolation where each test needs its own subscriber.

Practical Use Case: Application Startup

use tracing::{info, dispatcher};
use tracing_subscriber::FmtSubscriber;
 
fn application_startup() {
    // Application initialization: set global subscriber
    
    let subscriber = FmtSubscriber::builder()
        .with_max_level(tracing::Level::INFO)
        .with_target(true)
        .with_thread_ids(true)
        .finish();
    
    // Set once at startup
    dispatcher::set_global_default(subscriber)
        .expect("Failed to set global default subscriber");
    
    // Now the entire application uses this subscriber
    info!("Application started");
    
    // All subsequent code benefits from logging
    run_application();
}
 
fn run_application() {
    // Events are logged via global subscriber
    info!("Running application");
}

set_global_default is ideal for application-wide logging setup at startup.

Practical Use Case: Scoped Logging Configuration

use tracing::{info, debug, dispatcher, Level};
use tracing_subscriber::FmtSubscriber;
 
fn scoped_logging() {
    // Application uses INFO level normally
    let normal_subscriber = FmtSubscriber::builder()
        .with_max_level(Level::INFO)
        .finish();
    
    dispatcher::set_global_default(normal_subscriber).unwrap();
    
    info!("Normal logging");
    debug!("Not visible - INFO level");
    
    // For specific operations, enable DEBUG
    let debug_subscriber = FmtSubscriber::builder()
        .with_max_level(Level::DEBUG)
        .finish();
    
    dispatcher::with_default(&debug_subscriber, || {
        info!("Detailed logging for this section");
        debug!("Now visible - DEBUG level");
        
        // Perform detailed debugging
        perform_debug_operation();
    });
    
    // Back to INFO level
    debug!("Not visible again");
}
 
fn perform_debug_operation() {
    debug!("Internal details logged");
}

with_default enables temporary logging changes for specific operations.

Error Handling Differences

use tracing::{dispatcher};
use tracing_subscriber::FmtSubscriber;
 
fn error_handling() {
    // set_global_default returns Result
    // - Ok(()) if successfully set
    // - Err(subscriber) if already set (returns the subscriber back)
    
    let subscriber1 = FmtSubscriber::new();
    
    match dispatcher::set_global_default(subscriber1) {
        Ok(()) => println!("Global subscriber set"),
        Err(_subscriber) => {
            // Global subscriber was already set
            // The subscriber is returned in the error
            println!("Global subscriber already exists");
        }
    }
    
    // with_default returns the closure's return value
    // It cannot fail - just temporarily sets subscriber
    
    let subscriber2 = FmtSubscriber::new();
    
    let result = dispatcher::with_default(&subscriber2, || {
        "value computed with logging"
    });
    
    assert_eq!(result, "value computed with logging");
    
    // There's also try_global_default which returns Result
    // But it's less commonly used
}

set_global_default returns a Result; with_default returns the closure's value.

Subscriber Trait and Dispatch

use tracing::{info, Subscriber, dispatcher};
use tracing_subscriber::FmtSubscriber;
 
fn subscriber_trait() {
    // Both functions work with any type implementing Subscriber
    
    // FmtSubscriber implements Subscriber
    let fmt_subscriber = FmtSubscriber::new();
    dispatcher::set_global_default(fmt_subscriber).unwrap();
    
    // Custom subscriber would also work:
    // struct MySubscriber { /* ... */ }
    // impl Subscriber for MySubscriber { /* ... */ }
    
    // The dispatcher routes events to the subscriber
    // When you call info!, it goes through:
    // 1. The dispatcher (thread-local first, then global)
    // 2. The subscriber's enabled() method
    // 3. The subscriber's event() method
    
    // The lifetime and ownership are about HOW
    // the subscriber is stored, not WHAT it is
}

Both functions work with any Subscriber; the difference is storage and lifetime.

Comparison with get_default

use tracing::{info, dispatcher};
use tracing_subscriber::FmtSubscriber;
 
fn get_default_example() {
    // There's also get_default for inspecting current dispatcher
    
    let subscriber = FmtSubscriber::new();
    dispatcher::set_global_default(subscriber).unwrap();
    
    // get_default retrieves the current dispatcher
    dispatcher::get_default(|dispatch| {
        // dispatch is &Dispatch
        // Can inspect but not modify
        println!("Dispatcher exists");
    });
    
    // This is read-only access to the dispatcher
    // Useful for:
    // - Checking if subscriber is set
    // - Getting subscriber reference
    // - Advanced dispatch operations
}

get_default provides read-only access to the current dispatcher.

Common Patterns and Antipatterns

use tracing::{info, dispatcher};
use tracing_subscriber::FmtSubscriber;
 
fn patterns() {
    // GOOD: Set global subscriber once at startup
    fn good_app_setup() {
        let subscriber = FmtSubscriber::new();
        dispatcher::set_global_default(subscriber).unwrap();
        // Application runs with this subscriber
    }
    
    // GOOD: Use with_default for scoped changes
    fn good_scoped_debug() {
        let subscriber = FmtSubscriber::new();
        
        dispatcher::with_default(&subscriber, || {
            // Scoped logging
            info!("In scope");
        });
        // Subscriber lifetime is clear
    }
    
    // BAD: Try to change global subscriber
    fn bad_change_global() {
        let subscriber1 = FmtSubscriber::new();
        dispatcher::set_global_default(subscriber1).unwrap();
        
        // ...later...
        
        let subscriber2 = FmtSubscriber::new();
        // This will fail!
        dispatcher::set_global_default(subscriber2).unwrap();  // Error!
    }
    
    // BAD: Use set_global_default in tests
    fn bad_test_pattern() {
        // First test
        let sub = FmtSubscriber::new();
        dispatcher::set_global_default(sub).unwrap();
        
        // Second test would fail
        // Can only set once per process
    }
    
    // GOOD: Use with_default in tests
    fn good_test_pattern() {
        let sub = FmtSubscriber::new();
        dispatcher::with_default(&sub, || {
            // Test code
        });
        // Each test can have its own subscriber
    }
}

Common patterns show proper usage for each approach.

The Dispatch Type

use tracing::{dispatcher, Dispatch};
use tracing_subscriber::FmtSubscriber;
 
fn dispatch_type() {
    // Dispatch wraps a Subscriber
    // Both functions work with Dispatch or Subscriber
    
    let subscriber = FmtSubscriber::new();
    
    // Can pass Subscriber directly
    dispatcher::set_global_default(subscriber).unwrap();
    
    // Or create Dispatch first
    let subscriber2 = FmtSubscriber::new();
    let dispatch = Dispatch::new(subscriber2);
    
    // with_default accepts &Dispatch or &impl Subscriber
    dispatcher::with_default(&dispatch, || {
        // Uses dispatch
    });
    
    // Dispatch provides:
    // - Reference counting
    // - Thread-safe sharing
    // - Dynamic dispatch
}

Dispatch is the underlying type that wraps subscribers for sharing.

Summary Table

fn summary() {
    // | Aspect | set_global_default | with_default |
    // |--------|-------------------|--------------|
    // | Scope | Process-wide | Thread-local, closure-scoped |
    // | Ownership | Takes ownership | Borrows |
    // | Lifetime | Process lifetime | Closure lifetime |
    // | Can unset | No | N/A (automatic) |
    // | Can nest | No (single set) | Yes (restores) |
    // | Threads | Affects all | Current thread only |
    // | Use case | App startup | Tests, scoped changes |
    // | Returns | Result<()> | Closure return value |
    // | Failure | If already set | Cannot fail |
}

Synthesis

Quick reference:

use tracing::dispatcher;
use tracing_subscriber::FmtSubscriber;
 
// set_global_default: Permanent, process-wide subscriber
let subscriber = FmtSubscriber::new();
dispatcher::set_global_default(subscriber).unwrap();
// - Takes ownership
// - Cannot be changed
// - Affects all threads
// - Use at application startup
 
// with_default: Temporary, scoped subscriber
let subscriber = FmtSubscriber::new();
dispatcher::with_default(&subscriber, || {
    // Subscriber active here
});
// - Borrows subscriber
// - Automatically restored after closure
// - Thread-local only
// - Use for tests and scoped changes

Key insight: set_global_default and with_default represent two fundamentally different lifetime strategies for subscriber management. set_global_default is a one-time initialization suitable for application startup—it takes ownership, affects all threads, and cannot be undone. with_default is a scoped temporary override suitable for tests and specific operations—it borrows the subscriber, affects only the current thread, and automatically restores the previous state. The choice depends on your lifecycle needs: permanent global logging versus temporary scoped logging. For applications, use set_global_default once at startup; for tests and scoped changes, use with_default. Attempting to change the global default after it's set will fail, which is intentional—global state should be initialized once and remain stable.