# 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:
```rust,ignore
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
```rust
// 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
```rust,ignore
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:
| `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:
```rust,ignore
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:
```rust,ignore
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:
```rust,ignore
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:
```rust,ignore
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:
```rust,ignore
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](https://docs.rs/opentelemetry) 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`:
```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.