span

Macro span 

Source
macro_rules! span {
    (@maybe_cx $cx:ident, $tracer:ident, $name:expr $(, $($key:expr => $value:expr),+)?) => { ... };
    (@maybe_cx $tracer:ident, $name:expr $(, $($key:expr => $value:expr),+)?) => { ... };
    (@maybe_guard $cx:ident, $guard:ident) => { ... };
    (@maybe_guard $cx:ident,) => { ... };
    (@maybe_guard $cx:ident) => { ... };
    (@maybe_guard) => { ... };
    (@build $tracer:expr, $name:expr $(, $($key:expr => $value:expr),+)?) => { ... };
    ($name:expr, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => { ... };
    ($name:expr, $($key:expr => $value:expr),+, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => { ... };
    (@$tracer:ident, $name:expr, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => { ... };
    (@$tracer:ident, $name:expr, $($key:expr => $value:expr),+, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => { ... };
    ($name:expr $(,)?) => { ... };
    ($name:expr, $($key:expr => $value:expr),+ $(,)?) => { ... };
    (@$tracer:ident, $name:expr $(,)?) => { ... };
    (@$tracer:ident, $name:expr, $($key:expr => $value:expr),+ $(,)?) => { ... };
    (@detached, $name:expr $(,)?) => { ... };
    (@detached, $name:expr, $($key:expr => $value:expr),+ $(,)?) => { ... };
    (@detached, @$tracer:ident, $name:expr $(,)?) => { ... };
    (@detached, @$tracer:ident, $name:expr, $($key:expr => $value:expr),+ $(,)?) => { ... };
}
Expand description

Creates an OpenTelemetry span for tracing execution.

This macro creates a span with automatic code location attributes and returns both the span’s [Context] and a [ContextGuard] that keeps the span active.

§Return Value

Returns (Context, ContextGuard):

  • Context: The OpenTelemetry context containing the active span. Clone this to propagate through async boundaries or pass to child operations.
  • ContextGuard: RAII guard that keeps the context attached to the current thread. When dropped, the span ends and is exported.

Important: The span is only active while the guard is held. Dropping the guard ends the span, so keep it in scope for the duration of the traced operation.

§Syntax Variants

SyntaxReturnsUse Case
span!("name")()Default tracer, creates internal guard
span!("name", "k" => v, ...)()With custom attributes, internal guard
span!("name", in |cx| { ... })TWith closure (auto-managed lifetime)
span!("name", "k" => v, in |cx| { ... })TClosure + attributes
span!(@TRACER, "name")()Explicit tracer, internal guard
span!(@TRACER, "name", "k" => v)()Explicit tracer + attributes, internal guard
span!(@TRACER, "name", in |cx| { ... })TExplicit tracer with closure
span!(@TRACER, "name", "k" => v, in |cx| { ... })TExplicit tracer + attributes + closure
span!(@detached, "name")ContextDetached span (no automatic guard)
span!(@detached, "name", "k" => v)ContextDetached with attributes
span!(@detached, @TRACER, "name")ContextDetached with explicit tracer

§Automatic Attributes

All spans automatically include:

AttributeDescription
code.file.pathSource file (workspace-relative via [relative_filepath])
code.line.numberLine number where span was created
code.column.numberColumn number
thread.idCurrent thread ID
thread.nameCurrent thread name (or “unnamed”)

§Synchronous Usage

§Pattern 1: Statement form (automatic guard management)

The simplest form creates an internal guard that manages span lifetime automatically:

fn process_item(item: &Item) {
    otel::span!("item.process", "item.id" => item.id);

    validate(item);
    transform(item);
    save(item);

    // Span ends automatically at end of scope
}

Nested spans automatically form parent-child relationships:

fn process_batch(items: &[Item]) {
    otel::span!("batch.process", "count" => items.len() as i64);

    for item in items {
        // Automatically becomes a child of "batch.process"
        otel::span!("item.process", "item.id" => item.id);
        process_item(item);
    }
}

§Pattern 2: Closure-based spans

Use in closure syntax for automatic span lifetime management:

use crate::TRACER;

fn process_item(item: &Item) -> Result<ProcessedItem> {
    otel::span!("item.process", "item.id" => item.id, in |cx| {
        validate(item)?;
        let transformed = transform(item)?;
        save(&transformed)?;
        Ok(transformed)
    })
}

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

// Simple computation
let result = otel::span!("compute", in |cx| {
    expensive_calculation()
});

// With attributes
let user = otel::span!("db.fetch", "user.id" => user_id, in |cx| {
    db.get_user(user_id)
})?;

// Explicit tracer
let data = otel::span!(@CUSTOM_TRACER, "custom.operation", in |cx| {
    do_work()
});

§Asynchronous Usage

OpenTelemetry context is stored in thread-local storage. Since async tasks can migrate between threads at .await points, you must explicitly propagate context using [FutureExt::with_context].

§Pattern 1: Closure form for single async operation

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
}

§Pattern 2: Detached form for sequential awaits

Use the detached form (@detached) to get a context variable for multiple await points:

use opentelemetry::trace::FutureExt;

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

    let order = fetch_order(order_id)
        .with_context(cx.clone())
        .await?;

    validate_order(&order)
        .with_context(cx.clone())
        .await?;

    submit_order(&order)
        .with_context(cx)  // Last use, no clone needed
        .await
}

§Pattern 3: Concurrent/spawned tasks

Use detached form and clone the context for each spawned task:

use opentelemetry::trace::FutureExt;

async fn fetch_all(ids: Vec<u64>) -> Vec<Result<Item>> {
    let cx = otel::span!(@detached, "items.fetch_all", "count" => ids.len() as i64);

    let futures: Vec<_> = ids
        .into_iter()
        .map(|id| {
            let cx = cx.clone();
            async move {
                fetch_item(id)
                    .with_context(cx)
                    .await
            }
        })
        .collect();

    futures::future::join_all(futures).await
}

§Pattern 4: Child spans in async blocks

Use detached form for parent span, then create child spans inside async blocks:

use opentelemetry::trace::FutureExt;

async fn pipeline() -> Result<()> {
    let cx = otel::span!(@detached, "pipeline.run");

    async {
        otel::span!("pipeline.phase1");
        do_phase1().await
    }
    .with_context(cx.clone())
    .await?;

    async {
        otel::span!("pipeline.phase2");
        do_phase2().await
    }
    .with_context(cx)
    .await
}

§Detached Spans

Use @detached to create a span that returns only the Context, without a guard:

let cx = otel::span!(@detached, "background.task", "priority" => "low");

The span remains open until explicitly ended or the underlying span object is dropped. Use detached spans when:

  • Passing context to a spawned task that will manage its own lifetime
  • You need manual control over context attachment
  • The span lifetime doesn’t match lexical scope
async fn spawn_background_work() {
    let cx = otel::span!(@detached, "background.work");

    tokio::spawn(async move {
        // Attach context in the spawned task
        let _guard = cx.attach();
        do_background_work().await;
        // Span ends when _guard drops here
    });
}

§Span Naming Conventions

Use dot-separated hierarchical names describing the component and operation:

component.operation          → scheduler.run, cache.get, db.connect
component.subcomponent.op    → lsp.request.send, http.client.fetch
noun.verb                    → file.read, user.authenticate, order.submit

§Common Mistakes

§Forgetting to propagate context in async code

// WRONG: Context lost at await point
async fn bad_example() {
    otel::span!("operation");
    do_work().await;  // Span may not be active here!
}

// CORRECT: Use closure form or detached form for async
async fn good_example() {
    otel::span!("operation", in |cx| {
        do_work().with_context(cx)
    }).await;
}

// ALSO CORRECT: Detached form for multiple awaits
async fn also_good() {
    let cx = otel::span!(@detached, "operation");
    do_work().with_context(cx).await;
}

§Using statement form in expression position

// WRONG: Statement form returns (), not a value
fn bad_example() -> Result<Data> {
    let result = otel::span!("operation");  // result = ()
    get_data()
}

// CORRECT: Use closure form to return values
fn good_example() -> Result<Data> {
    otel::span!("operation", in |cx| {
        get_data()
    })
}

§Moving context without cloning for concurrent use

// WRONG: cx moved into first iteration
async fn bad_example(ids: Vec<u64>) {
    let cx = otel::span!(@detached, "fetch_all");
    for id in ids {
        tokio::spawn(fetch(id).with_context(cx));  // cx moved on first iteration!
    }
}

// CORRECT: Clone context for each task
async fn good_example(ids: Vec<u64>) {
    let cx = otel::span!(@detached, "fetch_all");
    for id in ids {
        let cx = cx.clone();
        tokio::spawn(async move {
            fetch(id).with_context(cx).await
        });
    }
}

§Requirements

  • For variants without @tracer: TRACER must be in scope (use crate::TRACER;)
  • For @tracer variants: The named tracer must be in scope
  • A tracer must have been declared with tracer!