How does tracing::instrument::Instrument::instrument attach span context to futures?

Instrument::instrument wraps a future with a span that is automatically entered whenever the future is polled, ensuring that all events and spans created within the future's execution are associated with that span's context. This provides seamless integration between async code and the tracing ecosystem, allowing spans to persist across .await points without manual management.

The Instrument Trait

use tracing::instrument::Instrument;
use tracing::Span;
 
// The Instrument trait provides the instrument method
pub trait Instrument: Sized {
    fn instrument(self, span: Span) -> Instrumented<Self>;
    fn in_current_span(self) -> Instrumented<Self>
    where
        Self: Future,
    {
        self.instrument(Span::current())
    }
}

The trait wraps futures with span context, implemented for all types that can become futures.

Basic Usage Pattern

use tracing::{info, instrument, span, Level};
use tracing::instrument::Instrument;
 
async fn my_async_function() {
    // Create a span
    let span = span!(Level::INFO, "my_operation");
    
    // Instrument the future with the span
    my_async_work().instrument(span).await;
}
 
async fn my_async_work() {
    // Events here will be associated with "my_operation" span
    info!("working hard");
}

The instrument method attaches the span to the future, entering it on every poll.

How Instrument Works Internally

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tracing::{Span, Instrument};
 
// Instrumented is a future wrapper
pub struct Instrumented<T> {
    inner: T,
    span: Span,
}
 
impl<T: Future> Future for Instrumented<T> {
    type Output = T::Output;
    
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // Enter the span before polling
        let _enter = self.span.enter();
        
        // Poll the inner future
        self.inner.poll(cx)
        
        // Span is exited when _enter is dropped
    }
}

Every time the future is polled, the span is entered before the poll and exited after.

Span Context Propagation Across Await Points

use tracing::{info, debug, span, Level};
use tracing::instrument::Instrument;
 
async fn demonstrate_context_propagation() {
    let span = span!(Level::INFO, "database_operation");
    
    let future = async {
        info!("starting query");  // Inside span context
        
        let result = query_database().await;  // Span persists across await
        
        info!("query complete");  // Still inside span context
        result
    }.instrument(span);
    
    future.await;
}
 
async fn query_database() -> String {
    debug!("executing SQL");  // This event is within "database_operation" span
    "data".to_string()
}

The span context automatically propagates through .await points.

The in_current_span Method

use tracing::{span, Level};
use tracing::instrument::Instrument;
 
async fn in_current_span_example() {
    // In a parent span
    let parent = span!(Level::INFO, "parent");
    let _enter = parent.enter();
    
    // in_current_span attaches the currently-active span
    let future = async {
        // This future runs within "parent" span
    }.in_current_span();
    
    future.await;
}

in_current_span captures the current span at creation time, not at execution time.

Comparison with Manual Span Management

use tracing::{info, span, Level};
 
async fn manual_span_management() {
    let span = span!(Level::INFO, "manual");
    
    // Manual: Enter span, but it exits after the statement
    let _enter = span.enter();
    info!("this is in the span");
    // _enter dropped here if not careful
    
    // Problem: Span not active across await
    some_async_work().await;  // Span context lost!
}
 
async fn instrumented_approach() {
    let span = span!(Level::INFO, "instrumented");
    
    // Instrumented: Span active for entire future execution
    async {
        info!("in span");
        some_async_work().await;  // Span still active
        info!("still in span");
    }.instrument(span).await;
}
 
async fn some_async_work() {}

Manual span management doesn't work correctly with async; instrument handles it properly.

The #[instrument] Attribute Macro

use tracing::instrument;
 
// The #[instrument] attribute uses Instrument internally
#[instrument]
async fn my_function(user_id: u64) {
    // Span automatically created with function name
    // Automatically entered for entire function body
    let user = fetch_user(user_id).await;
    process_user(user).await;
}
 
