OpenTelemetry Rust Tracing API

This document teaches you how to use the OpenTelemetry Rust API to instrument your applications with distributed tracing. To learn how to install and configure the OpenTelemetry Rust SDK, see Getting started with OpenTelemetry Rust.

Prerequisites

Make sure your exporter is configured before you start instrumenting code. Follow Getting started with OpenTelemetry Rust or set up Direct OTLP Configuration first.

Installation

Add the required dependencies to your Cargo.toml:

toml
[dependencies]
opentelemetry = "0.30.0"
opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] }
tokio = { version = "1.0", features = ["full"] }

Quickstart

Step 1. Let's instrument this async function:

rust
async fn create_user(name: String, email: String) -> Result<User, UserError> {
    User::create(name, email).await
}

Step 2. Wrap the operation with a span. Tracer::in_span only accepts a synchronous closure, so for async code, start the span manually and attach it to the future's context with FutureExt::with_context:

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

async fn create_user(name: String, email: String) -> Result<User, UserError> {
    let tracer = global::tracer("my-rust-app");
    let span = tracer.start("create-user");
    let cx = Context::current_with_span(span);

    async move { User::create(name, email).await }
        .with_context(cx)
        .await
}

Step 3. Add error handling and status. Use Context::current().span() inside the future to access the active span:

rust
use opentelemetry::trace::Status;

async fn create_user(name: String, email: String) -> Result<User, UserError> {
    let tracer = global::tracer("my-rust-app");
    let span = tracer.start("create-user");
    let cx = Context::current_with_span(span);

    async move {
        let span = Context::current().span();

        match User::create(name, email).await {
            Ok(user) => {
                span.set_status(Status::Ok);
                Ok(user)
            }
            Err(e) => {
                span.set_status(Status::error(format!("User creation failed: {}", e)));
                Err(e)
            }
        }
    }
    .with_context(cx)
    .await
}

Step 4. Add contextual attributes:

rust
async fn create_user(name: String, email: String) -> Result<User, UserError> {
    let tracer = global::tracer("my-rust-app");
    let span = tracer.start("create-user");
    let cx = Context::current_with_span(span);

    async move {
        let span = Context::current().span();

        if span.is_recording() {
            span.set_attribute(KeyValue::new("user.name", name.clone()));
            span.set_attribute(KeyValue::new("user.email", email.clone()));
        }

        match User::create(name, email).await {
            Ok(user) => {
                if span.is_recording() {
                    span.set_attribute(KeyValue::new("user.id", user.id.to_string()));
                }
                span.set_status(Status::Ok);
                Ok(user)
            }
            Err(e) => {
                span.set_status(Status::error(format!("Failed: {}", e)));
                Err(e)
            }
        }
    }
    .with_context(cx)
    .await
}

Tracer

Create a tracer to start generating spans:

rust
use opentelemetry::{global, trace::Tracer};

// Basic tracer
let tracer = global::tracer("my-rust-app");

// With version
let tracer = global::tracer_with_version("my-rust-app", "1.0.0");

Global Tracer Pattern

rust
use std::sync::OnceLock;

static TRACER: OnceLock<Box<dyn Tracer + Send + Sync>> = OnceLock::new();

pub fn tracer() -> &'static dyn Tracer {
    TRACER.get_or_init(|| global::tracer("my-rust-app")).as_ref()
}

Creating Spans

Basic Spans

For synchronous code, Tracer::in_span handles span lifecycle automatically:

rust
fn handle_request_sync() -> Result<String, MyError> {
    let tracer = global::tracer("my-app");

    tracer.in_span("handle-request", |cx| {
        let span = cx.span();
        span.set_attribute(KeyValue::new("operation.type", "request"));

        do_work_sync()
    })
}

For async code, start the span and attach it to the future instead:

rust
async fn handle_request() -> Result<String, MyError> {
    let tracer = global::tracer("my-app");
    let span = tracer.start("handle-request");
    span.set_attribute(KeyValue::new("operation.type", "request"));
    let cx = Context::current_with_span(span);

    async move { do_work().await }.with_context(cx).await
}

