What is the difference between tracing::info! and info_span! for creating log entries vs trace scopes?

tracing::info! creates a one-time event that records a log entry at a specific point in time, while info_span! creates a span that represents a duration of time and establishes a context for all events that occur within it. Events are instantaneous recordings of data, whereas spans are scoped contexts that can be entered and exited, carrying contextual information that applies to all nested operations and events.

Basic Event Creation with info!

use tracing::{info, Level};
 
fn basic_event() {
    // info! creates an event: a single point-in-time log record
    
    info!("Application started");
    
    // Events record data at the moment they're called
    let user_id = 12345;
    info!(user_id, "User logged in");
    
    // With structured fields
    info!(
        user_id = %user_id,
        action = "login",
        "User action recorded"
    );
    
    // Events are similar to traditional log statements
    // They record: timestamp, level, message, fields
    // Then they're done - no duration, no context
}

info! creates events that are recorded once and complete immediately.

Basic Span Creation with info_span!

use tracing::{info_span, Span};
 
fn basic_span() {
    // info_span! creates a span: a duration with context
    
    let span = info_span!("http_request", method = "GET", path = "/users");
    
    // The span exists but doesn't record anything yet
    // It needs to be entered to become active
    
    // Enter the span
    let _enter = span.enter();
    
    // Now this span is the current context
    // Any events inside will have this span's context
    
    info!("Processing request");  // This event is inside the span
    
    // Exiting the scope exits the span
    // The span's duration is: enter time to exit time
}

info_span! creates a span that represents a duration of time with contextual data.

Events vs Spans Conceptual Model

use tracing::{info, info_span, Level};
 
fn events_vs_spans() {
    // EVENTS: Point-in-time recordings
    // - Instantaneous
    // - Record data once
    // - Similar to traditional log statements
    // - No duration
    
    info!("This is an event");  // Recorded immediately, done
    
    // SPANS: Duration-based contexts
    // - Have a start and end
    // - Establish context for contained operations
    // - Can be entered and exited multiple times
    // - Carry fields that apply to all nested operations
    
    let span = info_span!("database_query", query_id = 123);
    let _enter = span.enter();
    
    info!("Query started");   // Event inside span context
    info!("Query completed"); // Event inside span context
    
    // The span provides context for both events
    // When analyzing: both events belong to "database_query" span
}

Events are points; spans are durations with context.

Span Context and Nested Events

use tracing::{info, info_span, span, Level};
 
fn span_context() {
    // Spans provide context for all events within them
    
    let request_span = info_span!("http_request", request_id = %12345);
    let _enter = request_span.enter();
    
    // All events here have request_id = 12345 in their context
    info!("Request received");
    
    {
        // Nested span
        let db_span = info_span!("database_query", table = "users");
        let _db_enter = db_span.enter();
        
        info!("Query executed");  // Context: both http_request and database_query
        
        // This event belongs to both spans:
        // - http_request (parent)
        // - database_query (current)
    }
    
    info!("Sending response");  // Back to just http_request context
}
 
// When analyzing traces:
// - http_request { request_id: 12345 }
//   - database_query { table: "users" }
//     - "Query executed" event (has both contexts)
//   - "Sending response" event (has http_request context)

Spans create a hierarchical context that events inherit.

Levels for Events and Spans

use tracing::{info, debug, trace, warn, error, info_span, debug_span, trace_span, Level};
 
fn levels() {
    // Events have levels (similar to log levels)
    trace!("Very detailed information");
    debug!("Debug information");
    info!("General information");
    warn!("Warning message");
    error!("Error message");
    
    // Spans also have levels
    // The level indicates the span's importance
    
    let trace_span = trace_span!("detailed_operation");  // TRACE level
    let debug_span = debug_span!("debug_operation");    // DEBUG level
    let info_span = info_span!("important_operation");   // INFO level
    
    // Level filtering applies to both:
    // - If level is filtered out, events aren't recorded
    // - If span level is filtered out, span isn't recorded
    // - Events inside filtered spans may still be recorded
    //   (depending on subscriber implementation)
}

Both events and spans use the same level hierarchy for filtering.

Span Lifecycle

use tracing::{info, info_span, Span};
 
fn span_lifecycle() {
    // Create span (not yet active)
    let span = info_span!("operation", id = 1);
    
    // Span exists but is idle
    // No duration tracking yet
    
    // Enter span (makes it current)
    let enter1 = span.enter();
    info!("First entry");  // Recorded inside span
    
    // Exit span (by dropping enter guard)
    drop(enter1);
    
    // Span is idle again, but still exists
    
    // Can enter again
    let enter2 = span.enter();
    info!("Second entry");  // Recorded inside same span
    
    drop(enter2);
    
    // Span can be cloned and entered from multiple threads
    let span2 = span.clone();
    
    // Span is finalized when all references are dropped
    // Subscriber receives: span created, entered, exited, closed
}

Spans have a lifecycle: created → entered → exited → closed.

Field Inheritance

use tracing::{info, info_span, debug_span};
 
