How does tracing::span::Entered guard ensure proper span exit on scope drop?

Entered is a RAII guard returned by Span::enter() that stores a reference to the span and implements Drop to automatically exit the span when the guard goes out of scope, ensuring proper span cleanup even in the presence of early returns, errors, or panics. This pattern guarantees that span exit always pairs with span entry, preventing mismatched span states that could corrupt tracing context.

Basic Span Entry and Exit

use tracing::{span, Level};
 
fn basic_span_usage() {
    // Create a span
    let span = span!(Level::INFO, "my_span");
    
    // enter() returns an Entered guard
    // The span is now "entered" - it becomes the current span
    let _enter = span.enter();
    
    // Code here executes within "my_span" context
    // Any events will be associated with this span
    
    // When _enter goes out of scope, the span is exited
    // This happens automatically at the end of the function
}

The _enter guard ensures the span is exited when it goes out of scope.

The Entered Guard Type

use tracing::{span, Level};
 
fn entered_type() {
    let span = span!(Level::DEBUG, "operation");
    
    // span.enter() returns a value of type Entered<'_>
    // The '_ is the lifetime of the borrow of the Span
    let _entered: tracing::span::Entered<'_> = span.enter();
    
    // Entered<'a> is defined roughly as:
    // pub struct Entered<'a> {
    //     span: &'a Span,
    // }
    
    // It holds a reference to the Span it entered
    // When dropped, it calls span.exit()
}

Entered<'a> borrows the span for its entire lifetime, preventing modification while entered.

How Drop Triggers Span Exit

use tracing::{span, Level};
 
fn drop_mechanism() {
    let span = span!(Level::INFO, "database_query");
    
    {
        // Enter the span
        let _enter = span.enter();
        
        // Within this block, we're inside "database_query"
        tracing::info!("executing query");  // This event is inside the span
        
        // _enter is dropped here at the end of the block
        // The Drop implementation for Entered calls span.exit()
    }
    
    // Outside the block, we're no longer in "database_query"
    // Events here are NOT associated with the span
    
    // This mechanism works even with early returns:
    let span2 = span!(Level::DEBUG, "early_return");
    let _enter2 = span2.enter();
    
    if true {
        // Early return
        return;  // _enter2 is dropped here, span2.exit() is called
    }
    
    // This code never runs, but span exit still happened
}

The Drop implementation ensures cleanup on any scope exit path.

RAII Pattern for Scoped Resources

use tracing::{span, Level};
 
fn raii_pattern() {
    // RAII = Resource Acquisition Is Initialization
    // The resource (span entry) is acquired during initialization
    // The resource (span exit) is released during destruction
    
    let span = span!(Level::INFO, "request_handler");
    
    // The Entered guard implements the RAII pattern:
    // 1. Construction: Acquire resource (enter span)
    // 2. Destruction: Release resource (exit span)
    
    // This is safer than manual enter/exit:
    fn manual_approach() {
        let span = span!(Level::INFO, "manual");
        span.enter();  // Problem: no guard returned
        
        // If this code panics or returns early,
        // span.exit() might never be called
        
        // ... code ...
        
        span.exit();  // Might be skipped
    }
    
    // The RAII guard prevents this issue
    fn safe_approach() {
        let span = span!(Level::INFO, "safe");
        let _enter = span.enter();  // Guard returned
        
        // If this panics:
        // _enter is dropped during unwinding
        // span.exit() is called automatically
        
        // ... code ...
        
        // span.exit() called when _enter drops
    }
}

RAII guarantees cleanup even during panics or early returns.

Panic Safety

use tracing::{span, Level};
 
fn panic_safety() {
    let span = span!(Level::ERROR, "panic_test");
    
    fn might_panic() {
        let _enter = span!(Level::INFO, "inner").enter();
        
        panic!("something went wrong!");
        
        // Even though we panic:
        // 1. _enter's Drop is called during unwinding
        // 2. The inner span is exited
        // 3. The tracing context is restored to the parent
    }
    
    // If might_panic() panics:
    // The Entered guard ensures span exit happens
    // The span stack is properly restored
    
    // This is critical for maintaining correct tracing context
    // after panics
}

Drop is called during unwinding, ensuring cleanup even on panic.

Nested Spans and the Span Stack

use tracing::{span, Level};
 
