otel/lib.rs
1#![doc = include_str!("../README.md")]
2
3/// OpenTelemetry specification version used for schema URLs.
4///
5/// This version is included in all tracer configurations to indicate
6/// which OpenTelemetry semantic conventions are being followed.
7pub const OTEL_SPEC_VERSION: &str = "1.38.0";
8
9/// Creates a static OpenTelemetry tracer for instrumentation.
10///
11/// This macro creates a `LazyLock<BoxedTracer>` static that initializes on first use
12/// from the global tracer provider. Declare at crate or module root, then import
13/// where needed.
14///
15/// # Prerequisites
16///
17/// The global tracer provider must be initialized before any spans are created.
18/// Typically done in `main()` before calling into application code:
19///
20/// ```ignore
21/// fn main() {
22/// let provider = opentelemetry_otlp::new_pipeline()
23/// .tracing()
24/// .with_exporter(opentelemetry_otlp::new_exporter().tonic())
25/// .install_batch(opentelemetry_sdk::runtime::Tokio)
26/// .expect("Failed to initialize tracer");
27///
28/// opentelemetry::global::set_tracer_provider(provider);
29///
30/// run_app(); // Now TRACER can be used
31///
32/// opentelemetry::global::shutdown_tracer_provider();
33/// }
34/// ```
35///
36/// # Variants
37///
38/// ## Default tracer (no arguments)
39///
40/// Creates a `TRACER` static using the crate's package name as the instrumentation scope:
41///
42/// ```ignore
43/// // In lib.rs or main.rs
44/// otel::tracer!();
45///
46/// // Creates: pub(crate) static TRACER: LazyLock<BoxedTracer>
47/// // Scope name: env!("CARGO_PKG_NAME")
48/// ```
49///
50/// ## Named tracer
51///
52/// Creates a tracer with a custom name suffix, useful for subsystems:
53///
54/// ```ignore
55/// // In src/client/mod.rs
56/// otel::tracer!(lsp_client);
57///
58/// // Creates: pub(crate) static LSP_CLIENT_TRACER: LazyLock<BoxedTracer>
59/// // Scope name: "{CARGO_PKG_NAME}.lsp_client"
60/// ```
61///
62/// # Tracer Configuration
63///
64/// Each tracer is configured with:
65///
66/// | Property | Value |
67/// |----------|-------|
68/// | Scope name | Crate name, or `{crate}.{subsystem}` for named tracers |
69/// | Version | `CARGO_PKG_VERSION` |
70/// | Schema URL | `https://opentelemetry.io/schemas/{OTEL_SPEC_VERSION}` |
71///
72/// # Multiple Tracers
73///
74/// Use named tracers to separate instrumentation by subsystem. This makes it easy
75/// to filter traces by scope in your observability backend:
76///
77/// ```ignore
78/// // src/lib.rs - default tracer for general use
79/// otel::tracer!();
80///
81/// // src/client/mod.rs - client subsystem
82/// otel::tracer!(client);
83///
84/// // src/server/mod.rs - server subsystem
85/// otel::tracer!(server);
86/// ```
87///
88/// Use explicit tracer syntax in `span!` to select which tracer to use:
89///
90/// ```ignore
91/// use crate::client::CLIENT_TRACER;
92///
93/// fn send_request() {
94/// let (_cx, _guard) = otel::span!(@CLIENT_TRACER, "request.send");
95/// }
96/// ```
97///
98/// # Example: Full Setup
99///
100/// ```ignore
101/// // src/lib.rs
102/// otel::tracer!();
103///
104/// pub mod engine;
105/// ```
106///
107/// ```ignore
108/// // src/engine.rs
109/// use crate::TRACER;
110///
111/// pub fn run() {
112/// let (_cx, _guard) = otel::span!("engine.run");
113/// // ...
114/// }
115/// ```
116#[macro_export]
117macro_rules! tracer {
118
119 ($name:ident) => {
120 paste::paste! {
121 pub(crate) static [<$name:snake:upper _TRACER>]: std::sync::LazyLock<opentelemetry::global::BoxedTracer> =
122 std::sync::LazyLock::new(|| {
123 use opentelemetry::trace::TracerProvider;
124
125 opentelemetry::global::tracer_provider().tracer_with_scope(
126 opentelemetry::InstrumentationScope::builder(concat!(
127 env!("CARGO_PKG_NAME"),
128 ".", stringify!([<$name:snake>])
129 ))
130 .with_version(env!("CARGO_PKG_VERSION"))
131 .with_schema_url(format!("https://opentelemetry.io/schemas/{}", $crate::OTEL_SPEC_VERSION))
132 .build(),
133 )
134 });
135 }
136 };
137
138 () => {
139 paste::paste! {
140 pub(crate) static TRACER: std::sync::LazyLock<opentelemetry::global::BoxedTracer> =
141 std::sync::LazyLock::new(|| {
142 use opentelemetry::trace::TracerProvider;
143
144 opentelemetry::global::tracer_provider().tracer_with_scope(
145 opentelemetry::InstrumentationScope::builder(concat!(
146 env!("CARGO_PKG_NAME"),
147 ))
148 .with_version(env!("CARGO_PKG_VERSION"))
149 .with_schema_url(format!("https://opentelemetry.io/schemas/{}", $crate::OTEL_SPEC_VERSION))
150 .build(),
151 )
152 });
153 }
154 };
155}
156
157/// Creates an OpenTelemetry span for tracing execution.
158///
159/// This macro creates a span with automatic code location attributes and returns
160/// both the span's [`Context`] and a [`ContextGuard`] that keeps the span active.
161///
162/// # Return Value
163///
164/// Returns `(Context, ContextGuard)`:
165///
166/// - **Context**: The OpenTelemetry context containing the active span. Clone this
167/// to propagate through async boundaries or pass to child operations.
168/// - **ContextGuard**: RAII guard that keeps the context attached to the current thread.
169/// When dropped, the span ends and is exported.
170///
171/// **Important**: The span is only active while the guard is held. Dropping the guard
172/// ends the span, so keep it in scope for the duration of the traced operation.
173///
174/// # Syntax Variants
175///
176/// | Syntax | Returns | Use Case |
177/// |--------|---------|----------|
178/// | `span!("name")` | `()` | Default tracer, creates internal guard |
179/// | `span!("name", "k" = v, ...)` | `()` | With custom attributes, internal guard |
180/// | `span!("name", in \|cx\| { ... })` | `T` | With closure (auto-managed lifetime) |
181/// | `span!("name", "k" = v, in \|cx\| { ... })` | `T` | Closure + attributes |
182/// | `span!(@TRACER, "name")` | `()` | Explicit tracer, internal guard |
183/// | `span!(@TRACER, "name", "k" = v)` | `()` | Explicit tracer + attributes, internal guard |
184/// | `span!(@TRACER, "name", in \|cx\| { ... })` | `T` | Explicit tracer with closure |
185/// | `span!(@TRACER, "name", "k" = v, in \|cx\| { ... })` | `T` | Explicit tracer + attributes + closure |
186/// | `span!(^ "name")` | `Context` | Detached span (no automatic guard) |
187/// | `span!(^ "name", "k" = v)` | `Context` | Detached with attributes |
188/// | `span!(^ @TRACER, "name")` | `Context` | Detached with explicit tracer |
189///
190/// # Automatic Attributes
191///
192/// All spans automatically include:
193///
194/// | Attribute | Description |
195/// |-----------|-------------|
196/// | `code.file.path` | Source file (workspace-relative via [`relative_filepath`]) |
197/// | `code.line.number` | Line number where span was created |
198/// | `code.column.number` | Column number |
199/// | `thread.id` | Current thread ID |
200/// | `thread.name` | Current thread name (or "unnamed") |
201///
202/// # Synchronous Usage
203///
204/// ## Pattern 1: Statement form (automatic guard management)
205///
206/// The simplest form creates an internal guard that manages span lifetime automatically:
207///
208/// ```ignore
209/// fn process_item(item: &Item) {
210/// otel::span!("item.process", "item.id" = item.id);
211///
212/// validate(item);
213/// transform(item);
214/// save(item);
215///
216/// // Span ends automatically at end of scope
217/// }
218/// ```
219///
220/// Nested spans automatically form parent-child relationships:
221///
222/// ```ignore
223/// fn process_batch(items: &[Item]) {
224/// otel::span!("batch.process", "count" = items.len() as i64);
225///
226/// for item in items {
227/// // Automatically becomes a child of "batch.process"
228/// otel::span!("item.process", "item.id" = item.id);
229/// process_item(item);
230/// }
231/// }
232/// ```
233///
234/// ## Pattern 2: Closure-based spans
235///
236/// Use `in` closure syntax for automatic span lifetime management:
237///
238/// ```ignore
239/// use crate::TRACER;
240///
241/// fn process_item(item: &Item) -> Result<ProcessedItem> {
242/// otel::span!("item.process", "item.id" = item.id, in |cx| {
243/// validate(item)?;
244/// let transformed = transform(item)?;
245/// save(&transformed)?;
246/// Ok(transformed)
247/// })
248/// }
249/// ```
250///
251/// The closure receives the `Context` as a parameter and can return any value.
252/// The span ends automatically when the closure completes or panics:
253///
254/// ```ignore
255/// // Simple computation
256/// let result = otel::span!("compute", in |cx| {
257/// expensive_calculation()
258/// });
259///
260/// // With attributes
261/// let user = otel::span!("db.fetch", "user.id" = user_id, in |cx| {
262/// db.get_user(user_id)
263/// })?;
264///
265/// // Explicit tracer
266/// let data = otel::span!(@CUSTOM_TRACER, "custom.operation", in |cx| {
267/// do_work()
268/// });
269/// ```
270///
271/// # Asynchronous Usage
272///
273/// OpenTelemetry context is stored in thread-local storage. Since async tasks can
274/// migrate between threads at `.await` points, you must explicitly propagate context
275/// using [`FutureExt::with_context`].
276///
277/// ## Pattern 1: Closure form for single async operation
278///
279/// ```ignore
280/// use opentelemetry::trace::FutureExt;
281///
282/// async fn fetch_user(id: u64) -> Result<User> {
283/// otel::span!("user.fetch", "user.id" = id as i64, in |cx| {
284/// db.get_user(id)
285/// .with_context(cx)
286/// }).await
287/// }
288/// ```
289///
290/// ## Pattern 2: Detached form for sequential awaits
291///
292/// Use the detached form (`@detached`) to get a context variable for multiple await points:
293///
294/// ```ignore
295/// use opentelemetry::trace::FutureExt;
296///
297/// async fn process_order(order_id: u64) -> Result<()> {
298/// let cx = otel::span!(^ "order.process", "order.id" = order_id as i64);
299///
300/// let order = fetch_order(order_id)
301/// .with_context(cx.clone())
302/// .await?;
303///
304/// validate_order(&order)
305/// .with_context(cx.clone())
306/// .await?;
307///
308/// submit_order(&order)
309/// .with_context(cx) // Last use, no clone needed
310/// .await
311/// }
312/// ```
313///
314/// ## Pattern 3: Concurrent/spawned tasks
315///
316/// Use detached form and clone the context for each spawned task:
317///
318/// ```ignore
319/// use opentelemetry::trace::FutureExt;
320///
321/// async fn fetch_all(ids: Vec<u64>) -> Vec<Result<Item>> {
322/// let cx = otel::span!(^ "items.fetch_all", "count" = ids.len() as i64);
323///
324/// let futures: Vec<_> = ids
325/// .into_iter()
326/// .map(|id| {
327/// let cx = cx.clone();
328/// async move {
329/// fetch_item(id)
330/// .with_context(cx)
331/// .await
332/// }
333/// })
334/// .collect();
335///
336/// futures::future::join_all(futures).await
337/// }
338/// ```
339///
340/// ## Pattern 4: Child spans in async blocks
341///
342/// Use detached form for parent span, then create child spans inside async blocks:
343///
344/// ```ignore
345/// use opentelemetry::trace::FutureExt;
346///
347/// async fn pipeline() -> Result<()> {
348/// let cx = otel::span!(^ "pipeline.run");
349///
350/// async {
351/// otel::span!("pipeline.phase1");
352/// do_phase1().await
353/// }
354/// .with_context(cx.clone())
355/// .await?;
356///
357/// async {
358/// otel::span!("pipeline.phase2");
359/// do_phase2().await
360/// }
361/// .with_context(cx)
362/// .await
363/// }
364/// ```
365///
366/// # Detached Spans
367///
368/// Use `@detached` to create a span that returns only the `Context`, without a guard:
369///
370/// ```ignore
371/// let cx = otel::span!(^ "background.task", "priority" = "low");
372/// ```
373///
374/// The span remains open until explicitly ended or the underlying span object is dropped.
375/// Use detached spans when:
376///
377/// - Passing context to a spawned task that will manage its own lifetime
378/// - You need manual control over context attachment
379/// - The span lifetime doesn't match lexical scope
380///
381/// ```ignore
382/// async fn spawn_background_work() {
383/// let cx = otel::span!(^ "background.work");
384///
385/// tokio::spawn(async move {
386/// // Attach context in the spawned task
387/// let _guard = cx.attach();
388/// do_background_work().await;
389/// // Span ends when _guard drops here
390/// });
391/// }
392/// ```
393///
394/// # Span Naming Conventions
395///
396/// Use dot-separated hierarchical names describing the component and operation:
397///
398/// ```text
399/// component.operation → scheduler.run, cache.get, db.connect
400/// component.subcomponent.op → lsp.request.send, http.client.fetch
401/// noun.verb → file.read, user.authenticate, order.submit
402/// ```
403///
404/// # Common Mistakes
405///
406/// ## Forgetting to propagate context in async code
407///
408/// ```ignore
409/// // WRONG: Context lost at await point
410/// async fn bad_example() {
411/// otel::span!("operation");
412/// do_work().await; // Span may not be active here!
413/// }
414///
415/// // CORRECT: Use closure form or detached form for async
416/// async fn good_example() {
417/// otel::span!("operation", in |cx| {
418/// do_work().with_context(cx)
419/// }).await;
420/// }
421///
422/// // ALSO CORRECT: Detached form for multiple awaits
423/// async fn also_good() {
424/// let cx = otel::span!(^ "operation");
425/// do_work().with_context(cx).await;
426/// }
427/// ```
428///
429/// ## Using statement form in expression position
430///
431/// ```ignore
432/// // WRONG: Statement form returns (), not a value
433/// fn bad_example() -> Result<Data> {
434/// let result = otel::span!("operation"); // result = ()
435/// get_data()
436/// }
437///
438/// // CORRECT: Use closure form to return values
439/// fn good_example() -> Result<Data> {
440/// otel::span!("operation", in |cx| {
441/// get_data()
442/// })
443/// }
444/// ```
445///
446/// ## Moving context without cloning for concurrent use
447///
448/// ```ignore
449/// // WRONG: cx moved into first iteration
450/// async fn bad_example(ids: Vec<u64>) {
451/// let cx = otel::span!(^ "fetch_all");
452/// for id in ids {
453/// tokio::spawn(fetch(id).with_context(cx)); // cx moved on first iteration!
454/// }
455/// }
456///
457/// // CORRECT: Clone context for each task
458/// async fn good_example(ids: Vec<u64>) {
459/// let cx = otel::span!(^ "fetch_all");
460/// for id in ids {
461/// let cx = cx.clone();
462/// tokio::spawn(async move {
463/// fetch(id).with_context(cx).await
464/// });
465/// }
466/// }
467/// ```
468///
469/// # Requirements
470///
471/// - For variants without `@tracer`: `TRACER` must be in scope (`use crate::TRACER;`)
472/// - For `@tracer` variants: The named tracer must be in scope
473/// - A tracer must have been declared with [`tracer!`]
474#[macro_export]
475macro_rules! span {
476
477 (@maybe_cx $cx:ident, $tracer:ident, $name:expr $(, $($key:literal = $value:expr),+)?) => {
478 let $cx = $crate::span!(@build $tracer, $name $(, $($key = $value),+)?);
479 };
480
481 (@maybe_cx $tracer:ident, $name:expr $(, $($key:literal = $value:expr),+)?) => {
482 let _cx = $crate::span!(@build $tracer, $name $(, $($key = $value),+)?);
483 };
484
485 (@maybe_cx $cx:ident, $tracer:ident, $name:expr, kind = $kind:expr $(, $($key:literal = $value:expr),+)?) => {
486 let $cx = $crate::span!(@build $tracer, $name, kind = $kind $(, $($key = $value),+)?);
487 };
488
489 (@maybe_cx $tracer:ident, $name:expr, kind = $kind:expr $(, $($key:literal = $value:expr),+)?) => {
490 let _cx = $crate::span!(@build $tracer, $name, kind = $kind $(, $($key = $value),+)?);
491 };
492
493 (@maybe_guard $cx:ident, $guard:ident) => {
494 let $guard = $cx.clone().attach();
495 };
496
497 (@maybe_guard $cx:ident $(,)?) => {
498 let _guard = $cx.clone().attach();
499 };
500
501 (@maybe_guard) => {};
502
503
504 // Internal: build span context with automatic attributes and optional custom attributes
505 (@build $tracer:expr, $name:expr $(, $($key:literal = $value:expr),+)?) => {{
506 use opentelemetry::trace::{Tracer as _, TraceContextExt as _};
507 let thread = std::thread::current();
508
509 opentelemetry::Context::current_with_span(
510 $tracer
511 .span_builder($name)
512 .with_attributes([
513 opentelemetry::KeyValue::new("code.namespace", file!()),
514 opentelemetry::KeyValue::new("code.lineno", line!() as i64),
515 opentelemetry::KeyValue::new("code.column", column!() as i64),
516
517 // waiting on (feature = "thread_id_value", issue = "67939")
518 // opentelemetry::KeyValue::new("thread.id", std::thread::current().id()),
519 opentelemetry::KeyValue::new("thread.name", format!("{}",thread.name().unwrap_or("unnamed"))),
520
521 $($(opentelemetry::KeyValue::new($key, $value)),+)?
522 ])
523 .start_with_context(&*$tracer, &opentelemetry::Context::current())
524 )
525 }};
526
527 // Internal: build span context with SpanKind
528 (@build $tracer:expr, $name:expr, kind = $kind:expr $(, $($key:literal = $value:expr),+)?) => {{
529 use opentelemetry::trace::{Tracer as _, TraceContextExt as _};
530 let thread = std::thread::current();
531
532 opentelemetry::Context::current_with_span(
533 $tracer
534 .span_builder($name)
535 .with_kind($kind)
536 .with_attributes([
537 opentelemetry::KeyValue::new("code.namespace", file!()),
538 opentelemetry::KeyValue::new("code.lineno", line!() as i64),
539 opentelemetry::KeyValue::new("code.column", column!() as i64),
540
541 // waiting on (feature = "thread_id_value", issue = "67939")
542 // opentelemetry::KeyValue::new("thread.id", std::thread::current().id()),
543 opentelemetry::KeyValue::new("thread.name", format!("{}",thread.name().unwrap_or("unnamed"))),
544
545 $($(opentelemetry::KeyValue::new($key, $value)),+)?
546 ])
547 .start_with_context(&*$tracer, &opentelemetry::Context::current())
548 )
549 }};
550
551 // Default TRACER with kind and attributes, with closure
552 ($name:expr, kind = $kind:expr, $($key:literal = $value:expr),+, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
553 $crate::span!(@maybe_cx $($cx,)? TRACER, $name, kind = $kind, $($key = $value),+);
554 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
555 $body
556 }};
557
558 // Default TRACER with kind, no attributes, with closure
559 ($name:expr, kind = $kind:expr, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
560 $crate::span!(@maybe_cx $($cx,)? TRACER, $name, kind = $kind);
561 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
562 $body
563 }};
564
565 // Default TRACER, no attributes, with closure
566 ($name:expr, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
567 $crate::span!(@maybe_cx $($cx,)? TRACER, $name);
568 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
569
570 $body
571 }};
572
573 // Default TRACER with attributes and closure
574 ($name:expr, $($key:literal = $value:expr),+, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
575 $crate::span!(@maybe_cx $($cx,)? TRACER, $name , $($key = $value),+);
576 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
577 $body
578 }};
579
580 // Explicit tracer with kind and attributes, with closure
581 (@$tracer:ident, $name:expr, kind = $kind:expr, $($key:literal = $value:expr),+, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
582 $crate::span!(@maybe_cx $($cx,)? $tracer, $name, kind = $kind, $($key = $value),+);
583 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
584 $body
585 }};
586
587 // Explicit tracer with kind, no attributes, with closure
588 (@$tracer:ident, $name:expr, kind = $kind:expr, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
589 $crate::span!(@maybe_cx $($cx,)? $tracer, $name, kind = $kind);
590 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
591 $body
592 }};
593
594 // Explicit tracer, no attributes, with closure
595 (@$tracer:ident, $name:expr, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
596 $crate::span!(@maybe_cx $($cx,)? $tracer, $name);
597 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
598 $body
599 }};
600
601 // Explicit tracer with attributes and closure
602 (@$tracer:ident, $name:expr, $($key:literal = $value:expr),+, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
603 $crate::span!(@maybe_cx $($cx,)? $tracer, $name , $($key = $value),+);
604 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
605
606 $body
607 }};
608
609 // Default TRACER with kind and attributes (statement form with internal guard)
610 ($name:expr, kind = $kind:expr, $($key:literal = $value:expr),+ $(,)?) => {
611 let cx = $crate::span!(@build TRACER, $name, kind = $kind, $($key = $value),+);
612 let _otel_guard = cx.clone().attach();
613 };
614
615 // Default TRACER with kind, no attributes (statement form with internal guard)
616 ($name:expr, kind = $kind:expr $(,)?) => {
617 let cx = $crate::span!(@build TRACER, $name, kind = $kind);
618 let _otel_guard = cx.clone().attach();
619 };
620
621 // Default TRACER, no attributes (statement form with internal guard)
622 ($name:expr $(,)?) => {
623 let cx = $crate::span!(@build TRACER, $name);
624 let _otel_guard = cx.clone().attach();
625 };
626
627 // Default TRACER with attributes (statement form with internal guard)
628 ($name:expr, $($key:literal = $value:expr),+ $(,)?) => {
629 let cx = $crate::span!(@build TRACER, $name, $($key = $value),+);
630 let _otel_guard = cx.clone().attach();
631 };
632
633 // Explicit tracer with kind and attributes (statement form with internal guard)
634 (@$tracer:ident, $name:expr, kind = $kind:expr, $($key:literal = $value:expr),+ $(,)?) => {
635 let cx = $crate::span!(@build $tracer, $name, kind = $kind, $($key = $value),+);
636 let _otel_guard = cx.clone().attach();
637 };
638
639 // Explicit tracer with kind, no attributes (statement form with internal guard)
640 (@$tracer:ident, $name:expr, kind = $kind:expr $(,)?) => {
641 let cx = $crate::span!(@build $tracer, $name, kind = $kind);
642 let _otel_guard = cx.clone().attach();
643 };
644
645 // Explicit tracer, no attributes (statement form with internal guard)
646 (@$tracer:ident, $name:expr $(,)?) => {
647 let cx = $crate::span!(@build $tracer, $name);
648 let _otel_guard = cx.clone().attach();
649 };
650
651 // Explicit tracer with attributes (statement form with internal guard)
652 (@$tracer:ident, $name:expr, $($key:literal = $value:expr),+ $(,)?) => {
653 let cx = $crate::span!(@build $tracer, $name, $($key = $value),+);
654 let _otel_guard = cx.clone().attach();
655 };
656
657 // Detached span (no guard) - default TRACER with kind and attributes
658 (^ $name:expr, kind = $kind:expr, $($key:literal = $value:expr),+ $(,)?) => {
659 $crate::span!(@build TRACER, $name, kind = $kind, $($key = $value),+)
660 };
661
662 // Detached span (no guard) - default TRACER with kind, no attributes
663 (^ $name:expr, kind = $kind:expr $(,)?) => {
664 $crate::span!(@build TRACER, $name, kind = $kind)
665 };
666
667 // Detached span (no guard) - default TRACER, no attributes
668 (^ $name:expr $(,)?) => {
669 $crate::span!(@build TRACER, $name)
670 };
671
672 // Detached span (no guard) - default TRACER with attributes
673 (^ $name:expr, $($key:literal = $value:expr),+ $(,)?) => {
674 $crate::span!(@build TRACER, $name, $($key = $value),+)
675 };
676
677 // Detached span (no guard) - explicit tracer with kind and attributes
678 (^ @$tracer:ident, $name:expr, kind = $kind:expr, $($key:literal = $value:expr),+ $(,)?) => {
679 $crate::span!(@build $tracer, $name, kind = $kind, $($key = $value),+)
680 };
681
682 // Detached span (no guard) - explicit tracer with kind, no attributes
683 (^ @$tracer:ident, $name:expr, kind = $kind:expr $(,)?) => {
684 $crate::span!(@build $tracer, $name, kind = $kind)
685 };
686
687 // Detached span (no guard) - explicit tracer, no attributes
688 (^ @$tracer:ident, $name:expr $(,)?) => {
689 $crate::span!(@build $tracer, $name)
690 };
691
692 // Detached span (no guard) - explicit tracer with attributes
693 (^ @$tracer:ident, $name:expr, $($key:literal = $value:expr),+ $(,)?) => {
694 $crate::span!(@build $tracer, $name, $($key = $value),+)
695 };
696}
697
698/// Records an exception event on the current span and returns an error value.
699///
700/// This macro adds an exception event to the current OpenTelemetry span following
701/// the semantic conventions, then evaluates to the provided error value.
702///
703/// # Syntax
704///
705/// ```ignore
706/// // In closures (ok_or_else, map_err, etc.)
707/// .ok_or_else(|| otel::exception!("invalid_params", Error::new("msg")))?
708///
709/// // In function bodies with explicit return
710/// return Err(otel::exception!("invalid_params", Error::new("msg")));
711///
712/// // With additional attributes
713/// otel::exception!("source_not_found", error, "uri" => uri.to_string())
714/// ```
715///
716/// # Parameters
717///
718/// - `exception_type`: String literal for the exception.type attribute
719/// - `error_value`: The error value to return (must implement Display for exception.message)
720/// - Additional key-value pairs: Optional extra attributes to add to the exception event
721///
722/// # Examples
723///
724/// ```ignore
725/// // In ok_or_else closure
726/// let arg = arguments.first().ok_or_else(|| {
727/// otel::exception!("invalid_params",
728/// jsonrpc::Error::invalid_params("Missing arguments"))
729/// })?;
730///
731/// // In map_err closure
732/// let uri = Uri::parse(uri_str).map_err(|e| {
733/// otel::exception!("invalid_uri",
734/// jsonrpc::Error::invalid_params(format!("Invalid URI: {}", e)),
735/// "uri_str" => uri_str.to_string()
736/// )
737/// })?;
738///
739/// // Direct return
740/// if cache.is_empty() {
741/// return Err(otel::exception!("cache_empty",
742/// jsonrpc::Error::internal_error("Cache is empty"),
743/// "operation" => "lookup"
744/// ));
745/// }
746/// ```
747///
748/// # OpenTelemetry Semantic Conventions
749///
750/// The macro follows the [OpenTelemetry semantic conventions for exceptions](https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/):
751///
752/// - Event name: `"exception"`
753/// - Required attributes:
754/// - `exception.type`: The type/category of the exception
755/// - `exception.message`: The error message extracted via Display
756/// - Additional attributes: Any extra key-value pairs provided
757#[macro_export]
758macro_rules! exception {
759 ($exception_type:expr, $error:expr $(, $key:literal = $value:expr)* $(,)?) => {{
760 use opentelemetry::trace::TraceContextExt as _;
761 let error_value = $error;
762 let error_message = format!("{}", error_value);
763
764 opentelemetry::Context::current().span().add_event(
765 "exception",
766 vec![
767 opentelemetry::KeyValue::new("exception.type", $exception_type),
768 opentelemetry::KeyValue::new("exception.message", error_message),
769 $(opentelemetry::KeyValue::new($key, $value)),*
770 ],
771 );
772
773 error_value
774 }};
775}
776
777/// Records an error event on the current span and returns an error value.
778///
779/// This macro adds an error event to the current OpenTelemetry span following
780/// the semantic conventions, then evaluates to the provided error value.
781///
782/// # Syntax
783///
784/// ```ignore
785/// // In closures (ok_or_else, map_err, etc.)
786/// .ok_or_else(|| otel::error!("timeout", Error::new("msg")))?
787///
788/// // In function bodies with explicit return
789/// return Err(otel::error!("validation_failed", Error::new("msg")));
790///
791/// // With additional attributes
792/// otel::error!("database_unavailable", error, "retry_count" = 3)
793/// ```
794///
795/// # Parameters
796///
797/// - `error_type`: String literal for the error.type attribute
798/// - `error_value`: The error value to return (must implement Display for error.message)
799/// - Additional key-value pairs: Optional extra attributes to add to the error event
800///
801/// # Examples
802///
803/// ```ignore
804/// // In ok_or_else closure
805/// let config = load_config().ok_or_else(|| {
806/// otel::error!("config_not_found",
807/// jsonrpc::Error::internal_error("Configuration file not found"))
808/// })?;
809///
810/// // In map_err closure
811/// let connection = establish_connection().map_err(|e| {
812/// otel::error!("connection_failed",
813/// jsonrpc::Error::internal_error(format!("Failed to connect: {}", e)),
814/// "host" = host.to_string(),
815/// "port" = port as i64
816/// )
817/// })?;
818///
819/// // Direct return
820/// if !is_valid {
821/// return Err(otel::error!("invalid_state",
822/// jsonrpc::Error::internal_error("System is in an invalid state"),
823/// "state" = current_state
824/// ));
825/// }
826/// ```
827///
828/// # OpenTelemetry Semantic Conventions
829///
830/// The macro follows the [OpenTelemetry semantic conventions for errors](https://opentelemetry.io/docs/specs/semconv/attributes-registry/error/):
831///
832/// - Event name: `"error"`
833/// - Required attributes:
834/// - `error.type`: Describes the class of error the operation ended with
835/// - `error.message`: A human-readable message providing more detail about the error
836/// - Additional attributes: Any extra key-value pairs provided
837///
838/// # Difference from `exception!`
839///
840/// While both macros record error-related events, they follow different semantic conventions:
841///
842/// - `error!`: For general error conditions (uses `error.type` and `error.message`)
843/// - `exception!`: For programming exceptions with stack traces (uses `exception.type` and `exception.message`)
844///
845/// Use `error!` for application-level errors and `exception!` for exception handling.
846#[macro_export]
847macro_rules! error {
848 ($error_type:expr, $error:expr $(, $key:literal = $value:expr)* $(,)?) => {{
849 use opentelemetry::trace::TraceContextExt as _;
850 let error_value = $error;
851 let error_message = format!("{}", error_value);
852
853 opentelemetry::Context::current().span().add_event(
854 "error",
855 vec![
856 opentelemetry::KeyValue::new("error.type", $error_type),
857 opentelemetry::KeyValue::new("error.message", error_message),
858 $(opentelemetry::KeyValue::new($key, $value)),*
859 ],
860 );
861
862 error_value
863 }};
864}
865
866/// Adds an event to the current span with optional attributes.
867///
868/// # Examples
869///
870/// ```ignore
871/// // Simple event
872/// otel::event!("query.start");
873///
874/// // Event with attributes
875/// otel::event!("query.result",
876/// "record_count" = 42,
877/// "partition_key" = "users"
878/// );
879/// ```
880#[macro_export]
881macro_rules! event {
882 ($event_name:expr $(, $key:literal = $value:expr)* $(,)?) => {
883 use opentelemetry::trace::TraceContextExt as _;
884 opentelemetry::Context::current().span().add_event(
885 $event_name,
886 vec![
887 $(opentelemetry::KeyValue::new($key, $value)),*
888 ],
889 );
890 };
891}
892
893#[cfg(test)]
894mod tests {
895 use super::*;
896
897 tracer!();
898
899 #[test]
900 fn span_with_closure_returns_value() {
901 let result = span!("test.closure", in |_cx| {
902 42
903 });
904 assert_eq!(result, 42);
905 }
906
907 #[test]
908 fn span_with_closure_and_attributes() {
909 let result = span!("test.closure_attrs", "foo" = "bar", in |_cx| {
910 "hello"
911 });
912 assert_eq!(result, "hello");
913 }
914
915 #[test]
916 fn span_with_closure_explicit_tracer() {
917 let result = span!(@TRACER, "test.explicit", in |_cx| {
918 100
919 });
920 assert_eq!(result, 100);
921 }
922
923 #[test]
924 fn span_with_closure_explicit_tracer_and_attributes() {
925 let result = span!(@TRACER, "test.explicit_attrs", "key" = "value", in |_cx| {
926 vec![1, 2, 3]
927 });
928 assert_eq!(result, vec![1, 2, 3]);
929 }
930}