fn field_inheritance() {
    // Parent span with fields
    let parent = info_span!("request", request_id = 123, user_id = 456);
    let _parent_enter = parent.enter();
    
    // Child span
    let child = debug_span!("database_query", query = "SELECT * FROM users");
    let _child_enter = child.enter();
    
    info!("Query result returned");
    
    // This event has access to all fields:
    // - request_id = 123 (from parent)
    // - user_id = 456 (from parent)
    // - query = "SELECT * FROM users" (from current)
    
    // Subscribers can access all parent fields when processing events
}

Events inherit fields from all active spans in their context.

When to Use Events

use tracing::{info, info_span, warn, error, Level};
 
fn when_to_use_events() {
    // Use events for:
    
    // 1. Point-in-time occurrences
    info!("Application started");
    info!("Configuration loaded");
    
    // 2. One-off observations
    let connection_count = 42;
    info!(connection_count, "Current connection count");
    
    // 3. Error and warning messages
    error!(error_code = 500, "Internal server error");
    warn!("Cache miss rate is high");
    
    // 4. Metric recordings
    let latency_ms = 150;
    info!(latency_ms, "Request latency");
    
    // 5. Debug information
    info!(?result, "Function returned");
    
    // Events are appropriate when you just need to record something
    // without establishing a context for other operations
}

Use events for single-point observations without duration context.

When to Use Spans

use tracing::{info, info_span, debug_span, Level};
 
fn when_to_use_spans() {
    // Use spans for:
    
    // 1. Operations with duration
    let span = info_span!("http_request", method = "GET", path = "/api/users");
    let _enter = span.enter();
    
    // All events during this operation share context
    info!("Request received");
    // ... processing ...
    info!("Response sent");
    
    // 2. Establishing context for nested operations
    fn process_request() {
        let span = info_span!("process_request", request_id = 123);
        let _enter = span.enter();
        
        validate_input();   // Events inside know they're in process_request
        query_database();   // Events inside know they're in process_request
        format_response();  // Events inside know they're in process_request
    }
    
    // 3. Tracing distributed operations
    let span = info_span!("database_transaction", tx_id = %generate_id());
    let _enter = span.enter();
    
    // 4. Performance profiling
    let span = info_span!("expensive_operation");
    let _enter = span.enter();
    // Span duration = time spent in expensive operation
}
 
fn validate_input() {
    debug!("Validating input");
}
 
fn query_database() {
    let span = debug_span!("database_query");
    let _enter = span.enter();
    debug!("Executing query");
}
 
fn format_response() {
    debug!("Formatting response");
}

Use spans when context should carry across multiple operations.

Structured Fields Comparison

use tracing::{info, info_span, Level};
 
fn structured_fields() {
    // Events: fields are recorded once with the event
    
    info!(
        user_id = 12345,
        action = "login",
        ip_address = "192.168.1.1",
        "User logged in"
    );
    
    // All fields are attached to this single event
    
    // Spans: fields establish context for duration
    
    let span = info_span!(
        "http_request",
        method = "GET",
        path = "/api/users",
        user_id = 12345  // This user_id applies to ALL events in span
    );
    let _enter = span.enter();
    
    // Events inside can omit user_id - it's in the span
    info!("Request started");
    info!("Processing parameters");
    info!("Sending response");
    
    // All three events have user_id = 12345 in their context
    // Without repeating the field in each event
}

Span fields provide context for multiple events; event fields are one-time.

Span Creation Patterns

use tracing::{info, info_span, Level, Span};
 
fn span_patterns() {
    // Pattern 1: Enter immediately
    {
        let _enter = info_span!("operation").entered();
        info!("Inside operation");
    }
    
    // Pattern 2: Create and enter separately
    let span = info_span!("another_operation", id = 42);
    {
        let _enter = span.enter();
        info!("Inside another operation");
    }
    // Can enter again later
    {
        let _enter = span.enter();
        info!("Inside same operation again");
    }
    
    // Pattern 3: Span with dynamic fields
    let user_id = get_user_id();
    let span = info_span!("user_operation", user_id = %user_id);
    let _enter = span.enter();
    
    // Pattern 4: Conditional span level
    let level = if is_important { Level::INFO } else { Level::DEBUG };
    let span = Span::new(level, "conditional_operation", 
        tracing::field::display("value"));
    let _enter = span.enter();
}
 
fn get_user_id() -> u64 { 42 }

Spans offer flexible creation and entry patterns.

Practical Example: HTTP Handler

use tracing::{info, info_span, debug, error, Level};
 
// Full example showing events and spans together
 
struct HttpRequest {
    method: String,
    path: String,
    user_id: Option<u64>,
}
 
async fn handle_request(req: HttpRequest) -> Result<String, String> {
    // Create span for entire request
    let span = info_span!(
        "http_request",
        method = %req.method,
        path = %req.path,
        user_id = tracing::field::Empty,  // May be set later
    );
    let _enter = span.enter();
    
    // Event: request received
    info!("Request received");
    
    // Set user_id if available
    if let Some(uid) = req.user_id {
        span.record("user_id", &uid);
    }
    
    // Validate
    validate_request(&req)?;
    debug!("Request validated");  // Event inside span
    
    // Process
    let result = process_request(&req).await;
    
    // Event: request completed
    info!("Request completed successfully");
    
    Ok(result)
}
 
