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.
