# 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
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. Import and create spans
```rust
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:
```rust
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);
}
}
```
### Closure-Based Spans
For automatic span lifetime management with return values, use the `in` keyword:
```rust
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
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 [`FutureExt::with_context`] to propagate context across `.await` points:
```rust
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:
```rust
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](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.