async fn process_request(req: &HttpRequest) -> String {
    // Child span for processing
    let span = info_span!("process_request");
    let _enter = span.enter();
    
    // Events inside have both parent and child span context
    debug!("Processing request data");
    
    // Database query (another nested span)
    let db_result = query_database().await;
    
    debug!("Database query completed");
    
    db_result
}
 
async fn query_database() -> String {
    let span = debug_span!("database_query", table = "users");
    let _enter = span.enter();
    
    debug!("Executing SQL query");  // Event
    
    "result".to_string()
}
 
fn validate_request(req: &HttpRequest) -> Result<(), String> {
    if req.method.is_empty() {
        error!("Invalid method");  // Event
        return Err("Invalid method".to_string());
    }
    Ok(())
}

A complete HTTP handler shows how spans create context hierarchy.

Performance Considerations

use tracing::{info, info_span, Level};
 
fn performance() {
    // Events: minimal overhead
    // - Record and done
    // - No state to track
    
    for i in 0..1000 {
        info!(i, "Processing item");  // 1000 events
    }
    
    // Spans: more overhead
    // - Track entry/exit
    // - Maintain context stack
    // - But fields are recorded once
    
    for i in 0..1000 {
        let span = info_span!("process_item", item_id = i);
        let _enter = span.enter();
        
        info!("Processing");  // Has item_id in context
        
        // vs recording item_id in every event:
        // info!(item_id = i, "Processing");  // Repeats field
    }
    
    // Best practice: Use spans when context saves repetition
    // Use events for one-off recordings
}

Spans have overhead but can reduce repetition in events.

Subscriber Behavior

use tracing::{info, info_span, Level};
 
// Different subscribers handle events and spans differently
 
fn subscriber_behavior() {
    // Subscriber receives:
    
    // For events:
    // - event() callback with: metadata, fields, parent span
    
    // For spans:
    // - new_span() when created
    // - enter() when entered
    // - exit() when exited
    // - close() when all references dropped
    
    // Example subscriber behavior:
    
    // fmt subscriber (pretty printing):
    // - Events print as log lines
    // - Spans print "enter" and "exit" markers
    
    // json subscriber:
    // - Each event is a JSON object with span context
    // - Spans are tracked separately
    
    // jaeger/tracing subscriber:
    // - Spans become trace spans
    // - Events become logs within spans
    
    // The key insight:
    // - Events are always recorded (if not filtered)
    // - Spans may be sampled/filtered based on level
}

Subscribers receive different callbacks for events vs spans.

Memory and References

use tracing::{info, info_span, Span};
 
fn memory_management() {
    // Events have no persistent state
    info!("This event is recorded and forgotten");
    // No memory retained after recording
    
    // Spans have state
    
    let span = info_span!("operation");
    
    // Span tracks:
    // - Creation time
    // - Field values
    // - Entry count (how many times entered)
    // - Reference count
    
    // Multiple references to same span
    let span2 = span.clone();
    
    // Span is closed when all references dropped
    drop(span);
    // span2 still holds reference - span not closed yet
    
    drop(span2);
    // Now span is closed
}

Events are transient; spans persist until all references are dropped.

Synthesis

Core distinction:

// Event: point-in-time recording
info!("User logged in", user_id = 123);
// Records once, done
// No context for subsequent events
 
// Span: duration-based context
let span = info_span!("http_request", request_id = 123);
let _enter = span.enter();
info!("Processing request");  // Has request_id in context
info!("Sending response");    // Has request_id in context
// Both events belong to the span

Comparison:

Aspect info! Event info_span! Span
Duration Instantaneous Start to end
Context None (standalone) Provides context
Fields Recorded once Available to nested events
State None Tracks entry/exit
Use case One-off observations Operations with duration
Hierarchy None Parent-child relationships
Memory Transient Persistent until closed

When to use each:

// Use info! for:
// - Single log messages
// - Error/warning recordings
// - Metric observations
// - Debug output
 
// Use info_span! for:
// - HTTP requests
// - Database transactions
// - Function calls with duration
// - Any operation that has child operations
// - When context should carry to nested calls
 
// Typical pattern: span around operation, events inside
 
async fn handle_user(user_id: u64) {
    let span = info_span!("handle_user", user_id);
    let _enter = span.enter();
    
    info!("Starting user handling");  // Event
    
    validate_user(user_id).await;    // May have its own spans
    process_user(user_id).await;     // May have its own spans
    
    info!("Completed user handling"); // Event
}

Key insight: The fundamental difference is that info! creates events—point-in-time log entries similar to traditional logging—while info_span! creates a scope that represents a duration of time and provides context for all operations within it. Events answer "what happened at this moment?" while spans answer "what happened during this operation, and what was the context?" Use events for single observations that don't need to establish context for subsequent code, and use spans when you're tracing an operation that has internal steps, child operations, or multiple related events that should share context. The span's fields are automatically inherited by all events within its scope, eliminating repetitive field declarations and creating a trace hierarchy that distributed tracing systems can visualize as parent-child relationships.