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.
