How does tracing::field::debug differ from display for formatting traced values?

tracing::field::debug and display control how values are formatted when recorded in trace spans and events, with debug using the Debug trait format (via {:?}) and display using the Display trait format (via {}). This distinction matters because the same type may have very different representations under Debug versus Display—Debug typically shows internal structure suitable for developers, while Display provides user-facing output. The tracing crate's field::debug() and field::display() functions allow you to explicitly choose which representation to use, overriding the default behavior. Understanding this choice helps you control what appears in logs and traces, making them more useful for their intended audience.

The Default Behavior

use tracing::{info_span, span, Instrument};
use tracing_subscriber::fmt;
 
#[derive(Debug)]
struct User {
    id: u64,
    name: String,
    email: String,
}
 
impl std::fmt::Display for User {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} ({})", self.name, self.email)
    }
}
 
fn default_behavior() {
    // Set up a simple subscriber to see output
    let subscriber = fmt::Subscriber::builder()
        .with_max_level(tracing::Level::INFO)
        .finish();
    
    tracing::subscriber::set_global_default(subscriber)
        .expect("Could not set global default subscriber");
    
    let user = User {
        id: 42,
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    
    // By default, tracing uses Debug format
    let span = info_span!("processing_user", user = ?user);
    //                              ^^^^
    //                         shorthand for field::debug(user)
    
    let _enter = span.enter();
    // Records: user=User { id: 42, name: "Alice", email: "alice@example.com" }
}

The ? sigil in tracing macros is shorthand for field::debug(), using Debug format.

Explicit debug and display Functions

use tracing::{info, info_span};
use tracing::field::{debug, display};
 
fn explicit_formatting() {
    let user = User {
        id: 42,
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    
    // Using debug() explicitly - same as ? shorthand
    info_span!("user_debug", user = debug(&user));
    // Output: user=User { id: 42, name: "Alice", email: "alice@example.com" }
    
    // Using display() - uses Display trait
    info_span!("user_display", user = display(&user));
    // Output: user=Alice (alice@example.com)
    
    // The difference is in what's recorded
}

Use field::debug() and field::display() to explicitly control formatting.

The % Sigil for Display

use tracing::{info, info_span};
 
fn display_sigil() {
    let user = User {
        id: 42,
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
    };
    
    // The % sigil is shorthand for display()
    info_span!("user_display", user = %user);
    //                               ^^
    //                          equivalent to display(&user)
    
    // Compare:
    // ?user  -> debug format (Debug trait)
    // %user  -> display format (Display trait)
    // user   -> uses value's Recorded implementation
}

The % sigil in tracing macros is shorthand for field::display().

When Types Implement Both Traits

use tracing::{info, span, Level};
 
struct Config {
    path: std::path::PathBuf,
    max_connections: usize,
}
 
impl std::fmt::Debug for Config {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Config")
            .field("path", &self.path)
            .field("max_connections", &self.max_connections)
            .finish()
    }
}
 
impl std::fmt::Display for Config {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Config(max={})", self.max_connections)
    }
}
 
fn both_traits() {
    let config = Config {
        path: std::path::PathBuf::from("/etc/app/config.toml"),
        max_connections: 100,
    };
    
    // Debug shows internal structure
    info!(?config, "Loaded config");
    // config=Config { path: "/etc/app/config.toml", max_connections: 100 }
    
    // Display shows user-friendly version
    info!(%config, "Loaded config");
    // config=Config(max=100)
    
    // Choose based on audience:
    // - Debug for developers troubleshooting
    // - Display for operators reading logs
}

Types with both Debug and Display can present different information depending on context.

Types with Only Debug

use tracing::{info, debug};
 
#[derive(Debug)]
struct InternalState {
    buffer: Vec<u8>,
    counters: [usize; 4],
    flags: u32,
}
 
// Many types don't implement Display - only Debug
// Using % (display) would fail to compile
 
fn debug_only_types() {
    let state = InternalState {
        buffer: vec![1, 2, 3, 4, 5],
        counters: [10, 20, 30, 40],
        flags: 0b1010,
    };
    
    // This works - Debug is derived
    info!(?state, "Current state");
    // state=InternalState { buffer: [1, 2, 3, 4, 5], counters: [10, 20, 30, 40], flags: 10 }
    
    // This would NOT compile - no Display implementation:
    // info!(%state, "Current state");  // Error: Display not implemented
}

For types that only implement Debug, use ? or debug().

The Default Without Sigils

use tracing::{info, info_span};
 