// Equivalent manual instrumentation:
async fn my_function_manual(user_id: u64) {
    let span = tracing::span!(Level::INFO, "my_function", user_id);
    async {
        let user = fetch_user(user_id).await;
        process_user(user).await;
    }.instrument(span).await;
}
 
async fn fetch_user(id: u64) -> String {
    format!("user_{}", id)
}
 
async fn process_user(user: String) {}

The #[instrument] macro automates what instrument does manually.

Span Fields in Instrument

use tracing::{span, Level};
use tracing::instrument::Instrument;
 
async fn with_fields() {
    let span = span!(
        Level::INFO,
        "http_request",
        method = "GET",
        path = "/users",
    );
    
    async {
        // Fields are available throughout the span
        handle_request().await;
    }.instrument(span).await;
}
 
async fn handle_request() {}

Span fields are captured when the span is created and persist for all events within.

Nested Instrumentation

use tracing::{info, span, Level};
use tracing::instrument::Instrument;
 
async fn nested_spans() {
    let outer = span!(Level::INFO, "outer");
    
    async {
        info!("in outer span");
        
        let inner = span!(Level::INFO, "inner");
        async {
            info!("in both spans");
        }.instrument(inner).await;
        
        info!("back to outer span only");
    }.instrument(outer).await;
}

Nested instrumentation creates a span stack; inner spans override while active.

Thread Safety and Context Propagation

use tracing::{info, span, Level};
use tracing::instrument::Instrument;
use tokio::task::spawn;
 
async fn thread_safety() {
    let span = span!(Level::INFO, "main_task");
    
    // Each spawned task gets its own instrumented future
    let handle1 = spawn(
        async {
            info!("task 1");  // In "main_task" span copy
        }.instrument(span.clone())
    );
    
    let handle2 = spawn(
        async {
            info!("task 2");  // In "main_task" span copy
        }.instrument(span.clone())
    );
    
    let _ = tokio::join!(handle1, handle2);
}

Spans can be cloned and shared across tasks; each task maintains its own context.

Span Follows Execution, Not Thread

use tracing::{info, span, Level};
use tracing::instrument::Instrument;
 
async fn execution_follows() {
    let span = span!(Level::INFO, "follows_execution");
    
    let future = async {
        info!("before yield");
        
        // Yield point - execution may move to another thread
        tokio::task::yield_now().await;
        
        // Span context is restored when execution resumes
        info!("after yield");  // Still in span
    }.instrument(span);
    
    future.await;
}

The span context follows the logical execution, regardless of which thread polls the future.

Performance Considerations

use tracing::{span, Level};
use tracing::instrument::Instrument;
 
async fn performance_example() {
    // Creating spans has minimal overhead
    let span = span!(Level::DEBUG, "expensive_operation");
    
    // Instrumenting a future is cheap (just wrapping)
    let future = expensive_work().instrument(span);
    
    // Overhead is on every poll, but it's very small
    // The enter/exit of spans is optimized for performance
    
    future.await;
}
 
async fn expensive_work() {
    // Do expensive work here
}

Span entry/exit is lightweight; the overhead of instrument is minimal.

Combining with Other Tracing Features

use tracing::{info, debug, span, Level, event};
use tracing::instrument::Instrument;
 
async fn combined_features() {
    let span = span!(Level::INFO, "complex_operation");
    
    async {
        // Regular events use current span context
        info!("starting operation");
        
        // Explicit events can reference span
        event!(Level::DEBUG, "debug details");
        
        // Child spans automatically nest
        let child = span!(Level::INFO, "child_operation");
        async {
            debug!("in child span");
        }.instrument(child).await;
        
        info!("operation complete");
    }.instrument(span).await;
}

instrument integrates with all tracing features; nested spans create hierarchies.

Error Handling with Instrument

use tracing::{span, Level};
use tracing::instrument::Instrument;
 
