How does tracing::span::Entered guard automatic span exit when it goes out of scope?

Entered is a RAII guard that automatically exits a span when dropped, ensuring tracing context is properly maintained even when code exits early through returns, errors, or panics. When you call span.enter(), it returns an Entered guard that marks the current thread as being inside that span. The Drop implementation on Entered handles the span exit, making it impossible to forget to leave a span—the compiler ensures cleanup when the guard goes out of scope. This pattern prevents subtle bugs where spans are accidentally left active, corrupting the tracing context for subsequent operations.

Basic Span Entry and Exit

use tracing::{span, Level};
 
fn process_item(id: u32) {
    let span = span!(Level::INFO, "process_item", id = id);
    
    // enter() returns an Entered guard
    let _enter = span.enter();
    
    // Code here runs within the span context
    tracing::info!("processing started");
    
    // When _enter goes out of scope, the span is exited
    // No explicit exit() call needed
}
 
fn main() {
    tracing_subscriber::fmt::init();
    
    process_item(42);
    // Span automatically exited when process_item returns
}

The Entered guard ensures the span exits when _enter is dropped.

RAII Pattern in Action

use tracing::{span, Level};
 
fn demonstrate_raii() {
    let span = span!(Level::INFO, "outer_operation");
    
    // Span enters scope
    {
        let _enter = span.enter();
        tracing::info!("inside span");
        
        // Span exits automatically at this closing brace
        // _enter's Drop impl handles exit
    }
    
    // No active span here
    tracing::info!("outside span");
}
 
fn early_return() {
    let span = span!(Level::INFO, "early_return_example");
    let _enter = span.enter();
    
    if some_condition() {
        return;  // _enter dropped here, span exits
    }
    
    // _enter dropped here too, span exits
}
 
fn some_condition() -> bool { false }

The guard ensures cleanup happens regardless of how the scope exits.

Nested Span Management

use tracing::{span, Level};
 
fn nested_spans() {
    let outer = span!(Level::INFO, "outer");
    let _outer_enter = outer.enter();
    
    tracing::info!("in outer span");
    
    {
        let inner = span!(Level::INFO, "inner");
        let _inner_enter = inner.enter();
        
        tracing::info!("in inner span");
        // Inner span is current here
        
        // _inner_enter dropped, inner span exits
    }
    
    tracing::info!("back in outer span");
    // Outer span is current again
    
    // _outer_enter dropped, outer span exits
}

Each Entered guard manages its own span, properly restoring the parent context.

The Drop Implementation

use tracing::{span, Level, Instrument};
 
// What happens when Entered is dropped:
// 1. The current span context is popped from the thread-local stack
// 2. The parent span (if any) becomes the current span
// 3. The span's reference count is decremented
 
fn manual_explanation() {
    let span = span!(Level::INFO, "example");
    
    // enter() does:
    // - Pushes span onto thread-local stack
    // - Returns Entered guard
    
    let _enter = span.enter();
    
    // When _enter is dropped:
    // - Pops span from thread-local stack
    // - Restores parent span context
    // - Thread-local state is cleaned up
}

Drop implementation handles all thread-local state management.

Preventing Use-After-Move

use tracing::{span, Level};
 
fn valid_usage() {
    let span = span!(Level::INFO, "valid");
    
    // Correct: guard stored and held for scope
    let _enter = span.enter();
    // ... work ...
}
 
fn invalid_usage() {
    let span = span!(Level::INFO, "invalid");
    
    // WRONG: entering without storing the guard
    span.enter();  // Guard immediately dropped!
    
    // The span is already exited here
    tracing::info!("NOT in span!");  // Misleading
}
 
fn main() {
    tracing_subscriber::fmt::init();
    
    valid_usage();
    
    // The compiler will warn about unused result, but
    // the code compiles. The span enters and immediately exits.
}

Storing the guard in a variable (even _enter) prevents immediate drop.

Span Context Restoration

use tracing::{span, Level, info_span};
 
fn demonstrate_context_restoration() {
    let parent = info_span!("parent");
    let _parent_enter = parent.enter();
    
    // Current span is "parent"
    
    {
        let child = info_span!(parent: &parent, "child");
        let _child_enter = child.enter();
        
        // Current span is "child"
        // Parent context is preserved in stack
        
        // When _child_enter drops:
        // - "child" exits
        // - "parent" restored as current
    }
    
    // Current span is "parent" again
}
 