fn nested_spans() {
    // tracing maintains a stack of entered spans
    // The current span is the top of the stack
    
    let outer = span!(Level::INFO, "outer");
    let _outer_enter = outer.enter();
    
    // Now "outer" is the current span
    
    {
        let inner = span!(Level::DEBUG, "inner");
        let _inner_enter = inner.enter();
        
        // Span stack: [outer, inner]
        // Current span: "inner"
        // Events here have both "outer" and "inner" as context
        
        tracing::info!("inside both spans");
        
        // _inner_enter drops, "inner" is popped from stack
    }
    
    // Span stack: [outer]
    // Current span: "outer"
    
    tracing::info!("back to outer span");
    
    // _outer_enter drops, "outer" is popped
}

Entered manages a thread-local span stack, pushing on entry and popping on exit.

The Thread-Local Context

use tracing::{span, Level};
 
fn thread_local_context() {
    // Each thread has its own span stack
    // Entered guards manipulate thread-local state
    
    let span = span!(Level::INFO, "thread_span");
    
    let _enter = span.enter();
    // This modifies the current thread's span stack
    
    std::thread::spawn(move || {
        // The spawned thread has its OWN span stack
        // It's not affected by the parent thread's Entered guard
        
        tracing::info!("in child thread");  // Not in "thread_span"
        
        let child_span = span!(Level::DEBUG, "child_span");
        let _child_enter = child_span.enter();
        
        // This thread's span stack is independent
    });
    
    // Back in original thread, we're still in "thread_span"
}

Span stacks are thread-local; Entered guards only affect the current thread.

Multiple Enter Calls

use tracing::{span, Level};
 
fn multiple_enter() {
    let span = span!(Level::INFO, "reusable");
    
    {
        let _enter1 = span.enter();
        // Span stack: [reusable]
    }
    // _enter1 dropped, span exited
    
    {
        let _enter2 = span.enter();
        // Span stack: [reusable] again
        // Same span can be entered multiple times
    }
    // _enter2 dropped, span exited again
    
    // Each enter/exit is tracked separately
    // The span itself remains valid across multiple enters
}

A single Span can be entered multiple times; each entry gets its own guard.

Entered Spans Cannot Be Modified

use tracing::{span, Level};
 
fn borrowed_immutability() {
    let mut span = span!(Level::INFO, "my_span");
    
    let _enter = span.enter();
    // _enter borrows span immutably for its lifetime
    
    // This prevents modifying the span while entered:
    // span = span!(Level::DEBUG, "new_span");  // Error: span is borrowed
    
    // The Entered guard holds:
    // Entered<'a> where 'a is tied to the Span's lifetime
    
    // This ensures the span isn't modified or dropped while entered
}

The lifetime tie prevents the span from being modified or dropped while entered.

Common Pitfall: Dropping the Guard Early

use tracing::{span, Level};
 
fn dropped_guard_pitfall() {
    let span = span!(Level::INFO, "important_operation");
    
    // WRONG: Dropping the guard immediately
    span.enter();  // Guard is returned but immediately dropped
    // Span is entered then immediately exited!
    
    tracing::info!("this event is NOT in the span!");
    // The guard was dropped, span was exited
    
    // CORRECT: Storing the guard
    let _enter = span.enter();  // Guard stored in variable
    tracing::info!("this event IS in the span");
    // _enter is held until end of scope
}

Storing the guard in a variable is essential; dropping it immediately exits the span.

Explicit Scope Control

use tracing::{span, Level};
 
fn explicit_scope() {
    // The Entered guard can be used to control exact scope:
    
    let outer = span!(Level::INFO, "outer");
    let inner = span!(Level::DEBUG, "inner");
    
    // Enter both spans, but control exactly when each exits
    let _outer = outer.enter();
    
    {
        let _inner = inner.enter();
        tracing::info!("in both spans");
        // _inner exits here
    }
    
    tracing::info!("only in outer span");
    // _outer exits at end of function
}

Blocks can be used to precisely control when spans exit.

Comparison with Manual enter/exit

use tracing::{span, Level};
 