Span Kinds

rust
use opentelemetry::trace::{SpanKind, Tracer};

// Server span (incoming requests)
let span = tracer.span_builder("handle-request")
    .with_kind(SpanKind::Server)
    .start(&tracer);

// Client span (outgoing requests)
let span = tracer.span_builder("http-request")
    .with_kind(SpanKind::Client)
    .start(&tracer);

// Internal operations (default)
let span = tracer.start("internal-work");
let cx = Context::current_with_span(span);
async move { process_data().await }.with_context(cx).await

Manual Span Management

rust
async fn manual_span() -> Result<(), MyError> {
    let tracer = global::tracer("my-app");

    let span = tracer.span_builder("manual-operation").start(&tracer);
    let cx = opentelemetry::Context::current_with_span(span);

    async move {
        let span = cx.span();
        span.set_status(Status::Ok);
        Ok(())
    }.with_context(cx).await
}

Adding Span Attributes

rust
async fn with_attributes() -> Result<(), MyError> {
    let tracer = global::tracer("my-app");
    let span = tracer.start("operation");
    let cx = Context::current_with_span(span);

    async move {
        let span = Context::current().span();

        if span.is_recording() {
            // Single attribute
            span.set_attribute(KeyValue::new("user.id", "12345"));

            // Multiple attributes
            span.set_attributes(vec![
                KeyValue::new("http.request.method", "GET"),
                KeyValue::new("http.response.status_code", 200),
                KeyValue::new("user.role", "admin"),
            ]);
        }

        Ok(())
    }
    .with_context(cx)
    .await
}

Adding Span Events

rust
async fn with_events() -> Result<(), MyError> {
    let tracer = global::tracer("my-app");
    let span = tracer.start("file-processing");
    let cx = Context::current_with_span(span);

    async move {
        let span = Context::current().span();

        // Simple event
        span.add_event("processing.started", vec![]);

        // Event with attributes
        span.add_event("user.authenticated", vec![
            KeyValue::new("user.id", "12345"),
            KeyValue::new("auth.method", "oauth"),
        ]);

        // Process file...
        Ok(())
    }
    .with_context(cx)
    .await
}

Setting Span Status

rust
use opentelemetry::trace::Status;

async fn with_status() -> Result<String, MyError> {
    let tracer = global::tracer("my-app");
    let span = tracer.start("database-query");
    let cx = Context::current_with_span(span);

    async move {
        let span = Context::current().span();

        match perform_query().await {
            Ok(result) => {
                span.set_status(Status::Ok);
                Ok(result)
            }
            Err(e) => {
                span.set_status(Status::error(format!("Query failed: {}", e)));
                Err(e)
            }
        }
    }
    .with_context(cx)
    .await
}

Current Span and Context

Getting Current Span

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

async fn add_user_info(user: &User) {
    let current_context = Context::current();
    let span = current_context.span();

    if span.is_recording() {
        span.set_attribute(KeyValue::new("user.id", user.id.to_string()));
    }
}

Context Nesting

Child spans automatically link to whichever span is active in the current context, so nest by attaching each span's context before running its child:

rust
async fn nested_operations() -> Result<(), MyError> {
    let tracer = global::tracer("my-app");

    // Parent span
    let parent_span = tracer.start("parent-operation");
    let parent_cx = Context::current_with_span(parent_span);

    async move {
        // Child span - automatically nested under the attached parent context
        let child_span = tracer.start("child-operation");
        let child_cx = Context::current_with_span(child_span);

        async move {
            // Both spans are properly linked
            Ok(())
        }
        .with_context(child_cx)
        .await
    }
    .with_context(parent_cx)
    .await
}

Async Context Propagation

rust
use tokio::task;

