otel 0.1.0

Ergonomic macros for OpenTelemetry tracing in Rust
Documentation

OpenTelemetry instrumentation macros for distributed tracing.

Just a few macros for when you're working directly with OpenTelemetry crates, and not tracing.

Features

  • Simple tracer creation: Declare static tracers with a single macro call
  • Ergonomic span creation: Create spans with minimal boilerplate
  • Automatic code location: All spans include file, line, and column attributes
  • Flexible API: Use default or named tracers, add custom attributes
  • Zero runtime overhead: Uses LazyLock for lazy initialization

Quick Start

1. Initialize OpenTelemetry in your application

Before any spans can be created, configure and install a tracer provider:

fn main() {
    // Example: Stdout exporter for development
    let provider = opentelemetry_stdout::new_pipeline()
        .install_simple();

    opentelemetry::global::set_tracer_provider(provider);

    run_app();

    opentelemetry::global::shutdown_tracer_provider();
}

2. Declare a tracer at your crate root

// In src/lib.rs or src/main.rs
otel::tracer!();

This creates a TRACER static available throughout your crate.

3. Import and create spans

use crate::TRACER;

fn process_data(items: &[Item]) {
    let (_cx, _guard) = otel::span!(
        "data.process",
        "item.count" => items.len() as i64
    );

    // Your code here - execution is traced
    // Span ends when _guard drops
}

Synchronous vs Asynchronous Usage

OpenTelemetry context is stored in thread-local storage. This works naturally in synchronous code, but async tasks can migrate between threads at .await points. You must explicitly propagate context through async boundaries.

Synchronous Code

The guard automatically manages span lifetime:

fn process_batch(items: &[Item]) {
    let (_cx, _guard) = otel::span!("batch.process");

    for item in items {
        // Child span - automatically parented to batch.process
        let (_cx, _guard) = otel::span!("item.process");
        process_item(item);
    }
}

Asynchronous Code

Use [FutureExt::with_context] to propagate context across .await points:

use opentelemetry::trace::FutureExt;

async fn fetch_user(id: u64) -> Result<User> {
    let (cx, _guard) = otel::span!("user.fetch", "user.id" => id as i64);

    db.get_user(id)
        .with_context(cx)
        .await
}

For multiple awaits, clone the context:

async fn process_order(id: u64) -> Result<()> {
    let (cx, _guard) = otel::span!("order.process");

    let order = fetch_order(id).with_context(cx.clone()).await?;
    validate(&order).with_context(cx.clone()).await?;
    submit(&order).with_context(cx).await
}

See [span!] documentation for more async patterns including spawned tasks and concurrent operations.

Requirements

Your application must initialize an OpenTelemetry tracer provider before using these macros. See the OpenTelemetry documentation for setup instructions.

Build Configuration

For clean file paths in span attributes (e.g., src/lib.rs instead of /home/user/project/src/lib.rs), enable path trimming in Cargo.toml:

[profile.dev]
trim-paths = "all"

[profile.release]
trim-paths = "all"

This affects the code.file.path attribute on all spans. Without this setting, paths will be absolute and vary across build environments.