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
| 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!(@detached, "name") | Context | Detached span (no automatic guard) |
span!(@detached, "name", "k" => v) | Context | Detached with attributes |
span!(@detached, @TRACER, "name") | Context | Detached with explicit tracer |
§Automatic Attributes
All spans automatically include:
| Attribute | Description |
|---|---|
code.file.path | Source file (workspace-relative via [relative_filepath]) |
code.line.number | Line number where span was created |
code.column.number | Column number |
thread.id | Current thread ID |
thread.name | Current 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:TRACERmust be in scope (use crate::TRACER;) - For
@tracervariants: The named tracer must be in scope - A tracer must have been declared with
tracer!