fn default_without_sigils() {
    let count = 42;
    let name = "test".to_string();
    
    // Without any sigil, what happens?
    info!(count = count, "Processing");
    // For primitive types like i32, it just records the value
    
    info!(name = name, "Processing");
    // For String, it records the string content
    
    // But for types implementing both Debug and Display:
    let user = User { id: 1, name: "Bob".into(), email: "bob@test.com".into() };
    
    // Without sigil, tracing will use the Display trait if available
    // Actually, it depends on the type's implementation of tracing::Value
    // For most types, this falls back to Debug
    
    // Best practice: Be explicit with ? or % when it matters
    info!(user = ?user, "Using debug");
    info!(user = %user, "Using display");
}

Without sigils, the behavior depends on the type's Value implementation.

Practical Example: HTTP Request Logging

use tracing::{info, debug, field};
use std::net::SocketAddr;
 
struct HttpRequest {
    method: String,
    path: String,
    client_ip: SocketAddr,
    headers: std::collections::HashMap<String, String>,
}
 
impl std::fmt::Display for HttpRequest {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {}", self.method, self.path)
    }
}
 
impl std::fmt::Debug for HttpRequest {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("HttpRequest")
            .field("method", &self.method)
            .field("path", &self.path)
            .field("client_ip", &self.client_ip)
            .field("headers", &self.headers)
            .finish()
    }
}
 
fn log_http_request() {
    let request = HttpRequest {
        method: "GET".to_string(),
        path: "/api/users/42".to_string(),
        client_ip: "192.168.1.100:54321".parse().unwrap(),
        headers: vec![
            ("Authorization".to_string(), "Bearer secret-token".to_string()),
            ("Content-Type".to_string(), "application/json".to_string()),
        ].into_iter().collect(),
    };
    
    // Display format for production logs - cleaner, more readable
    info!(request = %request, client_ip = %request.client_ip, "Incoming request");
    // request=GET /api/users/42 client_ip=192.168.1.100:54321
    
    // Debug format for troubleshooting - shows all details
    debug!(?request, "Full request details");
    // request=HttpRequest { method: "GET", path: "/api/users/42", 
    //        client_ip: 192.168.1.100:54321, headers: {...} }
}

Use Display for production logs, Debug for detailed troubleshooting.

Performance Considerations

use tracing::{debug_span, field};
 
struct LargeData {
    id: u64,
    data: Vec<u8>,  // Could be megabytes
}
 
impl std::fmt::Debug for LargeData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LargeData")
            .field("id", &self.id)
            .field("len", &self.data.len())
            .finish()
    }
}
 
impl std::fmt::Display for LargeData {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "LargeData({})", self.id)
    }
}
 
fn performance() {
    let large = LargeData {
        id: 1,
        data: vec![0u8; 1_000_000],  // 1MB of data
    };
    
    // Both are fast because we pass references
    // The formatter is only called if the span/event is recorded
    
    // But Debug might serialize more data if recorded
    // Display is more predictable in output size
    
    // If debugging is disabled at compile time, neither runs
    // If runtime level filtering, check level first:
    if tracing::enabled!(tracing::Level::DEBUG) {
        debug_span!("data", ?large);
    }
}

Both formats are lazy, but Display output size is typically more predictable.

Using display and debug in Span Creation

use tracing::{info_span, debug_span, field};
 
fn span_creation() {
    let user_id = 42;
    let user_name = "Alice".to_string();
    
    // In span creation, the shorthand forms are common
    let span = info_span!(
        "user_operation",
        user_id = user_id,      // Uses default (Display for String)
        user_id_debug = ?user_id, // Debug format
        name = %user_name,        // Display format
        name_debug = ?user_name,  // Debug format
    );
    
    // For dynamic values at span creation:
    let metrics = vec![("requests", 100), ("errors", 5)];
    
    // Can't use % with complex expressions directly
    // Use field::display() or field::debug() for that
    let span = info_span!(
        "metrics",
        data = field::debug(&metrics)
    );
}

Use sigils for simple values, field::debug() and field::display() for complex expressions.

The field::debug Function Signature

use tracing::field;
 
// field::debug returns a DebugValue wrapper
// It's generic over types implementing Debug
 
fn debug_function() {
    // The function wraps a reference and records using Debug
    let value = 42;
    let debug_value = field::debug(&value);
    
    // It implements field::Value, so it can be used in events/spans
    tracing::info!(value = field::debug(&value), "Logging value");
    
    // Equivalent to:
    tracing::info!(value = ?value, "Logging value");
    
    // But useful when you need to pass it around:
    fn log_value(value: &impl std::fmt::Debug) {
        tracing::info!(value = field::debug(value), "Logged");
    }
}

field::debug() creates a wrapper that implements the Value trait.

The field::display Function Signature

use tracing::field;
 
// field::display returns a DisplayValue wrapper
// It's generic over types implementing Display
 
