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 levelsThe 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 flowKey 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.
