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.
