Understand how to propagate trace context across service boundaries and async operations in OpenTelemetry Rust
Context propagation allows trace information to flow across service boundaries and through async operations, maintaining the parent-child relationships between spans.
The Context type stores the currently active span and other contextual information. The TraceContextExt trait provides methods for working with spans in a context.
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 spanlet cx = Context::current_with_span(span);// Or add a span to an existing contextlet parent_cx = Context::current();let child_span = tracer.start("child");let child_cx = parent_cx.with_span(child_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();});
OpenTelemetry uses thread-local storage to manage the active context:
use opentelemetry::Context;// Get the current contextlet cx = Context::current();// Attach a context and get a guardlet new_cx = Context::new();let _guard = new_cx.attach();// Context is active until guard is dropped
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 guardlet _guard = cx.attach();// Context is active for the current thread// Guard detaches context when dropped
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 activelet 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 againdrop(parent_active);// No active span
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.
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 futuremy_future.with_context(cx).await;
use opentelemetry::global;use opentelemetry_sdk::propagation::TraceContextPropagator;// Set the W3C Trace Context propagatorglobal::set_text_map_propagator(TraceContextPropagator::new());
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_endimpl 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 attributesimpl 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"); }}