otel

Crates.iootel
lib.rsotel
version0.6.0
created_at2025-12-09 07:59:16.785074+00
updated_at2025-12-29 06:52:23.175631+00
descriptionErgonomic macros for OpenTelemetry tracing in Rust
homepage
repositoryhttps://github.com/gold-build/otel
max_upload_size
id1974994
size151,990
Owen Kelly (ojkelly)

documentation

README

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
  • Closure-based spans: Use in |cx| { ... } syntax for automatic span lifetime management
  • 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. Create spans in your code

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

    // Your code here - execution is traced
    // Span ends automatically at end of scope
}

Syntax Reference

The span! macro supports multiple forms depending on your use case:

Syntax Returns Use Case
span!("name") () Default tracer, creates internal guard
span!("name", "k" = v, ...) () With custom attributes, internal guard
span!("name", in |cx| { ... }) T With closure (auto-managed lifetime)
span!("name", "k" = v, in |cx| { ... }) T Closure + attributes
span!(@TRACER, "name") () Explicit tracer, internal guard
span!(@TRACER, "name", "k" = v) () Explicit tracer + attributes, internal guard
span!(@TRACER, "name", in |cx| { ... }) T Explicit tracer with closure
span!(@TRACER, "name", "k" = v, in |cx| { ... }) T Explicit tracer + attributes + closure
span!(^ "name") Context Detached span (no automatic guard)
span!(^ "name", "k" = v) Context Detached with attributes
span!(^ @TRACER, "name") Context Detached with explicit tracer

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

Spans are automatically managed through lexical scoping:

fn process_batch(items: &[Item]) {
    otel::span!("batch.process");

    for item in items {
        // Child span - automatically parented to batch.process
        otel::span!("item.process");
        process_item(item);
    }
    // Spans end automatically at end of scope
}

Closure-Based Spans

For automatic span lifetime management with return values, use the in keyword:

fn compute_result(input: &Data) -> Result<Output> {
    otel::span!("computation.execute", in |cx| {
        validate(input)?;
        let processed = process(input)?;
        Ok(processed)
    })
}

The closure receives the Context as a parameter and can return any value. The span automatically ends when the closure completes or panics.

With attributes:

fn fetch_user(user_id: u64) -> Result<User> {
    otel::span!(
        "user.fetch",
        "user.id" = user_id as i64,
        in |cx| {
            database.get_user(user_id)
        }
    )
}

When to use closure syntax:

  • The span lifetime matches a single expression or block
  • You want automatic cleanup even on early return or ?
  • You need to return a value from the span

When to use manual guard syntax:

  • The span covers multiple statements with complex control flow
  • You need the context variable for explicit async propagation
  • You're wrapping a large function body

Asynchronous Code

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

use opentelemetry::trace::FutureExt;

async fn fetch_user(id: u64) -> Result<User> {
    otel::span!("user.fetch", "user.id" = id as i64, in |cx| {
        db.get_user(id)
            .with_context(cx)
    }).await
}

For multiple awaits, use the detached form to get a context variable:

async fn process_order(id: u64) -> Result<()> {
    let cx = 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.

Commit count: 0

cargo fmt