Loading pageā¦
Rust walkthroughs
Loading pageā¦
tracing::span::Entered guard ensure span context is properly exited when dropped?tracing::span::Entered is a RAII guard that exits its associated span when dropped, using Rust's deterministic drop order to guarantee span context is properly exited even in the presence of early returns, errors, or panics. When you call span.enter(), it returns an Entered guard that registers the span as active in the current thread's context. When the guard goes out of scopeāwhether through normal execution, an early return, or unwinding from a panicāthe Drop implementation automatically exits the span, removing it from the active context. This pattern ensures correct span nesting and prevents context leakage without requiring manual cleanup.
use tracing::{span, Level};
fn basic_usage() {
let span = span!(Level::INFO, "my_operation");
// enter() returns an Entered guard
let _enter = span.enter();
// Within this scope, the span is active
// All events and child spans will be recorded under this span
// When _enter goes out of scope, the span is exited automatically
}The _enter guard ensures the span is exited when the scope ends.
use tracing::{span, Level};
fn drop_implementation() {
let span = span!(Level::INFO, "operation");
{
let _enter = span.enter();
// Span is now active
tracing::info!("Inside span"); // Recorded under "operation"
// _enter is about to be dropped
}
// Span is no longer active
tracing::info!("Outside span"); // Not under "operation"
// The Drop implementation for Entered:
// 1. Pops this span from the current thread's span stack
// 2. Restores the previously active span (if any)
// 3. Dispatches the exit event to subscribers
}The Drop implementation handles all cleanup automatically.
use tracing::{span, Level};
fn raii_pattern() {
// RAII: Resource Acquisition Is Initialization
// The guard acquires the span context on creation
// The guard releases the span context on drop
let outer = span!(Level::INFO, "outer");
let _outer_guard = outer.enter();
{
let inner = span!(Level::INFO, "inner");
let _inner_guard = inner.enter();
// Stack: outer -> inner (inner is current)
tracing::info!("Deeply nested");
}
// _inner_guard dropped, stack: outer (outer is current)
tracing::info!("Back in outer");
}The RAII pattern ensures proper nesting through deterministic drop order.
use tracing::{span, Level};
fn early_return() -> Result<(), String> {
let span = span!(Level::INFO, "database_operation");
let _enter = span.enter();
// Even with early returns, the span is exited properly
let data = fetch_data()?;
if data.is_empty() {
// Early return - _enter is still dropped
return Err("no data".to_string());
}
process_data(data)?;
Ok(())
// _enter dropped here on success path too
}
fn fetch_data() -> Result<Vec<u8>, String> { Ok(vec
![]) }
fn process_data(_: Vec<u8>) -> Result<(), String> { Ok(()) }Early returns don't skip the cleanupā_enter is dropped in all cases.
use tracing::{span, Level};
fn error_propagation() -> Result<(), std::io::Error> {
let span = span!(Level::INFO, "file_operation");
let _enter = span.enter();
let file = std::fs::File::open("nonexistent.txt")?;
// If this fails, ? returns early
// But _enter is dropped first, exiting the span
// Code after successful open
Ok(())
}
fn test_error() {
// Even if the function returns early with an error,
// the span is properly exited
let _ = error_propagation();
// The span "file_operation" is exited before this line
}The ? operator doesn't bypass the guard's drop.
use tracing::{span, Level};
fn panic_safety() {
let span = span!(Level::INFO, "risky_operation");
let _enter = span.enter();
// Panics trigger unwinding, which drops all values
// _enter will be dropped during unwind
panic!("something went wrong");
// During unwinding:
// 1. _enter's Drop::drop is called
// 2. Span is exited properly
// 3. Unwinding continues
}
fn catch_panic() {
let result = std::panic::catch_unwind(|| {
panic_safety();
});
// The span was properly exited even though panic occurred
assert!(result.is_err());
}Even panics properly exit spans through unwinding.
use tracing::{span, Level};
fn nested_correctness() {
let outer = span!(Level::INFO, "outer");
let inner = span!(Level::INFO, "inner");
// Correct nesting with RAII
let _outer = outer.enter();
{
let _inner = inner.enter();
// Active: outer -> inner
}
// _inner dropped, active: outer
// Wrong nesting without guards:
// If you forgot to exit, context would leak
// But RAII prevents this mistake
}
fn incorrect_manual_management() {
// Without RAII (hypothetical, not how tracing works):
// outer.enter(); // Manual enter
// inner.enter(); // Manual enter
// // ... code ...
// inner.exit(); // Manual exit
// // If you forget exit, context leaks
// outer.exit(); // Manual exit
// RAII prevents this by making exit automatic
}RAII ensures nesting is always correct, even when code paths diverge.
use tracing::{span, Level};
fn guard_lifetime() {
let span = span!(Level::INFO, "operation");
// The guard must outlive the span reference
// This is enforced by borrow checker:
// enter(&self) -> Entered<'_>
let _enter = span.enter();
// _enter holds a reference to span
// Cannot drop span before _enter
// span lives until end of function
// _enter lives until its scope ends
}
fn guard_scope() {
let span = span!(Level::INFO, "scoped");
{
let _enter = span.enter();
tracing::info!("Inside scope");
// _enter dropped here
}
// Span is exited, but span value still exists
// Can enter again if needed
{
let _enter = span.enter();
tracing::info!("Inside another scope");
}
}The guard's lifetime is tied to the span's lifetime through borrowing.
use tracing::{span, Level, Span};
fn guard_type() {
let span: Span = span!(Level::INFO, "operation");
// enter() returns Entered<'_>
// Entered is a RAII guard
// The Entered type:
// pub struct Entered<'a> {
// span: &'a Span,
// // Internal state for context management
// }
let _enter: tracing::span::Entered<'_> = span.enter();
// Entered<'_> borrows span immutably
// This prevents:
// 1. Mutating span while entered
// 2. Dropping span while entered
}The Entered<'a> type borrows the span, ensuring the span outlives the guard.
use tracing::{span, Level};
fn context_stack() {
// Each thread has a span stack
// enter() pushes, drop pops
let span1 = span!(Level::INFO, "span1");
let span2 = span!(Level::INFO, "span2");
let span3 = span!(Level::INFO, "span3");
// Stack: (empty)
let _g1 = span1.enter();
// Stack: span1
{
let _g2 = span2.enter();
// Stack: span1 -> span2
{
let _g3 = span3.enter();
// Stack: span1 -> span2 -> span3
// Current span: span3
}
// _g3 dropped, popped span3
// Stack: span1 -> span2
// Current span: span2
}
// _g2 dropped, popped span2
// Stack: span1
// Current span: span1
// _g1 dropped when function ends
}The guard maintains a stack of active spans, ensuring correct parent-child relationships.
use tracing::{span, Level};
fn multiple_guards() {
let span = span!(Level::INFO, "operation");
// Each enter() creates a new guard
// Multiple guards can exist for same span
{
let _enter1 = span.enter();
let _enter2 = span.enter();
// Stack: operation -> operation
// Current: operation (second entry)
// This is valid but unusual
// Usually means same span is entered multiple times
}
// Both guards dropped, stack cleared
}The same span can be entered multiple times; each entry must be matched with an exit.
use tracing::{span, Level};
fn guard_in_loop() {
let span = span!(Level::INFO, "loop_operation");
for i in 0..3 {
let _enter = span.enter();
tracing::info!(iteration = i, "Processing");
// _enter dropped at end of each iteration
// Span exited and re-entered each iteration
}
}
fn guard_outside_loop() {
let span = span!(Level::INFO, "batch_operation");
let _enter = span.enter();
for i in 0..3 {
tracing::info!(iteration = i, "Processing");
}
// _enter dropped once at end
// Single span context for entire loop
}Guard placement affects span lifetimeāinside loops re-enters each iteration.
use tracing::{span, Level};
async fn async_example() {
let span = span!(Level::INFO, "async_operation");
// IMPORTANT: Don't hold Entered across await points
let _enter = span.enter();
// This is problematic:
// some_async_op().await; // Guard held across await!
// The guard is held across the await, but the task may
// move to a different thread. The span context would be
// incorrect.
// Better: use instrument()
}
async fn correct_async() {
let span = span!(Level::INFO, "async_operation");
// Use instrument() for async code
async {
tracing::info!("Inside async");
some_async_op().await;
}
.instrument(span)
.await;
// instrument() handles the span correctly across await points
// It enters the span on each poll, exits on each suspend
}
async fn some_async_op() {}Don't hold Entered guards across .await points; use .instrument() instead.
use tracing::{span, Level};
fn drop_details() {
let span = span!(Level::INFO, "operation");
// When enter() is called:
// 1. Span's internal reference count is incremented
// 2. Span is pushed onto thread-local context stack
// 3. Entered guard is created with reference to span
let _enter = span.enter();
// When Entered is dropped:
// impl<'a> Drop for Entered<'a> {
// fn drop(&mut self) {
// // Pop this span from the context stack
// dispatch::get_default(|dispatch| {
// dispatch.exit(self.span.id());
// });
// }
// }
// The exit operation:
// 1. Pops the span from the stack
// 2. Notifies subscribers of the exit event
// 3. Restores previous span as current
}The Drop implementation handles all the internal bookkeeping.
use tracing::{span, Level};
fn entered_vs_instrument() {
// Entered: for synchronous code
fn sync_operation() {
let span = span!(Level::INFO, "sync");
let _enter = span.enter();
// Do synchronous work
}
// instrument: for async code
async fn async_operation() {
let span = span!(Level::INFO, "async");
async {
// Do async work
}
.instrument(span)
.await;
}
// Key difference:
// - Entered: single entry/exit per scope
// - instrument: enters/exits on each poll of async
// For async, instrument handles the complexity of
// entering/exiting across suspend points
}Use enter() for sync code, .instrument() for async code.
use tracing::{span, Level};
fn check_entered() {
let span = span!(Level::INFO, "operation");
// Check if span is currently entered in this context
// (not a standard API, but conceptually)
let is_entered = span.is_recording(); // Different concept
// The Entered guard doesn't expose "is entered" directly
// But you can track it yourself if needed:
let span = span!(Level::INFO, "operation");
let mut entered = false;
{
let _enter = span.enter();
entered = true;
// ... work ...
}
entered = false; // Must set manually
}The guard doesn't expose entered state directly; tracking requires manual handling.
use tracing::{span, Level};
fn misuse() {
let span = span!(Level::INFO, "operation");
// DON'T do this: dropping guard immediately
span.enter(); // Guard dropped immediately!
tracing::info!("Not in span"); // Span was already exited
// The enter() returns a guard that must be held
// Dropping it immediately exits the span
// DON'T do this: holding guard too long
let span = span!(Level::INFO, "operation");
let _enter = span.enter();
// ... lots of code ...
// _enter held way too long, span context polluted
// DO this: tight scopes
let span = span!(Level::INFO, "operation");
{
let _enter = span.enter();
// Just the code that should be under this span
}
// Span exited, back to previous context
}Guards must be held for the correct durationāneither dropped immediately nor held too long.
use tracing::{span, Level};
fn process_request(id: u64) -> Result<String, String> {
// Create span for the entire request
let span = span!(Level::INFO, "process_request", id = id);
// Enter for the duration of processing
let _enter = span.enter();
// All operations here are under the span
tracing::info!("Starting request");
let data = fetch_data(id)?;
tracing::debug!("Fetched {} bytes", data.len());
let result = transform_data(data)?;
tracing::info!("Completed successfully");
Ok(result)
// _enter dropped, span exited
}
fn fetch_data(id: u64) -> Result<Vec<u8>, String> {
Ok(vec
![1, 2, 3])
}
fn transform_data(data: Vec<u8>) -> Result<String, String> {
Ok(format!("{:?}", data))
}The guard ensures the span context is correct for the entire operation, including on error.
How the Entered guard works:
use tracing::{span, Level};
// 1. span.enter() returns an Entered guard
// 2. The guard holds a reference to the span
// 3. On creation: span pushed to context stack
// 4. On drop: span popped from context stack
let span = span!(Level::INFO, "operation");
{
let _enter = span.enter();
// Push operation to context stack
// Current context: operation
// Any tracing events go to operation
} // _enter dropped
// Pop operation from context stack
// Previous context restoredWhy RAII matters for span context:
// RAII guarantees:
// 1. Cleanup on normal exit (scope end)
// 2. Cleanup on early return (return, ?)
// 3. Cleanup on panic (unwinding)
// 4. No forgotten cleanup
// Without RAII, you'd need:
span.enter();
try {
// work
} finally {
span.exit(); // Manual cleanup
}
// With RAII, cleanup is automatic and guaranteedKey invariants maintained:
// The Entered guard ensures:
// 1. Span is always exited (Drop guarantee)
// 2. Correct nesting (stack push/pop)
// 3. Previous context restored
// 4. Thread-local context consistency
// 5. Subscriber notifications for enter/exitAsync code warning:
// DON'T hold Entered across await
let _enter = span.enter();
some_async_op().await; // PROBLEM: guard held across suspend
// DO use instrument for async
async {
// work
}
.instrument(span)
.await;
// instrument() enters on poll, exits on suspend
// This keeps span context correct across thread movesThe fundamental insight: tracing::span::Entered is a classic RAII guard that leverages Rust's deterministic destructors to ensure span context is always cleaned up correctly. The guard's Drop implementation pops the span from the thread-local context stack, restoring the previous span as active. This pattern eliminates a whole class of bugs where context is leaked due to forgotten cleanup, early returns bypassing cleanup, or panics occurring before manual cleanup. For synchronous code, enter() and the Entered guard provide correct span management; for async code, .instrument() handles the additional complexity of suspend/resume across await points.