async fn spawn_with_context() -> Result<(), MyError> {
    let tracer = global::tracer("my-app");
    let span = tracer.start("parent-task");
    let cx = Context::current_with_span(span);

    async move {
        // Context must be passed explicitly into spawned tasks
        let task_cx = Context::current();
        let handle = task::spawn(
            async move {
                let span = Context::current().span();

                if span.is_recording() {
                    span.add_event("spawned-task-running", vec![]);
                }

                "Task completed"
            }
            .with_context(task_cx),
        );

        let result = handle.await?;
        println!("Result: {}", result);
        Ok(())
    }
    .with_context(cx)
    .await
}

Best Practices

Error Handling Pattern

rust
async fn service_operation(params: &ServiceParams) -> Result<ServiceResult, ServiceError> {
    let tracer = global::tracer("service");
    let span = tracer.start("service-operation");
    span.set_attribute(KeyValue::new("operation.type", "service_call"));
    let cx = Context::current_with_span(span);

    async move {
        let span = Context::current().span();

        match perform_operation(params).await {
            Ok(result) => {
                span.set_status(Status::Ok);
                if span.is_recording() {
                    span.set_attribute(KeyValue::new("operation.result", "success"));
                }
                Ok(result)
            }
            Err(e) => {
                span.set_status(Status::error(format!("Operation failed: {}", e)));
                if span.is_recording() {
                    span.set_attribute(KeyValue::new("error.type", e.error_type()));
                }
                Err(e)
            }
        }
    }
    .with_context(cx)
    .await
}

Attribute Optimization

rust
async fn optimized_spans() -> Result<(), MyError> {
    let tracer = global::tracer("my-app");
    let span = tracer.start("expensive-operation");
    let cx = Context::current_with_span(span);

    async move {
        let span = Context::current().span();

        // Only do expensive work if span is being recorded
        if span.is_recording() {
            let expensive_data = calculate_metrics().await;
            span.set_attribute(KeyValue::new("computed.value", expensive_data));
        }

        // Always do the main work
        perform_operation().await
    }
    .with_context(cx)
    .await
}

Span Naming

rust
// Good: Low cardinality, descriptive
let span = tracer.start("GET /users/:id");
let cx = Context::current_with_span(span);
async move {
    let span = Context::current().span();
    if span.is_recording() {
        span.set_attribute(KeyValue::new("user.id", user_id.to_string()));
    }
    get_user(user_id).await
}
.with_context(cx)
.await

// Avoid: High cardinality
// tracer.start(&format!("GET /users/{}", user_id))

Integration Examples

HTTP Client

rust
use reqwest::Client;

pub struct TracedHttpClient {
    client: Client,
    tracer: Box<dyn opentelemetry::trace::Tracer + Send + Sync>,
}

impl TracedHttpClient {
    pub fn new() -> Self {
        Self {
            client: Client::new(),
            tracer: Box::new(global::tracer("http-client")),
        }
    }

    pub async fn get(&self, url: &str) -> Result<reqwest::Response, reqwest::Error> {
        let span = self.tracer.start("HTTP GET");
        let cx = Context::current_with_span(span);

        async move {
            let span = Context::current().span();

            if span.is_recording() {
                span.set_attributes(vec![
                    KeyValue::new("http.request.method", "GET"),
                    KeyValue::new("url.full", url.to_string()),
                ]);
            }

            match self.client.get(url).send().await {
                Ok(response) => {
                    if span.is_recording() {
                        span.set_attribute(KeyValue::new("http.response.status_code", response.status().as_u16() as i64));
                    }
                    span.set_status(Status::Ok);
                    Ok(response)
                }
                Err(e) => {
                    span.set_status(Status::error(format!("HTTP request failed: {}", e)));
                    Err(e)
                }
            }
        }
        .with_context(cx)
        .await
    }
}

Axum Integration

rust
use axum::{extract::Path, response::Json, routing::get, Router};
use serde_json::{json, Value};
use tracing::{info, instrument};

