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 changesKey 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.