fn error_handling() -> Result<(), Box<dyn std::error::Error>> {
    let span = span!(Level::INFO, "error_example");
    let _enter = span.enter();
    
    // Early exit via error - guard still dropped
    some_fallible_operation()?;
    
    // Normal exit - guard still dropped
    Ok(())
}
 
fn some_fallible_operation() -> Result<(), Box<dyn std::error::Error>> {
    Ok(())
}

Parent spans are correctly restored when child guards drop.

The entered Method for Immediate Entry

use tracing::{span, Level};
 
fn main() {
    // Standard approach: enter() returns guard
    let span = span!(Level::INFO, "standard");
    let _guard = span.enter();  // Guard must be stored
    
    // Alternative: entered() for immediate use
    let span2 = span!(Level::INFO, "immediate");
    span2.entered();  // Warning: guard immediately dropped
    
    // Correct with entered():
    let _guard = span!(Level::INFO, "correct").entered();
    // entered() is shorthand for creating span + enter in one call
}

entered() creates the span and enters it in one operation.

Span and Guard Separation

use tracing::{span, Level};
 
fn separation_pattern() {
    // Span can be created without entering
    let span = span!(Level::INFO, "deferred");
    
    // Do some work before entering
    let data = prepare_data();
    
    // Enter span only when needed
    {
        let _enter = span.enter();
        process_data(&data);
    }
    
    // Span exited, but span object still exists
    // Can enter again if needed
    {
        let _enter = span.enter();
        log_results();
    }
}
 
fn prepare_data() -> Vec<u8> { vec![] }
fn process_data(_: &[u8]) {}
fn log_results() {}

The span and its entry guard are separate, enabling deferred entry.

Cloning Spans vs Guards

use tracing::{span, Level};
 
fn clone_span() {
    let original = span!(Level::INFO, "original");
    let _enter = original.enter();
    
    // Spans can be cloned
    let cloned = original.clone();
    
    // But Entered guards CANNOT be cloned
    // let _enter2 = _enter.clone();  // Does not compile!
    
    // Each entry needs its own guard
    let _enter2 = cloned.enter();  // This works
}
 
fn main() {
    clone_span();
}

Entered guards cannot be cloned—they represent unique entry into a span.

Thread Safety

use tracing::{span, Level};
use std::thread;
 
fn thread_local_guards() {
    let span = span!(Level::INFO, "multi_thread");
    
    // Each thread has its own span context stack
    let span_clone = span.clone();
    
    let handle = thread::spawn(move || {
        let _enter = span_clone.enter();
        tracing::info!("in spawned thread");
        // Guard dropped here, exits span in this thread
    });
    
    handle.join().unwrap();
    
    // Original span still valid in main thread
    let _enter = span.enter();
    tracing::info!("in main thread");
}

Entered guards are thread-local—each thread manages its own context.

In_async Context

use tracing::{span, Level, Instrument};
use tokio::time::{sleep, Duration};
 
// WRONG: Entered guard across await points
async fn wrong_usage() {
    let span = span!(Level::INFO, "wrong");
    let _enter = span.enter();  // Guard held across await!
    
    sleep(Duration::from_millis(100)).await;
    // Problem: thread may change, span context invalid
    
    tracing::info!("still in span? maybe!");
}
 
// RIGHT: Use instrument() for async
async fn right_usage() {
    let span = span!(Level::INFO, "right");
    
    async {
        sleep(Duration::from_millis(100)).await;
        tracing::info!("correctly in span");
    }
    .instrument(span)
    .await;
}
 
// Manual approach for async (if needed)
async fn manual_async() {
    let span = span!(Level::INFO, "manual");
    
    // Enter span only in sync code
    {
        let _enter = span.enter();
        tracing::info!("sync work");
    }
    
    // Async work without span context
    sleep(Duration::from_millis(100)).await;
    
    // Re-enter for more sync work
    {
        let _enter = span.enter();
        tracing::info!("more sync work");
    }
}
 
#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();
    
    right_usage().await;
}

Entered guards should not be held across .await points—use .instrument() instead.

Custom Drop Behavior

use tracing::{span, Level};
 
fn drop_order() {
    let outer = span!(Level::INFO, "outer");
    let inner = span!(Level::INFO, "inner");
    
    {
        let _outer = outer.enter();
        tracing::info!("entered outer");  // Span: outer
        
        {
            let _inner = inner.enter();
            tracing::info!("entered inner");  // Span: inner
            
            // _inner dropped first (LIFO order)
        }
        
        tracing::info!("back to outer");  // Span: outer
        
        // _outer dropped second
    }
    
    tracing::info!("no span");  // No span active
}
 