// Use #[instrument] for automatic span creation
#[instrument(skip_all, fields(user_id = %user_id))]
async fn get_user(Path(user_id): Path<u32>) -> Result<Json<Value>, axum::http::StatusCode> {
    info!("Fetching user data");

    match fetch_user(user_id).await {
        Ok(user) => Ok(Json(json!({ "id": user_id, "name": user.name }))),
        Err(_) => Err(axum::http::StatusCode::NOT_FOUND),
    }
}

#[instrument]
async fn health_check() -> Json<Value> {
    Json(json!({ "status": "healthy" }))
}

pub fn create_app() -> Router {
    Router::new()
        .route("/users/:id", get(get_user))
        .route("/health", get(health_check))
}

Performance Considerations

Conditional Instrumentation

rust
pub struct PerformanceCriticalService {
    enable_tracing: bool,
    tracer: Option<Box<dyn opentelemetry::trace::Tracer + Send + Sync>>,
}

impl PerformanceCriticalService {
    pub fn new(enable_tracing: bool) -> Self {
        let tracer = if enable_tracing {
            Some(Box::new(global::tracer("critical-service")) as Box<dyn opentelemetry::trace::Tracer + Send + Sync>)
        } else {
            None
        };

        Self { enable_tracing, tracer }
    }

    pub async fn perform_operation(&self) -> Result<String, MyError> {
        if let Some(tracer) = &self.tracer {
            let span = tracer.start("critical-operation");
            let cx = Context::current_with_span(span);
            async move { self.do_work().await }.with_context(cx).await
        } else {
            self.do_work().await
        }
    }

    async fn do_work(&self) -> Result<String, MyError> {
        Ok("Operation completed".to_string())
    }
}

Sampling Awareness

rust
async fn sampling_aware() -> Result<(), MyError> {
    let tracer = global::tracer("my-app");
    let span = tracer.start("operation");
    let cx = Context::current_with_span(span);

    async move {
        let span = Context::current().span();

        // Only do expensive work if span is being recorded
        if span.is_recording() {
            let expensive_data = calculate_expensive_metrics().await;
            span.set_attribute(KeyValue::new("expensive.data", expensive_data));
        }

        perform_main_operation().await
    }
    .with_context(cx)
    .await
}

Auto-instrumentation

OpenTelemetry Rust supports automatic instrumentation through the tracing ecosystem.

Installation

toml
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-opentelemetry = "0.31"
opentelemetry = "0.30.0"
opentelemetry_sdk = { version = "0.30.0", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.30.0", features = ["grpc-tonic"] }

Supported Libraries

LibraryInstrumentation
reqwestAutomatic HTTP request spans
sqlxDatabase query tracing
axumHTTP request/response tracing
tokioTask and runtime tracing
tower-httpHTTP middleware tracing

Setup

rust
use opentelemetry::{global, trace::TracerProvider, KeyValue};
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::{trace::SdkTracerProvider, Resource};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
    // Build the OTLP span exporter
    let exporter = opentelemetry_otlp::SpanExporter::builder()
        .with_tonic()
        .with_endpoint("https://api.uptrace.dev:4317")
        .build()?;

    // Build the tracer provider
    let provider = SdkTracerProvider::builder()
        .with_batch_exporter(exporter)
        .with_resource(
            Resource::builder()
                .with_attributes(vec![
                    KeyValue::new("service.name", "rust-app"),
                    KeyValue::new("service.version", "1.0.0"),
                ])
                .build(),
        )
        .build();

    global::set_tracer_provider(provider.clone());
    let tracer = provider.tracer("rust-app");

    // Create tracing layer
    let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);

    // Initialize subscriber
    tracing_subscriber::registry()
        .with(telemetry)
        .with(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    // Your application code - automatically traced
    run_app().await?;

    provider.shutdown()?;
    Ok(())
}

Usage

rust
use tracing::{info, instrument};

#[instrument(skip_all, fields(user_id = %user_id))]
async fn get_user(user_id: u32) -> Result<User, UserError> {
    info!("Fetching user data");

    // SQLx queries will be automatically traced
    let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
        .bind(user_id)
        .fetch_one(&pool)
        .await?;

    Ok(user)
}

What's next?