async fn error_handling() -> Result<(), Box<dyn std::error::Error>> {
    let span = span!(Level::INFO, "fallible_operation");
    
    let result = async {
        // Span active during entire operation, including error paths
        do_something().await?;
        do_another_thing().await?;
        Ok(())
    }.instrument(span).await;
    
    // Span is exited before this point
    result
}
 
async fn do_something() -> Result<(), Box<dyn std::error::Error>> { Ok(()) }
async fn do_another_thing() -> Result<(), Box<dyn std::error::Error>> { Ok(()) }

Spans remain active throughout the future, including through error paths.

WithSubscriber and Instrument Together

use tracing::{span, Level};
use tracing::instrument::Instrument;
use tracing_subscriber::Registry;
use tracing_subscriber::layer::SubscriberExt;
 
async fn with_subscriber() {
    // Create a subscriber
    let subscriber = Registry::default()
        .with(tracing_subscriber::fmt::layer());
    
    // Set as default
    tracing::subscriber::set_global_default(subscriber).unwrap();
    
    // Instrument works with any subscriber
    let span = span!(Level::INFO, "operation");
    
    async {
        // Events go to the global subscriber
    }.instrument(span).await;
}

instrument works with any configured subscriber; it uses the global subscriber.

Practical Example: HTTP Request Tracing

use tracing::{info, debug, span, Level};
use tracing::instrument::Instrument;
 
struct HttpRequest {
    method: String,
    path: String,
}
 
async fn handle_http_request(request: HttpRequest) -> String {
    let span = span!(
        Level::INFO,
        "http_request",
        method = %request.method,
        path = %request.path,
    );
    
    async {
        info!("received request");
        
        let response = process_request(&request).await;
        
        info!("sending response");
        response
    }.instrument(span).await
}
 
async fn process_request(request: &HttpRequest) -> String {
    debug!("processing");
    format!("Response to {}", request.path)
}
 
async fn http_example() {
    let request = HttpRequest {
        method: "GET".to_string(),
        path: "/users/123".to_string(),
    };
    
    let response = handle_http_request(request).await;
}

Real-world usage typically wraps entire request handlers with instrumented spans.

The WithInstrument Trait Variation

use tracing::{span, Level};
use tracing::instrument::Instrument;
 
// Some libraries provide with_instrument or similar methods
// that internally use Instrument
 
async fn library_pattern() {
    // Instead of:
    let span = span!(Level::INFO, "operation");
    let future = async {
        // work
    }.instrument(span);
    
    // Some services provide:
    // service.call().instrument(span)
    // which is the same pattern
}

The instrument pattern is commonly used in async Rust for service instrumentation.

Avoiding Common Mistakes

use tracing::{info, span, Level};
use tracing::instrument::Instrument;
 
async fn common_mistakes() {
    // MISTAKE 1: Not awaiting the instrumented future
    let span = span!(Level::INFO, "mistake");
    let _future = async {
        info!("never runs");
    }.instrument(span);  // Just creates Instrumented, doesn't run
    // Need to await!
    
    // MISTAKE 2: Instrumenting at wrong level
    let span = span!(Level::INFO, "wrong_level");
    let result = some_sync_function();  // Sync, not async
    // Can't instrument sync functions with Instrument trait
    // (but can use span.enter() for sync code)
    
    // MISTAKE 3: Creating span inside async block
    async {
        let span = span!(Level::INFO, "inside");  // Created on every poll!
        // This is inefficient
    }.await;
    
    // CORRECT: Create span outside, instrument outside
    let span = span!(Level::INFO, "outside");
    async {
        // Use span context here
    }.instrument(span).await;
}
 
async fn some_sync_function() -> i32 { 42 }

Common mistakes include forgetting to await, instrumenting sync code, and creating spans in hot paths.

Comparison Table

