Skip to main content

Overview

Context propagation is fundamental to distributed tracing. It allows OpenTelemetry to:
  1. Link related operations - Connect spans within a single process
  2. Trace across services - Maintain trace continuity as requests flow between services
  3. Share metadata - Propagate baggage and other contextual information
OpenTelemetry provides two types of context propagation:
  • In-process propagation - Using the Context API
  • Cross-service propagation - Using propagators to inject/extract context from message headers

In-Process Context Propagation

The Context type is an immutable, execution-scoped collection of values that flows through your application.

Creating and Using Context

use opentelemetry::Context;

#[derive(Debug, PartialEq)]
struct UserId(u64);

#[derive(Debug, PartialEq)]
struct RequestId(String);

// Create a context with values
let ctx = Context::new()
    .with_value(UserId(123))
    .with_value(RequestId("req-abc".to_string()));

// Retrieve values
assert_eq!(ctx.get::<UserId>(), Some(&UserId(123)));
assert_eq!(ctx.get::<RequestId>(), Some(&RequestId("req-abc".to_string())));
Contexts are immutable. Operations like with_value() return a new context containing both the original and new values.

Attaching Context to the Current Thread

Use attach() to make a context active for the current thread:
use opentelemetry::Context;

#[derive(Debug, PartialEq)]
struct UserId(u64);

let ctx = Context::new().with_value(UserId(123));

// Attach the context
let _guard = ctx.attach();

// Now it's accessible via Context::current()
assert_eq!(Context::current().get::<UserId>(), Some(&UserId(123)));

// The guard automatically restores the previous context when dropped
You must assign the guard to a variable (not _) or Rust will drop it immediately, restoring the previous context.
// ❌ Wrong - guard is immediately dropped
Context::new().with_value(UserId(123)).attach();

// ✅ Correct - guard lives for the scope
let _guard = Context::new().with_value(UserId(123)).attach();

Nested Contexts

Contexts can be nested, creating a stack of active contexts:
use opentelemetry::Context;

