Skip to main content
Context propagation allows trace information to flow across service boundaries and through async operations, maintaining the parent-child relationships between spans.

Context Basics

The Context type stores the currently active span and other contextual information. The TraceContextExt trait provides methods for working with spans in a context.

TraceContextExt Trait

From opentelemetry/src/trace/context.rs:223-299:
pub trait TraceContextExt {
    fn current_with_span<T: Span + Send + Sync + 'static>(span: T) -> Self;
    fn with_span<T: Span + Send + Sync + 'static>(&self, span: T) -> Self;
    fn span(&self) -> SpanRef<'_>;
    fn has_active_span(&self) -> bool;
    fn with_remote_span_context(&self, span_context: SpanContext) -> Self;
}

Managing Active Spans

Creating Context with Spans

use opentelemetry::{Context, trace::{Tracer, TraceContextExt}};
use opentelemetry::global;

let tracer = global::tracer("my-component");
let span = tracer.start("operation");

// Create a new context containing this span
let cx = Context::current_with_span(span);

// Or add a span to an existing context
let parent_cx = Context::current();
let child_span = tracer.start("child");
let child_cx = parent_cx.with_span(child_span);

Accessing the Active Span

use opentelemetry::{Context, trace::TraceContextExt};
use opentelemetry::KeyValue;

let cx = Context::current();
let span = cx.span();

// Use the span reference
span.add_event("something happened", vec![
    KeyValue::new("detail", "important")
]);

Using get_active_span

The get_active_span function provides access to the current thread’s active span:
use opentelemetry::trace::{get_active_span, Tracer};
use opentelemetry::{global, KeyValue};

fn my_function() {
    // Access the active span without needing a context reference
    get_active_span(|span| {
        span.add_event(
            "my_function called",
            vec![KeyValue::new("timestamp", "2024-01-01")]
        );
    });
}

let tracer = global::tracer("my-component");
tracer.in_span("parent", |_cx| {
    // The span is active, my_function can access it
    my_function();
});

Propagating Context

Thread-Local Context Storage

OpenTelemetry uses thread-local storage to manage the active context:
use opentelemetry::Context;

// Get the current context
let cx = Context::current();

// Attach a context and get a guard
let new_cx = Context::new();
let _guard = new_cx.attach();

// Context is active until guard is dropped

Manual Context Attachment

For explicit control over context lifetime:
use opentelemetry::{Context, trace::{Tracer, TraceContextExt}};
use opentelemetry::global;

let tracer = global::tracer("my-component");
let span = tracer.start("operation");
let cx = Context::current_with_span(span);

// Attach and get guard
let _guard = cx.attach();

// Context is active for the current thread
// Guard detaches context when dropped

Using mark_span_as_active

From opentelemetry/src/trace/context.rs:327-361:
use opentelemetry::trace::{mark_span_as_active, Tracer, Span};
use opentelemetry::global;

let tracer = global::tracer("my-component");
let span = tracer.start("parent_span");

// Mark as active
let parent_active = mark_span_as_active(span);

{
    let child = tracer.start("child_span");
    let _child_active = mark_span_as_active(child);
    // Child is active here
}
// Parent is active again

drop(parent_active);
// No active span

Async Context Propagation

The Problem with Async

Standard context guards don’t work correctly with async code:
// ❌ INCORRECT - Don't do this!
async {
    let _guard = mark_span_as_active(span);
    // Guard stays active for entire future lifetime,
    // not just during polling!
};
From opentelemetry/src/trace/tracer.rs:79-95:
The context guard _g will not exit until the future generated by the async block is complete. Since futures can be entered and exited multiple times without them completing, the span remains active for as long as the future exists.

Using FutureExt

The correct way to propagate context in async code:
use opentelemetry::{Context, trace::FutureExt};

let cx = Context::current();

let my_future = async {
    // Async work here
};

// Attach context to the future
my_future.with_context(cx).await;

Complete Async Example

use opentelemetry::{
    global,
    trace::{FutureExt, Tracer, TraceContextExt},
    Context, KeyValue,
};

async fn fetch_data(id: u64) -> Result<String, Box<dyn std::error::Error>> {
    let tracer = global::tracer("my-service");

    tracer.in_span("fetch_data", |cx| async move {
        let span = cx.span();
        span.set_attribute(KeyValue::new("data.id", id as i64));

        // Propagate context to nested async operation
        let result = fetch_from_db(id)
            .with_context(cx.clone())
            .await?;

        span.add_event("data_fetched", vec![]);
        Ok(result)
    }).await
}

Remote Context Propagation

Extracting Context for Propagation

When making HTTP requests or RPC calls, inject context into headers:
use opentelemetry::{global, propagation::Injector};
use opentelemetry::trace::TraceContextExt;

// Custom injector for your HTTP library
struct HeaderInjector<'a>(&'a mut http::HeaderMap);

impl Injector for HeaderInjector<'_> {
    fn set(&mut self, key: &str, value: String) {
        if let Ok(name) = http::header::HeaderName::from_bytes(key.as_bytes()) {
            if let Ok(val) = http::header::HeaderValue::from_str(&value) {
                self.0.insert(name, val);
            }
        }
    }
}

// Inject context into request headers
let cx = Context::current();
let mut headers = http::HeaderMap::new();