fn comparison() {
    // ┌─────────────────────────────────────────────────────────────────────────┐
    // │ Approach            │ Span Scope        │ Crosses Await    │ Correctness │
    // ├─────────────────────────────────────────────────────────────────────────┤
    // │ span.enter()        │ Statement/block   │ No               │ Wrong async│
    // │ instrument()        │ Entire future     │ Yes              │ Correct    │
    // │ #[instrument]       │ Entire function   │ Yes              │ Correct    │
    // │ in_current_span()   │ Entire future     │ Yes              │ Correct    │
    // └─────────────────────────────────────────────────────────────────────────┘
}

Complete Example

use tracing::{info, debug, error, span, Level};
use tracing::instrument::Instrument;
use tracing_subscriber;
 
async fn complete_example() {
    // Initialize subscriber (typically done in main)
    tracing_subscriber::fmt::init();
    
    // Create parent span with fields
    let parent_span = span!(
        Level::INFO,
        "request_handler",
        request_id = %uuid::Uuid::new_v4(),
    );
    
    // Instrument a complex async operation
    let result = async {
        info!("starting request processing");
        
        // Nested spans
        let db_span = span!(Level::DEBUG, "database_query");
        let user = async {
            debug!("fetching user from database");
            fetch_user_from_db().await
        }.instrument(db_span).await;
        
        // More nested spans
        let process_span = span!(Level::DEBUG, "process_user");
        let processed = async {
            debug!("processing user data");
            process_user(user).await
        }.instrument(process_span).await;
        
        info!("request complete");
        processed
    }.instrument(parent_span).await;
    
    debug!("after instrumented block");  // Outside span
}
 
async fn fetch_user_from_db() -> String { "user_data".to_string() }
async fn process_user(user: String) -> String { format!("processed_{}", user) }
 
// Simplified uuid module for example
mod uuid {
    use std::fmt;
    pub struct Uuid;
    impl Uuid {
        pub fn new_v4() -> Self { Uuid }
    }
    impl fmt::Display for Uuid {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            write!(f, "12345678-1234-1234-1234-123456789abc")
        }
    }
}

Summary

fn summary() {
    // ┌─────────────────────────────────────────────────────────────────────────┐
    // │ Concept             │ Description                                 │
    // ├─────────────────────────────────────────────────────────────────────────┤
    // │ Instrument::instrument │ Wraps future with span, enters on poll    │
    // │ Span propagation    │ Context persists across await points      │
    // │ Enter on poll        │ Span entered every time future is polled  │
    // │ Exit after poll      │ Span exited after poll completes          │
    // │ #[instrument]        │ Macro that automates instrument pattern    │
    // │ in_current_span      │ Captures current span at creation time     │
    // │ Thread-safe          │ Context follows execution, not thread     │
    // └─────────────────────────────────────────────────────────────────────────┘
    
    // Key points:
    // 1. instrument() wraps a future with a span that's entered on every poll
    // 2. Span context automatically propagates across .await points
    // 3. Manual span.enter() doesn't work correctly with async
    // 4. #[instrument] macro provides convenient function-level instrumentation
    // 5. instrument() is lightweight and safe for hot paths
    // 6. Nested spans create a context hierarchy
    // 7. Spans can be cloned for use across spawned tasks
    // 8. The span is entered before poll and exited after poll
    // 9. in_current_span() captures the active span at creation time
    // 10. Use instrument for async code, enter() for sync code
}

Key insight: Instrument::instrument solves the fundamental challenge of maintaining tracing context across async boundaries. In synchronous code, span.enter() works because execution is linear and the span is active for the duration of the block. In async code, futures are polled multiple times, potentially on different threads, with suspensions at .await points. A simple enter() would exit when the future suspends, losing context. instrument wraps the future in an Instrumented type that enters the span before each poll and exits after, ensuring the span context is active whenever the future makes progress. This creates a seamless experience where all events and spans created within an instrumented future are properly associated with the parent span, regardless of how many times the future is polled or which thread executes it. The #[instrument] attribute macro provides syntactic sugar that automatically applies this pattern to async functions, making it the idiomatic way to add tracing to async Rust code.