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.