global::get_text_map_propagator(|propagator| {
    propagator.inject_context(&cx, &mut HeaderInjector(&mut headers))
});

Real-world gRPC Example

From examples/tracing-grpc/src/client.rs:23-34:
use opentelemetry::propagation::Injector;
use tonic::metadata::MetadataMap;

struct MetadataMap<'a>(&'a mut tonic::metadata::MetadataMap);

impl Injector for MetadataMap<'_> {
    fn set(&mut self, key: &str, value: String) {
        if let Ok(key) = tonic::metadata::MetadataKey::from_bytes(key.as_bytes()) {
            if let Ok(val) = tonic::metadata::MetadataValue::try_from(&value) {
                self.0.insert(key, val);
            }
        }
    }
}

// Inject into gRPC request
let mut request = tonic::Request::new(HelloRequest {
    name: "Tonic".into(),
});

global::get_text_map_propagator(|propagator| {
    propagator.inject_context(&cx, &mut MetadataMap(request.metadata_mut()))
});

Extracting Context from Incoming Requests

use opentelemetry::{global, propagation::Extractor};

// Custom extractor for your HTTP library
struct HeaderExtractor<'a>(&'a http::HeaderMap);

impl Extractor for HeaderExtractor<'_> {
    fn get(&self, key: &str) -> Option<&str> {
        self.0.get(key).and_then(|v| v.to_str().ok())
    }

    fn keys(&self) -> Vec<&str> {
        self.0.keys().map(|k| k.as_str()).collect()
    }
}

// Extract parent context from incoming request
let parent_cx = global::get_text_map_propagator(|propagator| {
    propagator.extract(&HeaderExtractor(&headers))
});

// Create child span with remote parent
let span = tracer.start_with_context("handle_request", &parent_cx);

W3C Trace Context Propagator

Configure the global text map propagator:
use opentelemetry::global;
use opentelemetry_sdk::propagation::TraceContextPropagator;

// Set the W3C Trace Context propagator
global::set_text_map_propagator(TraceContextPropagator::new());

Remote Span Context

When you receive a span context from another service:
use opentelemetry::{Context, trace::{SpanContext, TraceContextExt}};

// Create a context with a remote span context
let remote_span_context: SpanContext = /* extracted from headers */;
let cx = Context::current().with_remote_span_context(remote_span_context);

// Create a child span
let span = tracer.start_with_context("local_operation", &cx);

Context in Span Processors

Do not rely on Context::current() in SpanProcessor::on_end. The context at cleanup time is unrelated to the span being ended.
From opentelemetry-sdk/src/trace/span_processor.rs:87-118:
// ❌ INCORRECT - Don't access current context in on_end
impl SpanProcessor for MyProcessor {
    fn on_end(&self, span: SpanData) {
        // Context::current() is NOT related to this span!
        let cx = Context::current();
    }
}

// ✅ CORRECT - Extract info in on_start and store as attributes
impl SpanProcessor for MyProcessor {
    fn on_start(&self, span: &mut Span, cx: &Context) {
        // Extract baggage and store as span attribute
        if let Some(value) = cx.baggage().get("my-key") {
            span.set_attribute(KeyValue::new("my-key", value.to_string()));
        }
    }

    fn on_end(&self, span: SpanData) {
        // Access the attribute stored in on_start
        let my_value = span.attributes.iter()
            .find(|kv| kv.key.as_str() == "my-key");
    }
}

Best Practices

Never use context guards directly in async blocks. Use .with_context() to attach context to futures.
When making network calls, inject the current context into request metadata so downstream services can continue the trace.
For server-side code, extract the parent span context from incoming request headers to maintain trace continuity.
Configure a single text map propagator (like W3C Trace Context) globally to ensure consistent propagation across your application.
In span processors, extract needed information during on_start and store it as span attributes.

Complete Example

Here’s a complete HTTP client/server example:
use opentelemetry::{
    global,
    propagation::{Injector, Extractor},
    trace::{Tracer, TraceContextExt, SpanKind},
    Context, KeyValue,
};

// Client side - inject context
async fn make_request() -> Result<(), Box<dyn std::error::Error>> {
    let tracer = global::tracer("http-client");

    tracer.in_span("http_request", |cx| async move {
        let mut request = http::Request::builder()
            .uri("https://api.example.com/data")
            .body(())?;

        // Inject trace context into headers
        global::get_text_map_propagator(|propagator| {
            propagator.inject_context(
                &cx,
                &mut HeaderInjector(request.headers_mut())
            )
        });

        // Make the request...
        Ok(())
    }).await
}

// Server side - extract context
async fn handle_request(req: http::Request<()>) -> http::Response<()> {
    // Extract parent context from headers
    let parent_cx = global::get_text_map_propagator(|propagator| {
        propagator.extract(&HeaderExtractor(req.headers()))
    });

    let tracer = global::tracer("http-server");

    // Create span with remote parent
    let span = tracer
        .span_builder("handle_request")
        .with_kind(SpanKind::Server)
        .start_with_context(&tracer, &parent_cx);

    let cx = parent_cx.with_span(span);

    // Process request with context
    // ...

    http::Response::new(())
}

Next Steps

Sampling

Learn how to control which traces are recorded

Span Processors

Configure how spans are processed and exported

Build docs developers (and LLMs) love