#[derive(Debug, PartialEq)]
struct ValueA(&'static str);

#[derive(Debug, PartialEq)]
struct ValueB(u64);

let _outer = Context::new().with_value(ValueA("a")).attach();

// Only ValueA is set
assert_eq!(Context::current().get::<ValueA>(), Some(&ValueA("a")));
assert_eq!(Context::current().get::<ValueB>(), None);

{
    let _inner = Context::current_with_value(ValueB(42)).attach();
    
    // Both values are accessible
    assert_eq!(Context::current().get::<ValueA>(), Some(&ValueA("a")));
    assert_eq!(Context::current().get::<ValueB>(), Some(&ValueB(42)));
}

// Inner guard dropped, only ValueA remains
assert_eq!(Context::current().get::<ValueA>(), Some(&ValueA("a")));
assert_eq!(Context::current().get::<ValueB>(), None);

Context with Spans

Spans are automatically stored in the context:
use opentelemetry::{global, trace::{Tracer, TraceContextExt}, Context};

let tracer = global::tracer("my_service");
let span = tracer.start("operation");

// Create context with the span
let ctx = Context::current_with_span(span);

// Access the span from context
ctx.span().set_attribute(KeyValue::new("key", "value"));

Async Context Propagation

For async code, use FutureExt to propagate context across .await points:
use opentelemetry::{Context, trace::FutureExt};

async fn fetch_data() -> String {
    // This operation can access the context
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;
    "data".to_string()
}

async fn process_request() {
    let ctx = Context::new().with_value(UserId(123));
    
    // Propagate context to async operation
    let result = fetch_data().with_context(ctx.clone()).await;
    
    // Context is available throughout the async chain
}

Cross-Service Propagation

When a request crosses service boundaries (e.g., HTTP, gRPC), context must be serialized into message headers and deserialized on the receiving side.

Propagators

Propagators handle serialization and deserialization of context. OpenTelemetry provides several standard propagators:
  • TraceContext - W3C Trace Context standard (recommended)
  • Baggage - W3C Baggage standard
  • Jaeger - Jaeger-specific format
  • B3 - Zipkin B3 format

Setting Up Propagation

use opentelemetry::global;
use opentelemetry_sdk::propagation::TraceContextPropagator;

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

Injecting Context (Client Side)

When making an outbound request, inject the current context into headers:
use opentelemetry::{global, Context, trace::{TraceContextExt, Tracer}};
use opentelemetry_http::HeaderInjector;
use hyper::Request;

let tracer = global::tracer("example/client");

// Create a span for the outbound request
let span = tracer
    .span_builder("http_request")
    .with_kind(opentelemetry::trace::SpanKind::Client)
    .start(&tracer);

let cx = Context::current_with_span(span);

// Create HTTP request
let mut req = Request::builder().uri("https://api.example.com/data");

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

let request = req.body("request body").unwrap();
// Send the request...
This adds headers like:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: vendor1=value1,vendor2=value2

Extracting Context (Server Side)

When receiving a request, extract the context from headers:
use opentelemetry::{global, trace::{Tracer, SpanKind}};
use opentelemetry_http::HeaderExtractor;
use hyper::Request;

fn handle_request(req: Request<Body>) {
    // Extract context from incoming headers
    let parent_cx = global::get_text_map_propagator(|propagator| {
        propagator.extract(&HeaderExtractor(req.headers()))
    });
    
    let tracer = global::tracer("example/server");
    
    // Create a span that continues the trace
    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 the request with the extracted context...
}

Complete HTTP Example

Here’s a complete example of context propagation between HTTP client and server: Client:
use opentelemetry::{global, trace::{SpanKind, TraceContextExt, Tracer}, Context};
use opentelemetry_http::HeaderInjector;
use opentelemetry_sdk::propagation::TraceContextPropagator;

// Configure propagator
global::set_text_map_propagator(TraceContextPropagator::new());

async fn send_request(url: &str, body: &str) -> Result<(), Box<dyn std::error::Error>> {
    let tracer = global::tracer("example/client");
    
    // Create client span
    let span = tracer
        .span_builder("http_request")
        .with_kind(SpanKind::Client)
        .start(&tracer);
    
    let cx = Context::current_with_span(span);
    
    // Build request and inject context
    let mut req = hyper::Request::builder().uri(url);
    
    global::get_text_map_propagator(|propagator| {
        propagator.inject_context(&cx, &mut HeaderInjector(req.headers_mut().unwrap()));
    });
    
    let request = req.body(body.to_string())?;
    
    // Send request...
    
    Ok(())
}
Server:
use opentelemetry::{global, trace::{FutureExt, Span, SpanKind, TraceContextExt, Tracer}, Context};
use opentelemetry_http::HeaderExtractor;
use opentelemetry_sdk::propagation::TraceContextPropagator;
use hyper::{Request, Response, Body};

// Configure propagator
global::set_text_map_propagator(TraceContextPropagator::new());

async fn handle_request(req: Request<Body>) -> Response<Body> {
    // Extract context from headers
    let parent_cx = global::get_text_map_propagator(|propagator| {
        propagator.extract(&HeaderExtractor(req.headers()))
    });
    
    let tracer = global::tracer("example/server");
    
    // Create server span with extracted context as 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 with context
    async {
        // Your request handling logic
        Response::new(Body::from("Response"))
    }
    .with_context(cx)
    .await
}

Baggage

Baggage allows you to propagate arbitrary key-value pairs across service boundaries. Unlike span attributes, baggage is propagated to all downstream services.

Using Baggage

use opentelemetry::{baggage::BaggageExt, Context, KeyValue};

// Add baggage to context
let cx = Context::current()
    .with_baggage(vec![
        KeyValue::new("user.id", "123"),
        KeyValue::new("tenant.id", "acme-corp"),
    ]);

let _guard = cx.attach();

// Access baggage
let current_cx = Context::current();
for (key, (value, _metadata)) in current_cx.baggage() {
    println!("{}: {}", key, value);
}

Propagating Baggage Across Services

To propagate baggage, use a composite propagator:
use opentelemetry::{global, propagation::TextMapCompositePropagator};
use opentelemetry_sdk::propagation::{BaggagePropagator, TraceContextPropagator};

let composite = TextMapCompositePropagator::new(vec![
    Box::new(TraceContextPropagator::new()),
    Box::new(BaggagePropagator::new()),
]);

global::set_text_map_propagator(composite);
Now both trace context and baggage will be propagated via HTTP headers:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
baggage: user.id=123,tenant.id=acme-corp

Baggage in Spans and Logs

Baggage is not automatically added to spans or logs. You need custom processors:
use opentelemetry::{baggage::BaggageExt, Context, KeyValue};
use opentelemetry_sdk::trace::SpanProcessor;

#[derive(Debug)]
struct EnrichWithBaggageSpanProcessor;

impl SpanProcessor for EnrichWithBaggageSpanProcessor {
    fn on_start(&self, span: &mut opentelemetry_sdk::trace::Span, cx: &Context) {
        // Add all baggage items as span attributes
        for (key, (value, _metadata)) in cx.baggage().iter() {
            span.set_attribute(KeyValue::new(key.clone(), value.clone()));
        }
    }
    
    fn on_end(&self, _span: opentelemetry_sdk::trace::SpanData) {}
    
    fn force_flush(&self) -> opentelemetry_sdk::error::OTelSdkResult {
        Ok(())
    }
    
    fn shutdown_with_timeout(&self, _timeout: std::time::Duration) -> opentelemetry_sdk::error::OTelSdkResult {
        Ok(())
    }
}

// Register the processor
use opentelemetry_sdk::trace::SdkTracerProvider;

let provider = SdkTracerProvider::builder()
    .with_span_processor(EnrichWithBaggageSpanProcessor)
    .build();
Use baggage sparingly: Baggage is propagated to all downstream services and can add significant overhead. Only include essential key-value pairs.

Custom Propagators

You can implement custom propagators for proprietary formats:
use opentelemetry::propagation::{TextMapPropagator, Extractor, Injector};
use opentelemetry::Context;

struct CustomPropagator;

impl TextMapPropagator for CustomPropagator {
    fn inject_context(&self, cx: &Context, injector: &mut dyn Injector) {
        // Serialize context into custom headers
        if let Some(span) = cx.span().span_context() {
            injector.set("x-custom-trace-id", span.trace_id().to_string());
            injector.set("x-custom-span-id", span.span_id().to_string());
        }
    }
    
    fn extract_with_context(&self, cx: &Context, extractor: &dyn Extractor) -> Context {
        // Deserialize context from custom headers
        // Implementation details...
        cx.clone()
    }
    
    fn fields(&self) -> Vec<String> {
        vec![
            "x-custom-trace-id".to_string(),
            "x-custom-span-id".to_string(),
        ]
    }
}

Telemetry Suppression

OpenTelemetry components can suppress telemetry generation to prevent infinite loops:
use opentelemetry::Context;

// Enter a suppressed scope (used internally by exporters)
let _guard = Context::enter_telemetry_suppressed_scope();

// Check if telemetry is suppressed
if Context::is_current_telemetry_suppressed() {
    // Skip telemetry generation
    return;
}
This is primarily used by OpenTelemetry SDK components (exporters, processors) to prevent generating telemetry about telemetry operations.

Best Practices

Use W3C Trace Context: TraceContextPropagator is the recommended standard for interoperability.
Always inject and extract: Client-side code should inject context, server-side code should extract it.
Propagate context in async code: Use with_context() to ensure context flows across .await points.
Minimize baggage: Only propagate essential information as baggage—it’s sent with every request.
Don’t forget the guard variable: let _guard = ctx.attach() not just ctx.attach().

Next Steps

Signals

Learn about traces, metrics, and logs

Exporters

Configure where to send your telemetry data

Build docs developers (and LLMs) love