fn display_function() {
    let message = "Hello, world!";
    
    // The function wraps a reference and records using Display
    tracing::info!(msg = field::display(&message), "Logging message");
    
    // Equivalent to:
    tracing::info!(msg = %message, "Logging message");
    
    // Useful for types with both traits where you want Display:
    #[derive(Debug)]
    struct Status { code: u16 }
    impl std::fmt::Display for Status {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            write!(f, "Status({})", self.code)
        }
    }
    
    let status = Status { code: 200 };
    tracing::info!(status = field::display(&status), "Response");
    // Uses Display, even though Debug exists
}

field::display() creates a wrapper that uses the Display trait.

Custom Formatting with Custom Wrappers

use tracing::{info, field};
use std::fmt;
 
// When neither Debug nor Display gives what you want,
// create a wrapper with custom formatting
 
struct HexBytes<'a>(&'a [u8]);
 
impl fmt::Debug for HexBytes<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "0x")?;
        for byte in self.0 {
            write!(f, "{:02x}", byte)?;
        }
        Ok(())
    }
}
 
struct ShortSummary<'a>(&'a str);
 
impl fmt::Display for ShortSummary<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.0.len() > 20 {
            write!(f, "{}...", &self.0[..20])
        } else {
            write!(f, "{}", self.0)
        }
    }
}
 
fn custom_formatting() {
    let data = b"\xde\xad\xbe\xef\xca\xfe\xba\xbe";
    let long_text = "This is a very long string that should be truncated for display";
    
    // Custom hex format for bytes
    info!(data = ?HexBytes(data), "Raw data");
    // data=0xdeadbeefcafebabe
    
    // Truncated display for long strings
    info!(text = %ShortSummary(long_text), "Processing text");
    // text=This is a very long...
}

Create wrapper types with custom Debug or Display for specialized formatting.

Choosing Between debug and display

use tracing::{info, debug, trace, Level};
 
// Decision guide for choosing format:
//
// Use Debug (? or debug()):
// - Internal state representation
// - Struct fields and data structures
// - Error details for debugging
// - Complex types without Display
// - Development/troubleshooting logs
//
// Use Display (% or display()):
// - User-facing messages
// - Summary information
// - Types with meaningful Display
// - Production logs
// - Metrics and counts
 
#[derive(Debug)]
struct CacheEntry {
    key: String,
    value: Vec<u8>,
    created_at: std::time::Instant,
    hits: u64,
}
 
impl std::fmt::Display for CacheEntry {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "CacheEntry(key={}, hits={})", self.key, self.hits)
    }
}
 
fn choosing_format() {
    let entry = CacheEntry {
        key: "user:42".to_string(),
        value: vec![1, 2, 3, 4, 5],
        created_at: std::time::Instant::now(),
        hits: 100,
    };
    
    // Debug for detailed internal state
    debug!(?entry, "Cache miss");  // For troubleshooting
    
    // Display for summary in production
    info!(%entry, "Cache hit");    // For production logs
    
    // Mix both when useful
    info!(key = %entry.key, hits = entry.hits, "Cache stats");
    // Clear, structured, production-appropriate
}

Choose based on audience: developers need Debug, operators need Display.

Synthesis

Quick reference:

use tracing::{info, debug, field};
 
fn quick_reference() {
    let data = MyData::new();
    
    // Shorthand forms (inside macros):
    info!(data = ?data, "Debug format");      // Uses Debug trait
    info!(data = %data, "Display format");    // Uses Display trait
    
    // Function forms (for passing around):
    let debug_value = field::debug(&data);
    let display_value = field::display(&data);
    info!(data = debug_value, "Using function form");
    
    // Key differences:
    // - Debug: {:?} format, internal structure, developer-focused
    // - Display: {} format, user-facing, summary-focused
    
    // When to use which:
    // - Debug: troubleshooting, development, internal state
    // - Display: production logs, metrics, user-visible messages
    
    // Common patterns:
    // - Use ? (debug) for types implementing only Debug
    // - Use % (display) for user-facing summaries
    // - Use field::debug/display() for complex expressions
    // - Create wrapper types for custom formatting
}

Key insight: The distinction between field::debug() and field::display() mirrors Rust's Debug and Display traits, giving you control over how values appear in traces. Debug (via ? or field::debug()) shows internal structure—fields, collections, nested data—which is essential for development and troubleshooting. Display (via % or field::display()) provides user-facing summaries, making it better for production logs and operator-readable messages. The choice affects not just readability but also log volume: Debug output can be verbose for complex types, while Display is typically concise. When a type implements both traits, choose based on your audience: Debug when the reader is a developer investigating an issue, Display when the reader is an operator or when logs feed into monitoring systems. For types with only Debug, the choice is forced, but you can implement Display or create wrapper types for specialized formatting. The lazy evaluation of tracing macros means both formats are computed only when the span/event is actually recorded, so performance differences mainly relate to output size and serialization cost when enabled.