fn comparison() {
    let span = span!(Level::INFO, "comparison");
    
    // Manual approach (NOT recommended):
    // span.enter();
    // // ... code ...
    // span.exit();  // May be skipped!
    
    // Issues with manual approach:
    // 1. Early returns might skip exit
    // 2. Panics skip exit
    // 3. Forgetting exit call
    // 4. Mismatched enter/exit pairs
    
    // RAII approach with Entered:
    let _enter = span.enter();
    // ... code ...
    
    // Benefits:
    // 1. Exit is automatic
    // 2. Panic-safe
    // 3. Cannot forget exit
    // 4. Always matched enter/exit
    
    // The Entered guard is the only safe way to enter spans
}

Entered is the only safe way to manage span lifecycle.

Working with Async Code

use tracing::{span, Level};
 
async fn async_spans() {
    // CAUTION: Entered guards and async require care
    
    let span = span!(Level::INFO, "async_operation");
    
    // WRONG: Guard across await point
    // let _enter = span.enter();
    // some_async_fn().await;  // Problem!
    // The guard might be held across .await
    
    // CORRECT: Use instrument() for async
    some_async_fn()
        .instrument(span)
        .await;
    
    // Or enter only in synchronous sections:
    let span = span!(Level::DEBUG, "sync_section");
    {
        let _enter = span.enter();
        // Only synchronous code here
        let result = compute_something();
    }
    // Guard dropped before any .await
    
    some_async_fn().await;
}
 
fn compute_something() -> i32 { 42 }
async fn some_async_fn() {}

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

Span Enter vs. Span Creation

use tracing::{span, Level, info_span};
 
fn creation_vs_entry() {
    // Creating a span does NOT enter it
    let span = info_span!("created_span");
    // Span exists but is not current
    
    tracing::info!("not in created_span");
    
    // Entering makes it current
    let _enter = span.enter();
    tracing::info!("now in created_span");
    
    // Creation: allocates the span, assigns ID
    // Entry: makes span current, pushes to stack
    
    // Spans can be created once and entered multiple times
    let reusable = span!(Level::INFO, "reusable");
    
    for i in 0..3 {
        let _enter = reusable.enter();
        tracing::info!("iteration {}", i);
    }
}

Creating a span and entering it are separate operations.

Under the Hood

use tracing::{span, Level};
 
fn under_the_hood() {
    let span = span!(Level::INFO, "my_span");
    
    // span.enter() roughly does:
    // 1. Get thread-local dispatcher
    // 2. Push span to the current span stack
    // 3. Record span entry event (if enabled)
    // 4. Return Entered guard
    
    let _enter = span.enter();
    
    // When _enter drops:
    // 1. Pop span from the current span stack
    // 2. Record span exit event (if enabled)
    // 3. Restore previous span as current
    
    // The span stack is thread-local
    // Each Entered guard manages exactly one entry on the stack
}

The guard maintains the invariant: one enter, one exit, always matched.

Synthesis

The Entered guard pattern:

use tracing::{span, Level};
 
fn complete_example() {
    let span = span!(Level::INFO, "example");
    
    // Enter returns a guard that borrows the span
    let _enter = span.enter();
    
    // The guard's presence ensures:
    // 1. The span is the current span
    // 2. Events are associated with this span
    // 3. Nested spans will have this as parent
    
    // When _enter drops:
    // - Drop::drop() is called
    // - span.exit() is invoked
    // - The span stack is popped
    // - Previous span becomes current
}

Key invariant: Entered guarantees that every enter() call is paired with exactly one exit() call, even in the presence of:

  • Early returns
  • ? error propagation
  • Panics
  • Complex control flow

Quick reference:

Aspect Behavior
Acquired by Span::enter()
Stores Immutable reference to Span
On drop Calls Span::exit()
Lifetime Borrows Span for duration
Thread-local Affects only current thread
Nesting Pushes/pops from span stack

Key insight: The Entered guard implements RAII (Resource Acquisition Is Initialization) to guarantee proper span lifecycle management. The pattern is simple but powerful: enter() returns a guard that pushes the span onto a thread-local stack, and the guard's Drop implementation pops it back off. This ensures that span exit always happens—regardless of how control leaves the scope. The guard's lifetime is tied to the span via a lifetime parameter (Entered<'a>), preventing the span from being modified or dropped while entered. This design prevents the class of bugs that would arise from manual enter()/exit() calls, where early returns or exceptions could leave the span stack in an inconsistent state. For async code, prefer .instrument(span) over holding an Entered guard across .await points, as the guard is synchronous and should not be held across async boundaries. The Entered guard is the foundational mechanism that makes tracing's span tracking reliable and ergonomic.