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 ($name:ident) => {
119 paste::paste! {
120 pub(crate) static [<$name:snake:upper _TRACER>]: std::sync::LazyLock<opentelemetry::global::BoxedTracer> =
121 std::sync::LazyLock::new(|| {
122 use opentelemetry::trace::TracerProvider;
123
124 opentelemetry::global::tracer_provider().tracer_with_scope(
125 opentelemetry::InstrumentationScope::builder(concat!(
126 env!("CARGO_PKG_NAME"),
127 ".", stringify!([<$name:snake>])
128 ))
129 .with_version(env!("CARGO_PKG_VERSION"))
130 .with_schema_url(format!("https://opentelemetry.io/schemas/{}", $crate::OTEL_SPEC_VERSION))
131 .build(),
132 )
133 });
134 }
135 };
136
137 () => {
138 paste::paste! {
139 pub(crate) static TRACER: std::sync::LazyLock<opentelemetry::global::BoxedTracer> =
140 std::sync::LazyLock::new(|| {
141 use opentelemetry::trace::TracerProvider;
142
143 opentelemetry::global::tracer_provider().tracer_with_scope(
144 opentelemetry::InstrumentationScope::builder(concat!(
145 env!("CARGO_PKG_NAME"),
146 ))
147 .with_version(env!("CARGO_PKG_VERSION"))
148 .with_schema_url(format!("https://opentelemetry.io/schemas/{}", $crate::OTEL_SPEC_VERSION))
149 .build(),
150 )
151 });
152 }
153 };
154}
155
156/// Creates an OpenTelemetry span for tracing execution.
157///
158/// This macro creates a span with automatic code location attributes and returns
159/// both the span's [`Context`] and a [`ContextGuard`] that keeps the span active.
160///
161/// # Return Value
162///
163/// Returns `(Context, ContextGuard)`:
164///
165/// - **Context**: The OpenTelemetry context containing the active span. Clone this
166/// to propagate through async boundaries or pass to child operations.
167/// - **ContextGuard**: RAII guard that keeps the context attached to the current thread.
168/// When dropped, the span ends and is exported.
169///
170/// **Important**: The span is only active while the guard is held. Dropping the guard
171/// ends the span, so keep it in scope for the duration of the traced operation.
172///
173/// # Syntax Variants
174///
175/// | Syntax | Returns | Use Case |
176/// |--------|---------|----------|
177/// | `span!("name")` | `()` | Default tracer, creates internal guard |
178/// | `span!("name", "k" = v, ...)` | `()` | With custom attributes, internal guard |
179/// | `span!("name", in \|cx\| { ... })` | `T` | With closure (auto-managed lifetime) |
180/// | `span!("name", "k" = v, in \|cx\| { ... })` | `T` | Closure + attributes |
181/// | `span!(@TRACER, "name")` | `()` | Explicit tracer, internal guard |
182/// | `span!(@TRACER, "name", "k" = v)` | `()` | Explicit tracer + attributes, internal guard |
183/// | `span!(@TRACER, "name", in \|cx\| { ... })` | `T` | Explicit tracer with closure |
184/// | `span!(@TRACER, "name", "k" = v, in \|cx\| { ... })` | `T` | Explicit tracer + attributes + closure |
185/// | `span!(^ "name")` | `Context` | Detached span (no automatic guard) |
186/// | `span!(^ "name", "k" = v)` | `Context` | Detached with attributes |
187/// | `span!(^ @TRACER, "name")` | `Context` | Detached with explicit tracer |
188///
189/// # Automatic Attributes
190///
191/// All spans automatically include:
192///
193/// | Attribute | Description |
194/// |-----------|-------------|
195/// | `code.file.path` | Source file (workspace-relative via [`relative_filepath`]) |
196/// | `code.line.number` | Line number where span was created |
197/// | `code.column.number` | Column number |
198/// | `thread.id` | Current thread ID |
199/// | `thread.name` | Current thread name (or "unnamed") |
200///
201/// # Synchronous Usage
202///
203/// ## Pattern 1: Statement form (automatic guard management)
204///
205/// The simplest form creates an internal guard that manages span lifetime automatically:
206///
207/// ```ignore
208/// fn process_item(item: &Item) {
209/// otel::span!("item.process", "item.id" = item.id);
210///
211/// validate(item);
212/// transform(item);
213/// save(item);
214///
215/// // Span ends automatically at end of scope
216/// }
217/// ```
218///
219/// Nested spans automatically form parent-child relationships:
220///
221/// ```ignore
222/// fn process_batch(items: &[Item]) {
223/// otel::span!("batch.process", "count" = items.len() as i64);
224///
225/// for item in items {
226/// // Automatically becomes a child of "batch.process"
227/// otel::span!("item.process", "item.id" = item.id);
228/// process_item(item);
229/// }
230/// }
231/// ```
232///
233/// ## Pattern 2: Closure-based spans
234///
235/// Use `in` closure syntax for automatic span lifetime management:
236///
237/// ```ignore
238/// use crate::TRACER;
239///
240/// fn process_item(item: &Item) -> Result<ProcessedItem> {
241/// otel::span!("item.process", "item.id" = item.id, in |cx| {
242/// validate(item)?;
243/// let transformed = transform(item)?;
244/// save(&transformed)?;
245/// Ok(transformed)
246/// })
247/// }
248/// ```
249///
250/// The closure receives the `Context` as a parameter and can return any value.
251/// The span ends automatically when the closure completes or panics:
252///
253/// ```ignore
254/// // Simple computation
255/// let result = otel::span!("compute", in |cx| {
256/// expensive_calculation()
257/// });
258///
259/// // With attributes
260/// let user = otel::span!("db.fetch", "user.id" = user_id, in |cx| {
261/// db.get_user(user_id)
262/// })?;
263///
264/// // Explicit tracer
265/// let data = otel::span!(@CUSTOM_TRACER, "custom.operation", in |cx| {
266/// do_work()
267/// });
268/// ```
269///
270/// # Asynchronous Usage
271///
272/// OpenTelemetry context is stored in thread-local storage. Since async tasks can
273/// migrate between threads at `.await` points, you must explicitly propagate context
274/// using [`FutureExt::with_context`].
275///
276/// ## Pattern 1: Closure form for single async operation
277///
278/// ```ignore
279/// use opentelemetry::trace::FutureExt;
280///
281/// async fn fetch_user(id: u64) -> Result<User> {
282/// otel::span!("user.fetch", "user.id" = id as i64, in |cx| {
283/// db.get_user(id)
284/// .with_context(cx)
285/// }).await
286/// }
287/// ```
288///
289/// ## Pattern 2: Detached form for sequential awaits
290///
291/// Use the detached form (`@detached`) to get a context variable for multiple await points:
292///
293/// ```ignore
294/// use opentelemetry::trace::FutureExt;
295///
296/// async fn process_order(order_id: u64) -> Result<()> {
297/// let cx = otel::span!(^ "order.process", "order.id" = order_id as i64);
298///
299/// let order = fetch_order(order_id)
300/// .with_context(cx.clone())
301/// .await?;
302///
303/// validate_order(&order)
304/// .with_context(cx.clone())
305/// .await?;
306///
307/// submit_order(&order)
308/// .with_context(cx) // Last use, no clone needed
309/// .await
310/// }
311/// ```
312///
313/// ## Pattern 3: Concurrent/spawned tasks
314///
315/// Use detached form and clone the context for each spawned task:
316///
317/// ```ignore
318/// use opentelemetry::trace::FutureExt;
319///
320/// async fn fetch_all(ids: Vec<u64>) -> Vec<Result<Item>> {
321/// let cx = otel::span!(^ "items.fetch_all", "count" = ids.len() as i64);
322///
323/// let futures: Vec<_> = ids
324/// .into_iter()
325/// .map(|id| {
326/// let cx = cx.clone();
327/// async move {
328/// fetch_item(id)
329/// .with_context(cx)
330/// .await
331/// }
332/// })
333/// .collect();
334///
335/// futures::future::join_all(futures).await
336/// }
337/// ```
338///
339/// ## Pattern 4: Child spans in async blocks
340///
341/// Use detached form for parent span, then create child spans inside async blocks:
342///
343/// ```ignore
344/// use opentelemetry::trace::FutureExt;
345///
346/// async fn pipeline() -> Result<()> {
347/// let cx = otel::span!(^ "pipeline.run");
348///
349/// async {
350/// otel::span!("pipeline.phase1");
351/// do_phase1().await
352/// }
353/// .with_context(cx.clone())
354/// .await?;
355///
356/// async {
357/// otel::span!("pipeline.phase2");
358/// do_phase2().await
359/// }
360/// .with_context(cx)
361/// .await
362/// }
363/// ```
364///
365/// # Detached Spans
366///
367/// Use `@detached` to create a span that returns only the `Context`, without a guard:
368///
369/// ```ignore
370/// let cx = otel::span!(^ "background.task", "priority" = "low");
371/// ```
372///
373/// The span remains open until explicitly ended or the underlying span object is dropped.
374/// Use detached spans when:
375///
376/// - Passing context to a spawned task that will manage its own lifetime
377/// - You need manual control over context attachment
378/// - The span lifetime doesn't match lexical scope
379///
380/// ```ignore
381/// async fn spawn_background_work() {
382/// let cx = otel::span!(^ "background.work");
383///
384/// tokio::spawn(async move {
385/// // Attach context in the spawned task
386/// let _guard = cx.attach();
387/// do_background_work().await;
388/// // Span ends when _guard drops here
389/// });
390/// }
391/// ```
392///
393/// # Span Naming Conventions
394///
395/// Use dot-separated hierarchical names describing the component and operation:
396///
397/// ```text
398/// component.operation → scheduler.run, cache.get, db.connect
399/// component.subcomponent.op → lsp.request.send, http.client.fetch
400/// noun.verb → file.read, user.authenticate, order.submit
401/// ```
402///
403/// # Common Mistakes
404///
405/// ## Forgetting to propagate context in async code
406///
407/// ```ignore
408/// // WRONG: Context lost at await point
409/// async fn bad_example() {
410/// otel::span!("operation");
411/// do_work().await; // Span may not be active here!
412/// }
413///
414/// // CORRECT: Use closure form or detached form for async
415/// async fn good_example() {
416/// otel::span!("operation", in |cx| {
417/// do_work().with_context(cx)
418/// }).await;
419/// }
420///
421/// // ALSO CORRECT: Detached form for multiple awaits
422/// async fn also_good() {
423/// let cx = otel::span!(^ "operation");
424/// do_work().with_context(cx).await;
425/// }
426/// ```
427///
428/// ## Using statement form in expression position
429///
430/// ```ignore
431/// // WRONG: Statement form returns (), not a value
432/// fn bad_example() -> Result<Data> {
433/// let result = otel::span!("operation"); // result = ()
434/// get_data()
435/// }
436///
437/// // CORRECT: Use closure form to return values
438/// fn good_example() -> Result<Data> {
439/// otel::span!("operation", in |cx| {
440/// get_data()
441/// })
442/// }
443/// ```
444///
445/// ## Moving context without cloning for concurrent use
446///
447/// ```ignore
448/// // WRONG: cx moved into first iteration
449/// async fn bad_example(ids: Vec<u64>) {
450/// let cx = otel::span!(^ "fetch_all");
451/// for id in ids {
452/// tokio::spawn(fetch(id).with_context(cx)); // cx moved on first iteration!
453/// }
454/// }
455///
456/// // CORRECT: Clone context for each task
457/// async fn good_example(ids: Vec<u64>) {
458/// let cx = otel::span!(^ "fetch_all");
459/// for id in ids {
460/// let cx = cx.clone();
461/// tokio::spawn(async move {
462/// fetch(id).with_context(cx).await
463/// });
464/// }
465/// }
466/// ```
467///
468/// # Requirements
469///
470/// - For variants without `@tracer`: `TRACER` must be in scope (`use crate::TRACER;`)
471/// - For `@tracer` variants: The named tracer must be in scope
472/// - A tracer must have been declared with [`tracer!`]
473#[macro_export]
474macro_rules! span {
475 // -------------------------------------------------------------------------
476 // @build rules - 3 variants for context handling
477 // -------------------------------------------------------------------------
478 //
479 // Keyword order: kind → context → links → attrs
480 //
481 // Note: We need 3 @build rules because $($ctx:tt)+ is greedy and would
482 // consume tokens beyond the context expression. Using $ctx:expr is bounded.
483
484 // @build with context = None (literal match, must come first)
485 (@build $tracer:expr, $name:expr
486 $(, kind = $kind:expr)?
487 , context = None
488 $(, links = [$($link:expr),* $(,)?])?
489 $(, $($key:literal = $value:expr),+)?) => {{
490 use opentelemetry::trace::{Tracer as _, TraceContextExt};
491 let thread = std::thread::current();
492 let parent_cx = opentelemetry::Context::new();
493
494 TraceContextExt::with_span(
495 &parent_cx,
496 $tracer
497 .span_builder($name)
498 $(.with_kind($kind))?
499 $(.with_links(vec![$($link),*]))?
500 .with_attributes([
501 opentelemetry::KeyValue::new("code.namespace", file!()),
502 opentelemetry::KeyValue::new("code.lineno", line!() as i64),
503 opentelemetry::KeyValue::new("code.column", column!() as i64),
504 opentelemetry::KeyValue::new("thread.name", format!("{}", thread.name().unwrap_or("unnamed"))),
505 $($(opentelemetry::KeyValue::new($key, $value)),+)?
506 ])
507 .start_with_context(&*$tracer, &parent_cx)
508 )
509 }};
510
511 // @build with context = $ctx:expr (expression match)
512 (@build $tracer:expr, $name:expr
513 $(, kind = $kind:expr)?
514 , context = $ctx:expr
515 $(, links = [$($link:expr),* $(,)?])?
516 $(, $($key:literal = $value:expr),+)?) => {{
517 use opentelemetry::trace::{Tracer as _, TraceContextExt};
518 let thread = std::thread::current();
519 let parent_cx = $ctx;
520
521 TraceContextExt::with_span(
522 &parent_cx,
523 $tracer
524 .span_builder($name)
525 $(.with_kind($kind))?
526 $(.with_links(vec![$($link),*]))?
527 .with_attributes([
528 opentelemetry::KeyValue::new("code.namespace", file!()),
529 opentelemetry::KeyValue::new("code.lineno", line!() as i64),
530 opentelemetry::KeyValue::new("code.column", column!() as i64),
531 opentelemetry::KeyValue::new("thread.name", format!("{}", thread.name().unwrap_or("unnamed"))),
532 $($(opentelemetry::KeyValue::new($key, $value)),+)?
533 ])
534 .start_with_context(&*$tracer, &parent_cx)
535 )
536 }};
537
538 // @build without context (uses current context)
539 (@build $tracer:expr, $name:expr
540 $(, kind = $kind:expr)?
541 $(, links = [$($link:expr),* $(,)?])?
542 $(, $($key:literal = $value:expr),+)?) => {{
543 use opentelemetry::trace::{Tracer as _, TraceContextExt};
544 let thread = std::thread::current();
545 let parent_cx = opentelemetry::Context::current();
546
547 TraceContextExt::with_span(
548 &parent_cx,
549 $tracer
550 .span_builder($name)
551 $(.with_kind($kind))?
552 $(.with_links(vec![$($link),*]))?
553 .with_attributes([
554 opentelemetry::KeyValue::new("code.namespace", file!()),
555 opentelemetry::KeyValue::new("code.lineno", line!() as i64),
556 opentelemetry::KeyValue::new("code.column", column!() as i64),
557 opentelemetry::KeyValue::new("thread.name", format!("{}", thread.name().unwrap_or("unnamed"))),
558 $($(opentelemetry::KeyValue::new($key, $value)),+)?
559 ])
560 .start_with_context(&*$tracer, &parent_cx)
561 )
562 }};
563
564 // -------------------------------------------------------------------------
565 // CONSOLIDATED USER-FACING RULES
566 // -------------------------------------------------------------------------
567 //
568 // Keyword order: kind → context → links → attrs
569 //
570 // Note: Closure forms need separate rules for context = None, context = $expr,
571 // and no context, to avoid ambiguity between $($ctx:tt)+ and ", in |...|"
572 // -------------------------------------------------------------------------
573
574 // -------------------------------------------------------------------------
575 // CLOSURE FORMS - with context = None (literal match, must come first)
576 // -------------------------------------------------------------------------
577
578 // Default TRACER with context = None
579 ($name:expr
580 $(, kind = $kind:expr)?
581 , context = None
582 $(, links = [$($link:expr),* $(,)?])?
583 $(, $($key:literal = $value:expr),+)?
584 , in |$($cx_var:ident)? $(,$guard:ident)?| $body:expr) => {{
585 let cx = $crate::span!(@build TRACER, $name
586 $(, kind = $kind)?
587 , context = None
588 $(, links = [$($link),*])?
589 $(, $($key = $value),+)?);
590 let _guard = cx.clone().attach();
591 $(let $cx_var = cx.clone();)?
592 $(let $guard = _guard;)?
593 $body
594 }};
595
596 // Explicit @TRACER with context = None
597 (@$tracer:ident, $name:expr
598 $(, kind = $kind:expr)?
599 , context = None
600 $(, links = [$($link:expr),* $(,)?])?
601 $(, $($key:literal = $value:expr),+)?
602 , in |$($cx_var:ident)? $(,$guard:ident)?| $body:expr) => {{
603 let cx = $crate::span!(@build $tracer, $name
604 $(, kind = $kind)?
605 , context = None
606 $(, links = [$($link),*])?
607 $(, $($key = $value),+)?);
608 let _guard = cx.clone().attach();
609 $(let $cx_var = cx.clone();)?
610 $(let $guard = _guard;)?
611 $body
612 }};
613
614 // -------------------------------------------------------------------------
615 // CLOSURE FORMS - with explicit context expression
616 // -------------------------------------------------------------------------
617
618 // Default TRACER with explicit context
619 ($name:expr
620 $(, kind = $kind:expr)?
621 , context = $ctx:expr
622 $(, links = [$($link:expr),* $(,)?])?
623 $(, $($key:literal = $value:expr),+)?
624 , in |$($cx_var:ident)? $(,$guard:ident)?| $body:expr) => {{
625 let cx = $crate::span!(@build TRACER, $name
626 $(, kind = $kind)?
627 , context = $ctx
628 $(, links = [$($link),*])?
629 $(, $($key = $value),+)?);
630 let _guard = cx.clone().attach();
631 $(let $cx_var = cx.clone();)?
632 $(let $guard = _guard;)?
633 $body
634 }};
635
636 // Explicit @TRACER with explicit context
637 (@$tracer:ident, $name:expr
638 $(, kind = $kind:expr)?
639 , context = $ctx:expr
640 $(, links = [$($link:expr),* $(,)?])?
641 $(, $($key:literal = $value:expr),+)?
642 , in |$($cx_var:ident)? $(,$guard:ident)?| $body:expr) => {{
643 let cx = $crate::span!(@build $tracer, $name
644 $(, kind = $kind)?
645 , context = $ctx
646 $(, links = [$($link),*])?
647 $(, $($key = $value),+)?);
648 let _guard = cx.clone().attach();
649 $(let $cx_var = cx.clone();)?
650 $(let $guard = _guard;)?
651 $body
652 }};
653
654 // -------------------------------------------------------------------------
655 // CLOSURE FORMS - without context (uses current)
656 // -------------------------------------------------------------------------
657
658 // Default TRACER without context
659 ($name:expr
660 $(, kind = $kind:expr)?
661 $(, links = [$($link:expr),* $(,)?])?
662 $(, $($key:literal = $value:expr),+)?
663 , in |$($cx_var:ident)? $(,$guard:ident)?| $body:expr) => {{
664 let cx = $crate::span!(@build TRACER, $name
665 $(, kind = $kind)?
666 $(, links = [$($link),*])?
667 $(, $($key = $value),+)?);
668 let _guard = cx.clone().attach();
669 $(let $cx_var = cx.clone();)?
670 $(let $guard = _guard;)?
671 $body
672 }};
673
674 // Explicit @TRACER without context
675 (@$tracer:ident, $name:expr
676 $(, kind = $kind:expr)?
677 $(, links = [$($link:expr),* $(,)?])?
678 $(, $($key:literal = $value:expr),+)?
679 , in |$($cx_var:ident)? $(,$guard:ident)?| $body:expr) => {{
680 let cx = $crate::span!(@build $tracer, $name
681 $(, kind = $kind)?
682 $(, links = [$($link),*])?
683 $(, $($key = $value),+)?);
684 let _guard = cx.clone().attach();
685 $(let $cx_var = cx.clone();)?
686 $(let $guard = _guard;)?
687 $body
688 }};
689
690 // -------------------------------------------------------------------------
691 // DETACHED FORM - context = None (literal match, must come first)
692 // -------------------------------------------------------------------------
693
694 // Default TRACER with context = None
695 (^ $name:expr
696 $(, kind = $kind:expr)?
697 , context = None
698 $(, links = [$($link:expr),* $(,)?])?
699 $(, $($key:literal = $value:expr),+)?
700 $(,)?) => {
701 $crate::span!(@build TRACER, $name
702 $(, kind = $kind)?
703 , context = None
704 $(, links = [$($link),*])?
705 $(, $($key = $value),+)?)
706 };
707
708 // Explicit @TRACER with context = None
709 (^ @$tracer:ident, $name:expr
710 $(, kind = $kind:expr)?
711 , context = None
712 $(, links = [$($link:expr),* $(,)?])?
713 $(, $($key:literal = $value:expr),+)?
714 $(,)?) => {
715 $crate::span!(@build $tracer, $name
716 $(, kind = $kind)?
717 , context = None
718 $(, links = [$($link),*])?
719 $(, $($key = $value),+)?)
720 };
721
722 // -------------------------------------------------------------------------
723 // DETACHED FORM - context = $ctx:expr (expression match)
724 // -------------------------------------------------------------------------
725
726 // Default TRACER with explicit context
727 (^ $name:expr
728 $(, kind = $kind:expr)?
729 , context = $ctx:expr
730 $(, links = [$($link:expr),* $(,)?])?
731 $(, $($key:literal = $value:expr),+)?
732 $(,)?) => {
733 $crate::span!(@build TRACER, $name
734 $(, kind = $kind)?
735 , context = $ctx
736 $(, links = [$($link),*])?
737 $(, $($key = $value),+)?)
738 };
739
740 // Explicit @TRACER with explicit context
741 (^ @$tracer:ident, $name:expr
742 $(, kind = $kind:expr)?
743 , context = $ctx:expr
744 $(, links = [$($link:expr),* $(,)?])?
745 $(, $($key:literal = $value:expr),+)?
746 $(,)?) => {
747 $crate::span!(@build $tracer, $name
748 $(, kind = $kind)?
749 , context = $ctx
750 $(, links = [$($link),*])?
751 $(, $($key = $value),+)?)
752 };
753
754 // -------------------------------------------------------------------------
755 // DETACHED FORM - without context (uses current)
756 // -------------------------------------------------------------------------
757
758 // Default TRACER without context
759 (^ $name:expr
760 $(, kind = $kind:expr)?
761 $(, links = [$($link:expr),* $(,)?])?
762 $(, $($key:literal = $value:expr),+)?
763 $(,)?) => {
764 $crate::span!(@build TRACER, $name
765 $(, kind = $kind)?
766 $(, links = [$($link),*])?
767 $(, $($key = $value),+)?)
768 };
769
770 // Explicit @TRACER without context
771 (^ @$tracer:ident, $name:expr
772 $(, kind = $kind:expr)?
773 $(, links = [$($link:expr),* $(,)?])?
774 $(, $($key:literal = $value:expr),+)?
775 $(,)?) => {
776 $crate::span!(@build $tracer, $name
777 $(, kind = $kind)?
778 $(, links = [$($link),*])?
779 $(, $($key = $value),+)?)
780 };
781
782 // -------------------------------------------------------------------------
783 // STATEMENT FORM - context = None (literal match, must come first)
784 // -------------------------------------------------------------------------
785
786 // Default TRACER with context = None
787 ($name:expr
788 $(, kind = $kind:expr)?
789 , context = None
790 $(, links = [$($link:expr),* $(,)?])?
791 $(, $($key:literal = $value:expr),+)?
792 $(,)?) => {
793 let cx = $crate::span!(@build TRACER, $name
794 $(, kind = $kind)?
795 , context = None
796 $(, links = [$($link),*])?
797 $(, $($key = $value),+)?);
798 let _otel_guard = cx.clone().attach();
799 };
800
801 // Explicit @TRACER with context = None
802 (@$tracer:ident, $name:expr
803 $(, kind = $kind:expr)?
804 , context = None
805 $(, links = [$($link:expr),* $(,)?])?
806 $(, $($key:literal = $value:expr),+)?
807 $(,)?) => {
808 let cx = $crate::span!(@build $tracer, $name
809 $(, kind = $kind)?
810 , context = None
811 $(, links = [$($link),*])?
812 $(, $($key = $value),+)?);
813 let _otel_guard = cx.clone().attach();
814 };
815
816 // =------------------------------------------------------------------------- // STATEMENT FORM - context = $ctx:expr (expression match)
817 // -------------------------------------------------------------------------
818
819 // Default TRACER with explicit context
820 ($name:expr
821 $(, kind = $kind:expr)?
822 , context = $ctx:expr
823 $(, links = [$($link:expr),* $(,)?])?
824 $(, $($key:literal = $value:expr),+)?
825 $(,)?) => {
826 let cx = $crate::span!(@build TRACER, $name
827 $(, kind = $kind)?
828 , context = $ctx
829 $(, links = [$($link),*])?
830 $(, $($key = $value),+)?);
831 let _otel_guard = cx.clone().attach();
832 };
833
834 // Explicit @TRACER with explicit context
835 (@$tracer:ident, $name:expr
836 $(, kind = $kind:expr)?
837 , context = $ctx:expr
838 $(, links = [$($link:expr),* $(,)?])?
839 $(, $($key:literal = $value:expr),+)?
840 $(,)?) => {
841 let cx = $crate::span!(@build $tracer, $name
842 $(, kind = $kind)?
843 , context = $ctx
844 $(, links = [$($link),*])?
845 $(, $($key = $value),+)?);
846 let _otel_guard = cx.clone().attach();
847 };
848
849 // -------------------------------------------------------------------------
850 // STATEMENT FORM - without context (uses current)
851 // -------------------------------------------------------------------------
852
853 // Default TRACER without context
854 ($name:expr
855 $(, kind = $kind:expr)?
856 $(, links = [$($link:expr),* $(,)?])?
857 $(, $($key:literal = $value:expr),+)?
858 $(,)?) => {
859 let cx = $crate::span!(@build TRACER, $name
860 $(, kind = $kind)?
861 $(, links = [$($link),*])?
862 $(, $($key = $value),+)?);
863 let _otel_guard = cx.clone().attach();
864 };
865
866 // Explicit @TRACER without context
867 (@$tracer:ident, $name:expr
868 $(, kind = $kind:expr)?
869 $(, links = [$($link:expr),* $(,)?])?
870 $(, $($key:literal = $value:expr),+)?
871 $(,)?) => {
872 let cx = $crate::span!(@build $tracer, $name
873 $(, kind = $kind)?
874 $(, links = [$($link),*])?
875 $(, $($key = $value),+)?);
876 let _otel_guard = cx.clone().attach();
877 };
878}
879
880/// Records an exception event on the current span and returns an error value.
881///
882/// This macro adds an exception event to the current OpenTelemetry span following
883/// the semantic conventions, then evaluates to the provided error value.
884///
885/// # Syntax
886///
887/// ```ignore
888/// // In closures (ok_or_else, map_err, etc.)
889/// .ok_or_else(|| otel::exception!("invalid_params", Error::new("msg")))?
890///
891/// // In function bodies with explicit return
892/// return Err(otel::exception!("invalid_params", Error::new("msg")));
893///
894/// // With additional attributes
895/// otel::exception!("source_not_found", error, "uri" => uri.to_string())
896/// ```
897///
898/// # Parameters
899///
900/// - `exception_type`: String literal for the exception.type attribute
901/// - `error_value`: The error value to return (must implement Display for exception.message)
902/// - Additional key-value pairs: Optional extra attributes to add to the exception event
903///
904/// # Examples
905///
906/// ```ignore
907/// // In ok_or_else closure
908/// let arg = arguments.first().ok_or_else(|| {
909/// otel::exception!("invalid_params",
910/// jsonrpc::Error::invalid_params("Missing arguments"))
911/// })?;
912///
913/// // In map_err closure
914/// let uri = Uri::parse(uri_str).map_err(|e| {
915/// otel::exception!("invalid_uri",
916/// jsonrpc::Error::invalid_params(format!("Invalid URI: {}", e)),
917/// "uri_str" => uri_str.to_string()
918/// )
919/// })?;
920///
921/// // Direct return
922/// if cache.is_empty() {
923/// return Err(otel::exception!("cache_empty",
924/// jsonrpc::Error::internal_error("Cache is empty"),
925/// "operation" => "lookup"
926/// ));
927/// }
928/// ```
929///
930/// # OpenTelemetry Semantic Conventions
931///
932/// The macro follows the [OpenTelemetry semantic conventions for exceptions](https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/):
933///
934/// - Event name: `"exception"`
935/// - Required attributes:
936/// - `exception.type`: The type/category of the exception
937/// - `exception.message`: The error message extracted via Display
938/// - Additional attributes: Any extra key-value pairs provided
939#[macro_export]
940macro_rules! exception {
941 ($exception_type:expr, $error:expr $(, $key:literal = $value:expr)* $(,)?) => {{
942 use opentelemetry::trace::TraceContextExt as _;
943 let error_value = $error;
944 let error_message = format!("{}", error_value);
945
946 opentelemetry::Context::current().span().add_event(
947 "exception",
948 vec![
949 opentelemetry::KeyValue::new("exception.type", $exception_type),
950 opentelemetry::KeyValue::new("exception.message", error_message),
951 $(opentelemetry::KeyValue::new($key, $value)),*
952 ],
953 );
954
955 error_value
956 }};
957}
958
959/// Records an error event on the current span and returns an error value.
960///
961/// This macro adds an error event to the current OpenTelemetry span following
962/// the semantic conventions, then evaluates to the provided error value.
963///
964/// # Syntax
965///
966/// ```ignore
967/// // In closures (ok_or_else, map_err, etc.)
968/// .ok_or_else(|| otel::error!("timeout", Error::new("msg")))?
969///
970/// // In function bodies with explicit return
971/// return Err(otel::error!("validation_failed", Error::new("msg")));
972///
973/// // With additional attributes
974/// otel::error!("database_unavailable", error, "retry_count" = 3)
975/// ```
976///
977/// # Parameters
978///
979/// - `error_type`: String literal for the error.type attribute
980/// - `error_value`: The error value to return (must implement Display for error.message)
981/// - Additional key-value pairs: Optional extra attributes to add to the error event
982///
983/// # Examples
984///
985/// ```ignore
986/// // In ok_or_else closure
987/// let config = load_config().ok_or_else(|| {
988/// otel::error!("config_not_found",
989/// jsonrpc::Error::internal_error("Configuration file not found"))
990/// })?;
991///
992/// // In map_err closure
993/// let connection = establish_connection().map_err(|e| {
994/// otel::error!("connection_failed",
995/// jsonrpc::Error::internal_error(format!("Failed to connect: {}", e)),
996/// "host" = host.to_string(),
997/// "port" = port as i64
998/// )
999/// })?;
1000///
1001/// // Direct return
1002/// if !is_valid {
1003/// return Err(otel::error!("invalid_state",
1004/// jsonrpc::Error::internal_error("System is in an invalid state"),
1005/// "state" = current_state
1006/// ));
1007/// }
1008/// ```
1009///
1010/// # OpenTelemetry Semantic Conventions
1011///
1012/// The macro follows the [OpenTelemetry semantic conventions for errors](https://opentelemetry.io/docs/specs/semconv/attributes-registry/error/):
1013///
1014/// - Event name: `"error"`
1015/// - Required attributes:
1016/// - `error.type`: Describes the class of error the operation ended with
1017/// - `error.message`: A human-readable message providing more detail about the error
1018/// - Additional attributes: Any extra key-value pairs provided
1019///
1020/// # Difference from `exception!`
1021///
1022/// While both macros record error-related events, they follow different semantic conventions:
1023///
1024/// - `error!`: For general error conditions (uses `error.type` and `error.message`)
1025/// - `exception!`: For programming exceptions with stack traces (uses `exception.type` and `exception.message`)
1026///
1027/// Use `error!` for application-level errors and `exception!` for exception handling.
1028#[macro_export]
1029macro_rules! error {
1030 ($error_type:expr, $error:expr $(, $key:literal = $value:expr)* $(,)?) => {{
1031 use opentelemetry::trace::TraceContextExt as _;
1032 let error_value = $error;
1033 let error_message = format!("{}", error_value);
1034
1035 opentelemetry::Context::current().span().add_event(
1036 "error",
1037 vec![
1038 opentelemetry::KeyValue::new("error.type", $error_type),
1039 opentelemetry::KeyValue::new("error.message", error_message),
1040 $(opentelemetry::KeyValue::new($key, $value)),*
1041 ],
1042 );
1043
1044 error_value
1045 }};
1046}
1047
1048/// Adds an event to the current span with optional attributes.
1049///
1050/// # Examples
1051///
1052/// ```ignore
1053/// // Simple event
1054/// otel::event!("query.start");
1055///
1056/// // Event with attributes
1057/// otel::event!("query.result",
1058/// "record_count" = 42,
1059/// "partition_key" = "users"
1060/// );
1061/// ```
1062#[macro_export]
1063macro_rules! event {
1064 ($event_name:expr $(, $key:literal = $value:expr)* $(,)?) => {{
1065 #[allow(unused)]
1066 use opentelemetry::trace::TraceContextExt as _;
1067 opentelemetry::Context::current().span().add_event(
1068 $event_name,
1069 vec![
1070 $(opentelemetry::KeyValue::new($key, $value)),*
1071 ],
1072 );
1073 }};
1074}
1075
1076/// Creates a span [`Link`](opentelemetry::trace::Link) from a context for use with `span!` macro's `links` parameter.
1077///
1078/// Links are used to associate spans across trace boundaries when there's a causal
1079/// relationship but not a parent-child relationship. Common use cases include:
1080///
1081/// - Background tasks spawned from a request (linked to spawning context)
1082/// - Batch processing where one span triggers multiple independent operations
1083/// - Fan-out/fan-in patterns
1084///
1085/// # Syntax
1086///
1087/// ```ignore
1088/// // Link from current context
1089/// let link = otel::link!();
1090///
1091/// // Link from explicit context
1092/// let link = otel::link!(parent_cx);
1093///
1094/// // Link with attributes
1095/// let link = otel::link!(parent_cx, "link.type" = "spawned_from");
1096/// ```
1097///
1098/// # Examples
1099///
1100/// ## Background task with link to spawner
1101///
1102/// ```ignore
1103/// fn spawn_background_task(data: Data) {
1104/// // Capture link BEFORE spawning (while still in parent context)
1105/// let parent_link = otel::link!("link.type" = "spawned_from");
1106///
1107/// tokio::spawn(async move {
1108/// // New root trace, linked back to spawner
1109/// otel::span!("background.task", context = None, links = [parent_link], in |_cx| {
1110/// process(data).await
1111/// })
1112/// });
1113/// }
1114/// ```
1115///
1116/// ## Explicit context with attributes
1117///
1118/// ```ignore
1119/// fn process_batch(items: Vec<Item>, trigger_cx: Context) {
1120/// let trigger_link = otel::link!(trigger_cx, "link.type" = "batch_trigger");
1121///
1122/// for item in items {
1123/// // Each item gets its own trace, linked to the batch trigger
1124/// otel::span!("item.process", context = None, links = [trigger_link.clone()], in |_cx| {
1125/// process_item(item)
1126/// });
1127/// }
1128/// }
1129/// ```
1130///
1131/// # OpenTelemetry Semantic Conventions
1132///
1133/// Links follow the [OpenTelemetry link specification](https://opentelemetry.io/docs/concepts/signals/traces/#span-links).
1134/// The `link.type` attribute is commonly used to describe the relationship.
1135#[macro_export]
1136macro_rules! link {
1137 // Link from current context (no attributes)
1138 () => {{
1139 use opentelemetry::trace::TraceContextExt as _;
1140 let cx = opentelemetry::Context::current();
1141 opentelemetry::trace::Link::new(cx.span().span_context().clone(), vec![], 0)
1142 }};
1143
1144 // Link from current context with attributes
1145 ($($key:literal = $value:expr),+ $(,)?) => {{
1146 use opentelemetry::trace::TraceContextExt as _;
1147 let cx = opentelemetry::Context::current();
1148 opentelemetry::trace::Link::new(
1149 cx.span().span_context().clone(),
1150 vec![$(opentelemetry::KeyValue::new($key, $value)),+],
1151 0
1152 )
1153 }};
1154
1155 // Link from explicit context (no attributes)
1156 ($cx:expr) => {{
1157 use opentelemetry::trace::TraceContextExt as _;
1158 opentelemetry::trace::Link::new($cx.span().span_context().clone(), vec![], 0)
1159 }};
1160
1161 // Link from explicit context with attributes
1162 ($cx:expr, $($key:literal = $value:expr),+ $(,)?) => {{
1163 use opentelemetry::trace::TraceContextExt as _;
1164 opentelemetry::trace::Link::new(
1165 $cx.span().span_context().clone(),
1166 vec![$(opentelemetry::KeyValue::new($key, $value)),+],
1167 0
1168 )
1169 }};
1170}
1171
1172#[cfg(test)]
1173mod tests;