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!(@detached, "name")` | `Context` | Detached span (no automatic guard) |
187/// | `span!(@detached, "name", "k" => v)` | `Context` | Detached with attributes |
188/// | `span!(@detached, @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!(@detached, "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!(@detached, "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!(@detached, "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!(@detached, "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!(@detached, "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!(@detached, "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!(@detached, "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!(@detached, "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:expr => $value:expr),+)?) => {
478 let $cx = $crate::span!(@build $tracer, $name $(, $($key => $value),+)?);
479 };
480 (@maybe_cx $tracer:ident, $name:expr $(, $($key:expr => $value:expr),+)?) => {
481 let _cx = $crate::span!(@build $tracer, $name $(, $($key => $value),+)?);
482 };
483
484 (@maybe_guard $cx:ident, $guard:ident) => {
485 let $guard = $cx.clone().attach();
486 };
487 (@maybe_guard $cx:ident,) => {};
488 (@maybe_guard $cx:ident) => {};
489 (@maybe_guard) => {};
490
491
492 // Internal: build span context with automatic attributes and optional custom attributes
493 (@build $tracer:expr, $name:expr $(, $($key:expr => $value:expr),+)?) => {{
494 use opentelemetry::trace::{Tracer as _, TraceContextExt as _};
495 let thread = std::thread::current();
496
497 opentelemetry::Context::current_with_span(
498 $tracer
499 .span_builder($name)
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
505 // waiting on (feature = "thread_id_value", issue = "67939")
506 // opentelemetry::KeyValue::new("thread.id", std::thread::current().id()),
507 opentelemetry::KeyValue::new("thread.name", format!("{}",thread.name().unwrap_or("unnamed"))),
508
509 $($(opentelemetry::KeyValue::new($key, $value)),+)?
510 ])
511 .start_with_context(&*$tracer, &opentelemetry::Context::current())
512 )
513 }};
514
515 // Default TRACER, no attributes, with closure
516 ($name:expr, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
517 $crate::span!(@maybe_cx $($cx,)? TRACER, $name);
518 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
519
520 $body
521 }};
522
523 // Default TRACER with attributes and closure
524 ($name:expr, $($key:expr => $value:expr),+, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
525 $crate::span!(@maybe_cx $($cx,)? TRACER, $name , $($key => $value),+);
526 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
527 $body
528 }};
529
530 // Explicit tracer, no attributes, with closure
531 (@$tracer:ident, $name:expr, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
532 $crate::span!(@maybe_cx $($cx,)? $tracer, $name);
533 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
534 $body
535 }};
536
537 // Explicit tracer with attributes and closure
538 (@$tracer:ident, $name:expr, $($key:expr => $value:expr),+, in |$($cx:ident)? $(,$guard:ident)?| $body:expr) => {{
539 $crate::span!(@maybe_cx $($cx,)? $tracer, $name , $($key => $value),+);
540 $crate::span!(@maybe_guard $($cx,)? $($guard)?);
541
542 $body
543 }};
544
545 // Default TRACER, no attributes (statement form with internal guard)
546 ($name:expr $(,)?) => {{
547 let cx = $crate::span!(@build TRACER, $name);
548 let _otel_guard = cx.clone().attach();
549 }};
550
551 // Default TRACER with attributes (statement form with internal guard)
552 ($name:expr, $($key:expr => $value:expr),+ $(,)?) => {{
553 let cx = $crate::span!(@build TRACER, $name, $($key => $value),+);
554 let _otel_guard = cx.clone().attach();
555 }};
556
557 // Explicit tracer, no attributes (statement form with internal guard)
558 (@$tracer:ident, $name:expr $(,)?) => {{
559 let cx = $crate::span!(@build $tracer, $name);
560 let _otel_guard = cx.clone().attach();
561 }};
562
563 // Explicit tracer with attributes (statement form with internal guard)
564 (@$tracer:ident, $name:expr, $($key:expr => $value:expr),+ $(,)?) => {{
565 let cx = $crate::span!(@build $tracer, $name, $($key => $value),+);
566 let _otel_guard = cx.clone().attach();
567 }};
568
569 // Detached span (no guard) - default TRACER, no attributes
570 (@detached, $name:expr $(,)?) => {
571 $crate::span!(@build TRACER, $name)
572 };
573
574 // Detached span (no guard) - default TRACER with attributes
575 (@detached, $name:expr, $($key:expr => $value:expr),+ $(,)?) => {
576 $crate::span!(@build TRACER, $name, $($key => $value),+)
577 };
578
579 // Detached span (no guard) - explicit tracer, no attributes
580 (@detached, @$tracer:ident, $name:expr $(,)?) => {
581 $crate::span!(@build $tracer, $name)
582 };
583
584 // Detached span (no guard) - explicit tracer with attributes
585 (@detached, @$tracer:ident, $name:expr, $($key:expr => $value:expr),+ $(,)?) => {
586 $crate::span!(@build $tracer, $name, $($key => $value),+)
587 };
588}
589
590/// Records an exception event on the current span and returns an error value.
591///
592/// This macro adds an exception event to the current OpenTelemetry span following
593/// the semantic conventions, then evaluates to the provided error value.
594///
595/// # Syntax
596///
597/// ```ignore
598/// // In closures (ok_or_else, map_err, etc.)
599/// .ok_or_else(|| otel::exception!("invalid_params", Error::new("msg")))?
600///
601/// // In function bodies with explicit return
602/// return Err(otel::exception!("invalid_params", Error::new("msg")));
603///
604/// // With additional attributes
605/// otel::exception!("source_not_found", error, "uri" => uri.to_string())
606/// ```
607///
608/// # Parameters
609///
610/// - `exception_type`: String literal for the exception.type attribute
611/// - `error_value`: The error value to return (must implement Display for exception.message)
612/// - Additional key-value pairs: Optional extra attributes to add to the exception event
613///
614/// # Examples
615///
616/// ```ignore
617/// // In ok_or_else closure
618/// let arg = arguments.first().ok_or_else(|| {
619/// otel::exception!("invalid_params",
620/// jsonrpc::Error::invalid_params("Missing arguments"))
621/// })?;
622///
623/// // In map_err closure
624/// let uri = Uri::parse(uri_str).map_err(|e| {
625/// otel::exception!("invalid_uri",
626/// jsonrpc::Error::invalid_params(format!("Invalid URI: {}", e)),
627/// "uri_str" => uri_str.to_string()
628/// )
629/// })?;
630///
631/// // Direct return
632/// if cache.is_empty() {
633/// return Err(otel::exception!("cache_empty",
634/// jsonrpc::Error::internal_error("Cache is empty"),
635/// "operation" => "lookup"
636/// ));
637/// }
638/// ```
639///
640/// # OpenTelemetry Semantic Conventions
641///
642/// The macro follows the [OpenTelemetry semantic conventions for exceptions](https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/):
643///
644/// - Event name: `"exception"`
645/// - Required attributes:
646/// - `exception.type`: The type/category of the exception
647/// - `exception.message`: The error message extracted via Display
648/// - Additional attributes: Any extra key-value pairs provided
649#[macro_export]
650macro_rules! exception {
651 ($exception_type:expr, $error:expr $(, $key:expr => $value:expr)* $(,)?) => {{
652 let error_value = $error;
653 let error_message = format!("{}", error_value);
654
655 opentelemetry::Context::current().span().add_event(
656 "exception",
657 vec![
658 opentelemetry::KeyValue::new("exception.type", $exception_type),
659 opentelemetry::KeyValue::new("exception.message", error_message),
660 $(opentelemetry::KeyValue::new($key, $value)),*
661 ],
662 );
663
664 error_value
665 }};
666}
667
668/// Adds an event to the current span with optional attributes.
669///
670/// # Examples
671///
672/// ```ignore
673/// // Simple event
674/// otel::event!("query.start");
675///
676/// // Event with attributes
677/// otel::event!("query.result",
678/// "record_count" => 42,
679/// "partition_key" => "users"
680/// );
681/// ```
682#[macro_export]
683macro_rules! event {
684 ($event_name:expr $(, $key:expr => $value:expr)* $(,)?) => {
685 opentelemetry::Context::current().span().add_event(
686 $event_name,
687 vec![
688 $(opentelemetry::KeyValue::new($key, $value)),*
689 ],
690 );
691 };
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697
698 tracer!();
699
700 #[test]
701 fn span_with_closure_returns_value() {
702 let result = span!("test.closure", in |_cx| {
703 42
704 });
705 assert_eq!(result, 42);
706 }
707
708 #[test]
709 fn span_with_closure_and_attributes() {
710 let result = span!("test.closure_attrs", "foo" => "bar", in |_cx| {
711 "hello"
712 });
713 assert_eq!(result, "hello");
714 }
715
716 #[test]
717 fn span_with_closure_explicit_tracer() {
718 let result = span!(@TRACER, "test.explicit", in |_cx| {
719 100
720 });
721 assert_eq!(result, 100);
722 }
723
724 #[test]
725 fn span_with_closure_explicit_tracer_and_attributes() {
726 let result = span!(@TRACER, "test.explicit_attrs", "key" => "value", in |_cx| {
727 vec![1, 2, 3]
728 });
729 assert_eq!(result, vec![1, 2, 3]);
730 }
731}