OpenTelemetry Trace Context Propagation [Rust]
This guide covers Rust-specific implementation of context propagation. For a comprehensive overview of context propagation concepts, W3C TraceContext, propagators, and troubleshooting, see the OpenTelemetry Context Propagation guide.
Automatic propagation
OpenTelemetry Rust handles traceparent headers automatically when using specialized instrumentation libraries. Base HTTP client libraries (reqwest, hyper) and server libraries (axum, actix-web) require additional instrumentation crates for automatic header injection and extraction.
Auto-instrumentation
[dependencies]
opentelemetry = "0.24"
opentelemetry_sdk = { version = "0.24", features = ["rt-tokio"] }
opentelemetry-otlp = "0.17"
tracing = "0.1"
tracing-opentelemetry = "0.25"
tracing-subscriber = "0.3"
use opentelemetry::{global, KeyValue};
use opentelemetry_sdk::{trace, Resource};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
// Initialize OpenTelemetry
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint("https://api.uptrace.dev:4317")
)
.install_batch(opentelemetry_sdk::runtime::Tokio)?;
// Create tracing layer with automatic propagation
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(telemetry)
.with(tracing_subscriber::EnvFilter::from_default_env())
.init();
// HTTP clients and servers with instrumentation crates
// automatically handle propagation when using tracing
run_app().await?;
global::shutdown_tracer_provider();
Ok(())
}
Auto-instrumentation packages for HTTP clients (with tracing-reqwest, tracing-hyper) and web frameworks (with tower middleware) automatically inject W3C tracecontext headers to outgoing HTTP requests and extract them from incoming requests.
Manual propagation
When automatic instrumentation is not available, you can manually handle traceparent headers using OpenTelemetry's propagation API.
Extracting context
use opentelemetry::{global, propagation::Extractor, Context};
use std::collections::HashMap;
fn handle_incoming_request(request_headers: &HashMap<String, String>) -> Result<(), Box<dyn std::error::Error>> {
// Extract context from incoming request headers
let parent_context = global::get_text_map_propagator(|propagator| {
propagator.extract(&HeaderMapCarrier::new(request_headers))
});
// Create a new span with the extracted parent context
let tracer = global::tracer("my-rust-service");
let span = tracer
.span_builder("handle-request")
.with_kind(opentelemetry::trace::SpanKind::Server)
.start_with_context(&tracer, &parent_context);
let _guard = span.activate();
// Your business logic here
process_request(request_headers)?;
Ok(())
}
// Header carrier implementation
struct HeaderMapCarrier<'a> {
headers: &'a HashMap<String, String>,
}
impl<'a> HeaderMapCarrier<'a> {
fn new(headers: &'a HashMap<String, String>) -> Self {
Self { headers }
}
}
impl<'a> Extractor for HeaderMapCarrier<'a> {
fn get(&self, key: &str) -> Option<&str> {
// Case-insensitive header lookup
self.headers.iter()
.find(|(k, _)| k.to_lowercase() == key.to_lowercase())
.map(|(_, v)| v.as_str())
}
fn keys(&self) -> Vec<&str> {
self.headers.keys().map(|k| k.as_str()).collect()
}
}
fn process_request(headers: &HashMap<String, String>) -> Result<(), Box<dyn std::error::Error>> {
println!("Processing request with {} headers", headers.len());
Ok(())
}
Simple Axum Integration
use axum::{
extract::Request,
http::{HeaderMap, StatusCode},
middleware::{self, Next},
response::Response,
routing::get,
Router,
};
use opentelemetry::{global, propagation::Extractor, trace::{SpanKind, Tracer}};
// Simple propagation middleware for Axum
async fn propagation_middleware(request: Request, next: Next) -> Result<Response, StatusCode> {
let headers = request.headers();
// Extract trace context from headers
let parent_context = global::get_text_map_propagator(|propagator| {
propagator.extract(&AxumHeaderExtractor::new(headers))
});
let tracer = global::tracer("axum-server");
// Create span with extracted context
let span = tracer
.span_builder("http-request")
.with_kind(SpanKind::Server)
.start_with_context(&tracer, &parent_context);
let _guard = span.activate();
// Add basic attributes
span.add_attribute("http.method", request.method().to_string());
span.add_attribute("http.target", request.uri().path().to_string());
let response = next.run(request).await;
span.add_attribute("http.status_code", response.status().as_u16() as i64);
Ok(response)
}
// Header extractor for Axum
struct AxumHeaderExtractor<'a> {
headers: &'a HeaderMap,
}
impl<'a> AxumHeaderExtractor<'a> {
fn new(headers: &'a HeaderMap) -> Self {
Self { headers }
}
}
impl<'a> Extractor for AxumHeaderExtractor<'a> {
fn get(&self, key: &str) -> Option<&str> {
self.headers.get(key)?.to_str().ok()
}
fn keys(&self) -> Vec<&str> {
self.headers.keys().map(|k| k.as_str()).collect()
}
}
// Route handler
async fn hello() -> &'static str {
let tracer = global::tracer("handler");
let span = tracer.span_builder("hello-handler").start(&tracer);
let _guard = span.activate();
"Hello, World!"
}
// Application setup
pub fn create_app() -> Router {
Router::new()
.route("/hello", get(hello))
.layer(middleware::from_fn(propagation_middleware))
}
Injecting context
use opentelemetry::{global, propagation::Injector, trace::Tracer};
use reqwest::Client;
use std::collections::HashMap;
async fn make_http_request(url: &str) -> Result<reqwest::Response, reqwest::Error> {
let tracer = global::tracer("http-client");
let span = tracer.span_builder("http-request").start(&tracer);
let _guard = span.activate();
// Create carrier for headers
let mut headers = HashMap::new();
// Inject current trace context into headers
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&opentelemetry::Context::current(), &mut HeaderMapInjector::new(&mut headers));
});
span.add_attribute("http.url", url.to_string());
span.add_attribute("http.method", "GET");
// Make HTTP request with injected headers
let client = Client::new();
let mut request = client.get(url);
for (key, value) in headers {
request = request.header(&key, value);
}
let response = request.send().await?;
span.add_attribute("http.status_code", response.status().as_u16() as i64);
Ok(response)
}
// Header injector implementation
struct HeaderMapInjector<'a> {
headers: &'a mut HashMap<String, String>,
}
impl<'a> HeaderMapInjector<'a> {
fn new(headers: &'a mut HashMap<String, String>) -> Self {
Self { headers }
}
}
impl<'a> Injector for HeaderMapInjector<'a> {
fn set(&mut self, key: &str, value: String) {
self.headers.insert(key.to_string(), value);
}
}
// Usage
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize tracing...
let response = make_http_request("https://api.example.com/users").await?;
println!("Status: {}", response.status());
Ok(())
}
Simple HTTP Client
use opentelemetry::{global, propagation::Injector, trace::{SpanKind, Tracer}};
use reqwest::{Client, Response};
use std::collections::HashMap;
pub struct SimpleTracedClient {
client: Client,
}
impl SimpleTracedClient {
pub fn new() -> Self {
Self {
client: Client::new(),
}
}
pub async fn get(&self, url: &str) -> Result<Response, reqwest::Error> {
let tracer = global::tracer("http-client");
let span = tracer
.span_builder("HTTP GET")
.with_kind(SpanKind::Client)
.start(&tracer);
let _guard = span.activate();
span.add_attribute("http.method", "GET");
span.add_attribute("http.url", url.to_string());
// Inject trace context into request headers
let mut headers_map = HashMap::new();
global::get_text_map_propagator(|propagator| {
propagator.inject_context(&opentelemetry::Context::current(), &mut HeaderMapInjector::new(&mut headers_map));
});
let mut request = self.client.get(url);
for (key, value) in headers_map {
if let (Ok(header_name), Ok(header_value)) = (
key.parse::<reqwest::header::HeaderName>(),
value.parse::<reqwest::header::HeaderValue>()
) {
request = request.header(header_name, header_value);
}
}
match request.send().await {
Ok(response) => {
span.add_attribute("http.status_code", response.status().as_u16() as i64);
span.set_status(opentelemetry::trace::Status::Ok);
Ok(response)
}
Err(e) => {
span.set_status(opentelemetry::trace::Status::error(format!("Request failed: {}", e)));
Err(e)
}
}
}
}
// Usage
async fn example_usage() -> Result<(), Box<dyn std::error::Error>> {
let client = SimpleTracedClient::new();
let response = client.get("https://api.example.com/users").await?;
println!("Response status: {}", response.status());
Ok(())
}
Debugging propagation
Logging context
Log incoming traceparent headers and current span context for debugging:
use opentelemetry::trace::TraceContextExt;
use tracing::{info, warn};
pub fn log_trace_context(request_headers: &HashMap<String, String>) {
// Log incoming traceparent header
match request_headers.get("traceparent") {
Some(traceparent) => {
info!("Incoming traceparent: {}", traceparent);
}
None => {
info!("No traceparent header found");
}
}
// Log current span context
let current_context = opentelemetry::Context::current();
let span = current_context.span();
if span.span_context().is_valid() {
let span_context = span.span_context();
info!(
"Current trace context - TraceId: {}, SpanId: {}, Sampled: {}",
span_context.trace_id(),
span_context.span_id(),
span_context.trace_flags().is_sampled()
);
} else {
warn!("No valid span context found");
}
}
// Usage in middleware
async fn debug_middleware(request: Request, next: Next) -> Result<Response, StatusCode> {
let headers: HashMap<String, String> = request
.headers()
.iter()
.filter_map(|(k, v)| {
v.to_str().ok().map(|v| (k.to_string(), v.to_string()))
})
.collect();
log_trace_context(&headers);
next.run(request).await
}
Validating format
Validate traceparent headers to ensure they follow the W3C specification:
use regex::Regex;
pub struct TraceparentValidator {
pattern: Regex,
}
impl TraceparentValidator {
pub fn new() -> Self {
// W3C traceparent format: 00-{trace-id}-{span-id}-{trace-flags}
let pattern = Regex::new(r"^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$")
.expect("Valid regex pattern");
Self { pattern }
}
pub fn is_valid(&self, traceparent: &str) -> bool {
self.pattern.is_match(traceparent)
}
pub fn parse(&self, traceparent: &str) -> Result<TraceparentInfo, String> {
if !self.is_valid(traceparent) {
return Err(format!("Invalid traceparent format: {}", traceparent));
}
let parts: Vec<&str> = traceparent.split('-').collect();
Ok(TraceparentInfo {
version: parts[0].to_string(),
trace_id: parts[1].to_string(),
span_id: parts[2].to_string(),
flags: parts[3].to_string(),
sampled: parts[3] == "01",
})
}
}
#[derive(Debug)]
pub struct TraceparentInfo {
pub version: String,
pub trace_id: String,
pub span_id: String,
pub flags: String,
pub sampled: bool,
}
// Usage
fn validate_traceparent_example() {
let validator = TraceparentValidator::new();
let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01";
match validator.parse(traceparent) {
Ok(info) => {
println!("Valid traceparent: {:?}", info);
}
Err(e) => {
println!("Invalid traceparent: {}", e);
}
}
}
Getting trace info
Access current trace context information:
use opentelemetry::trace::TraceContextExt;
use serde_json::json;
pub fn get_trace_info() -> serde_json::Value {
let current_context = opentelemetry::Context::current();
let span = current_context.span();
if !span.span_context().is_valid() {
return json!({ "error": "No valid span context available" });
}
let span_context = span.span_context();
json!({
"trace_id": span_context.trace_id().to_string(),
"span_id": span_context.span_id().to_string(),
"sampled": span_context.trace_flags().is_sampled(),
"remote": span_context.is_remote(),
"trace_state": span_context.trace_state().header()
})
}
// Axum handler to expose trace info
async fn trace_info_handler() -> axum::Json<serde_json::Value> {
axum::Json(get_trace_info())
}
// Middleware to add trace info to response headers
async fn trace_info_middleware(request: Request, next: Next) -> Result<Response, StatusCode> {
let mut response = next.run(request).await;
// Add trace info to response headers for debugging
let trace_info = get_trace_info();
if trace_info.get("error").is_none() {
let headers = response.headers_mut();
if let Some(trace_id) = trace_info.get("trace_id").and_then(|v| v.as_str()) {
if let Ok(header_value) = trace_id.parse() {
headers.insert("X-Trace-Id", header_value);
}
}
if let Some(span_id) = trace_info.get("span_id").and_then(|v| v.as_str()) {
if let Ok(header_value) = span_id.parse() {
headers.insert("X-Span-Id", header_value);
}
}
}
Ok(response)
}
Configuration
Environment variables
Configure propagation format using environment variables:
# Default: W3C TraceContext and Baggage
export OTEL_PROPAGATORS="tracecontext,baggage"
# B3 format (requires opentelemetry-propagator-b3 crate)
export OTEL_PROPAGATORS="b3"
# Multiple formats
export OTEL_PROPAGATORS="tracecontext,b3,baggage"
Programmatic config
use opentelemetry::{
global,
propagation::{composite::TextMapCompositePropagator, text_map_propagator::TextMapPropagator},
};
use opentelemetry_sdk::propagation::{TraceContextPropagator, BaggagePropagator};
fn configure_propagation() {
// Create composite propagator with multiple formats
let propagator = TextMapCompositePropagator::new(vec![
// W3C TraceContext (default)
Box::new(TraceContextPropagator::new()),
// W3C Baggage
Box::new(BaggagePropagator::new()),
]);
// Set as global propagator
global::set_text_map_propagator(propagator);
}
// Usage in application initialization
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
// Configure propagation before initializing tracing
configure_propagation();
// Initialize OpenTelemetry...
init_tracing().await?;
// Run application
run_app().await?;
Ok(())
}
Production considerations
Performance
Propagation has minimal performance impact in Rust:
- Header extraction/injection is fast (< 1ms)
- Context objects are lightweight
- No network calls during propagation
- Zero-cost abstractions when not recording
Async Context Propagation
Rust's async system automatically propagates context across .await points:
async fn nested_async_operations() -> Result<(), Box<dyn std::error::Error>> {
let tracer = global::tracer("async-service");
let span = tracer.span_builder("parent-operation").start(&tracer);
let _guard = span.activate();
// Context is automatically available in nested async calls
let result1 = async_database_call().await?;
let result2 = async_http_call().await?;
// Spawned tasks inherit the current context
let handle = tokio::spawn(async move {
background_processing().await
});
handle.await??;
Ok((result1, result2))
}
async fn async_database_call() -> Result<String, Box<dyn std::error::Error>> {
let tracer = global::tracer("database");
let span = tracer.span_builder("db-query").start(&tracer);
let _guard = span.activate();
// Simulate database call
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
Ok("Database result".to_string())
}
async fn async_http_call() -> Result<String, Box<dyn std::error::Error>> {
let tracer = global::tracer("http-client");
let span = tracer.span_builder("http-call").start(&tracer);
let _guard = span.activate();
// HTTP client will automatically inject trace context
let client = reqwest::Client::new();
let response = client.get("https://api.example.com/data").send().await?;
Ok(response.text().await?)
}
async fn background_processing() -> Result<(), Box<dyn std::error::Error>> {
let tracer = global::tracer("background");
let span = tracer.span_builder("background-task").start(&tracer);
let _guard = span.activate();
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
Ok(())
}
Microservices Integration
Complete example for microservice communication:
use axum::{
extract::State,
http::StatusCode,
middleware,
response::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
http_client: Arc<SimpleTracedClient>,
}
#[derive(Serialize, Deserialize)]
struct UserRequest {
name: String,
email: String,
}
#[derive(Serialize, Deserialize)]
struct UserResponse {
id: u64,
name: String,
email: String,
}
// Service A: User service
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<UserRequest>,
) -> Result<Json<UserResponse>, StatusCode> {
let tracer = global::tracer("user-service");
let span = tracer.span_builder("create-user").start(&tracer);
let _guard = span.activate();
span.add_attribute("user.name", payload.name.clone());
span.add_attribute("user.email", payload.email.clone());
// Call external service (context automatically propagated)
let validation_result = state
.http_client
.get("http://validation-service:8080/validate")
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
if !validation_result.status().is_success() {
return Err(StatusCode::BAD_REQUEST);
}
// Create user (simulate database call)
let user = UserResponse {
id: 123,
name: payload.name,
email: payload.email,
};
span.set_status(opentelemetry::trace::Status::Ok);
Ok(Json(user))
}
// Service B: Validation service
async fn validate_user(Json(payload): Json<UserRequest>) -> Result<StatusCode, StatusCode> {
let tracer = global::tracer("validation-service");
let span = tracer.span_builder("validate-user").start(&tracer);
let _guard = span.activate();
// Validation logic
if payload.email.contains("@") && !payload.name.is_empty() {
span.set_status(opentelemetry::trace::Status::Ok);
Ok(StatusCode::OK)
} else {
span.set_status(opentelemetry::trace::Status::error("Validation failed"));
Err(StatusCode::BAD_REQUEST)
}
}
// Application setup with propagation
pub fn create_user_service() -> Router {
let http_client = Arc::new(SimpleTracedClient::new());
let state = AppState { http_client };
Router::new()
.route("/users", post(create_user))
.layer(middleware::from_fn(propagation_middleware))
.layer(middleware::from_fn(trace_info_middleware))
.with_state(state)
}
pub fn create_validation_service() -> Router {
Router::new()
.route("/validate", post(validate_user))
.layer(middleware::from_fn(propagation_middleware))
}
// Server startup
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
// Initialize tracing
init_tracing().await?;
// Choose service based on environment
let app = match std::env::var("SERVICE_TYPE").as_deref() {
Ok("validation") => create_validation_service(),
_ => create_user_service(),
};
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
println!("Server running on http://0.0.0.0:8080");
axum::serve(listener, app).await?;
Ok(())
}
async fn init_tracing() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
// Configure propagation
configure_propagation();
// Initialize OpenTelemetry
let tracer = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint("https://api.uptrace.dev:4317")
)
.install_batch(opentelemetry_sdk::runtime::Tokio)?;
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
tracing_subscriber::registry()
.with(telemetry)
.with(tracing_subscriber::EnvFilter::from_default_env())
.init();
Ok(())
}
OpenTelemetry APM
Uptrace is a OpenTelemetry APM that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks.
Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.
In just a few minutes, you can try Uptrace by visiting the cloud demo (no login required) or running it locally with Docker. The source code is available on GitHub.