fn main() {
    tracing_subscriber::fmt::init();
    drop_order();
}

Guards are dropped in reverse order, correctly unwinding the span stack.

The in_span Pattern

use tracing::{span, Level};
 
fn with_span<F, R>(f: F) -> R
where
    F: FnOnce() -> R,
{
    let span = span!(Level::INFO, "wrapper");
    let _enter = span.enter();
    f()
}
 
fn main() {
    tracing_subscriber::fmt::init();
    
    with_span(|| {
        tracing::info!("inside wrapper");
    });
    
    // Alternative: tracing::instrument
}
 
// Even with early returns:
fn early_exit_example() -> i32 {
    let span = span!(Level::INFO, "early_exit");
    let _enter = span.enter();
    
    if true {
        return 42;  // _enter dropped here, span exits
    }
    
    0  // Unreachable, but _enter would drop here too
}

The RAII pattern works seamlessly with early returns.

Comparing Manual vs RAII Management

use tracing::{span, Level};
 
// Hypothetical manual management (not how tracing works)
fn manual_anti_pattern() {
    let span = span!(Level::INFO, "manual");
    
    // If tracing required manual exit:
    // span.enter_manual();
    // ... work ...
    // span.exit_manual();  // Easy to forget!
    
    // Problem: What if an error occurs?
    // Problem: What if there are multiple returns?
}
 
// RAII approach (how tracing actually works)
fn raii_pattern() {
    let span = span!(Level::INFO, "raii");
    let _enter = span.enter();
    
    // Span automatically exits when _enter drops
    // Works with early returns:
    if some_condition() {
        return;  // _enter dropped, span exits
    }
    
    // Works with errors:
    some_fallible().unwrap();  // _enter dropped on panic too
    
    // Works with normal completion
}
 
fn some_condition() -> bool { false }
fn some_fallible() -> Result<(), ()> { Ok(()) }
 
fn main() {
    tracing_subscriber::fmt::init();
    raii_pattern();
}

RAII eliminates the possibility of forgetting to exit a span.

Memory and Performance

use tracing::{span, Level};
 
fn performance_characteristics() {
    // Entered guard is zero-sized
    let span = span!(Level::INFO, "perf");
    let _enter = span.enter();
    
    // std::mem::size_of::<Entered<'_>>() == 0
    // (approximately - there may be alignment overhead)
    
    // Entering a span:
    // - Increments span's reference count
    // - Pushes span onto thread-local stack
    // - Updates current span pointer
    
    // Exiting a span (drop):
    // - Pops span from thread-local stack
    // - Decrements reference count
    // - Restores parent span pointer
}
 
fn main() {
    tracing_subscriber::fmt::init();
    performance_characteristics();
}

The Entered guard itself is very lightweight—most overhead is in the span context management.

Synthesis

Quick reference:

use tracing::{span, Level};
 
fn main() {
    // Basic usage
    let span = span!(Level::INFO, "operation");
    let _enter = span.enter();
    tracing::info!("inside span");
    // _enter dropped here, span exits
    
    // Pattern: underscore variable
    // The underscore prefix (_enter) indicates:
    // "We need to keep this alive, but don't use it directly"
    
    // Short form
    let _span = span!(Level::INFO, "short").entered();
    
    // Nested spans
    let outer = span!(Level::INFO, "outer");
    let _o = outer.enter();
    {
        let inner = span!(Level::INFO, "inner");
        let _i = inner.enter();
        // Inside both spans
    }
    // Only in outer span
    
    // Key guarantee: span ALWAYS exits when guard drops
    // - Normal completion
    // - Early return
    // - Error propagation
    // - Panic (if unwinding)
}

Key insight: Entered implements the RAII (Resource Acquisition Is Initialization) pattern for span lifecycle management. The guard's Drop implementation is the critical piece—it ensures the thread-local span context is properly unwound regardless of how the scope exits. Without this pattern, you'd need manual enter/exit calls that are error-prone, especially with early returns and error handling. The Entered guard is zero-sized (no heap allocation) and carries no data—it's purely a compile-time mechanism that ensures drop runs at the right time. Remember: always store the guard in a variable (even if just _enter), never hold Entered guards across .await points (use .instrument() for async), and let Rust's scoping rules handle the rest. The span stack is thread-local, so each thread manages its own context independently, making the pattern safe for concurrent code.