Skip to main content

apollo_opentelemetry/macros/
span.rs

1//! Span macro support and helper macros.
2//!
3//! The main `span!` macro is a proc macro re-exported from `apollo_opentelemetry_macros`.
4//! This module contains helper macros used with spans.
5
6/// Converts an attribute value to `Option<Value>`.
7///
8/// - Literals are converted directly using `Value::from` (zero-alloc for string literals)
9/// - Expressions go through `ToValue` (supports `Option<T>`, allocates for strings)
10#[doc(hidden)]
11#[macro_export]
12macro_rules! __attr_to_option {
13    // Literal - convert directly, no allocation for &'static str
14    ($value:literal) => {
15        Some($crate::__private::opentelemetry::Value::from($value))
16    };
17    // Expression - go through ToValue (supports Option<T>)
18    ($value:expr) => {
19        $crate::ToValue::to_value(&$value)
20    };
21}
22
23/// Sets attributes on the current span.
24///
25/// Use this macro to add attributes to the current span from within a function
26/// instrumented with `#[traced]` or within a `span!` block.
27///
28/// Attributes with `None` values are skipped (not added to the span).
29///
30/// # Syntax
31///
32/// ```text
33/// span_attr!("key" = value)
34/// span_attr!("key1" = value1, "key2" = value2)
35/// ```
36///
37/// # Examples
38///
39/// ```
40/// use apollo_opentelemetry::{span, span_attr};
41///
42/// fn process(count: i64) {
43///     span!("process", || {
44///         span_attr!("result.count" = count, "result.status" = "success");
45///     });
46/// }
47/// ```
48#[macro_export]
49macro_rules! span_attr {
50    ($($key:literal = $value:expr),+ $(,)?) => {{
51        use $crate::__private::opentelemetry::trace::{TraceContextExt as _};
52        $(
53            if let Some(__attr_val) = $crate::ToValue::to_value(&$value) {
54                $crate::__private::opentelemetry::Context::current().span().set_attribute(
55                    $crate::__private::opentelemetry::KeyValue::new($key, __attr_val),
56                );
57            }
58        )+
59    }};
60}
61
62/// Sets the current span status to error with a formatted message.
63///
64/// Use this to mark the current span as failed with a descriptive error message.
65///
66/// Note: execution will continue after this macro has been called.
67///
68/// # Examples
69///
70/// ```
71/// use apollo_opentelemetry::{span, span_err};
72///
73/// fn process(id: u64) {
74///     span!("process", || {
75///         span_err!("failed to process id {}", id);
76///     });
77/// }
78/// ```
79#[macro_export]
80macro_rules! span_err {
81    ($($arg:tt)*) => {{
82        use $crate::__private::opentelemetry::trace::{Status, TraceContextExt as _};
83        $crate::__private::opentelemetry::Context::current().span().set_status(Status::error(format!($($arg)*)));
84    }};
85}
86
87/// Sets the current span status to ok.
88///
89/// Use this to explicitly mark the current span as successful.
90///
91/// # Examples
92///
93/// ```
94/// use apollo_opentelemetry::{span, span_ok};
95///
96/// fn process() {
97///     span!("process", || {
98///         span_ok!();
99///     });
100/// }
101/// ```
102#[macro_export]
103macro_rules! span_ok {
104    () => {{
105        use $crate::__private::opentelemetry::trace::{Status, TraceContextExt as _};
106        $crate::__private::opentelemetry::Context::current()
107            .span()
108            .set_status(Status::Ok);
109    }};
110}
111
112#[cfg(test)]
113mod tests {
114    use crate::span;
115
116    use apollo_opentelemetry_test::{TelemetryContext, assert_spans_snapshot};
117    use opentelemetry::trace::SpanKind;
118
119    #[test]
120    fn test_span_simple() {
121        let ctx = TelemetryContext::new();
122        drop(span!("simple"));
123        assert_spans_snapshot!(ctx, @r#"
124        - name: simple
125          span_kind: Internal
126          is_sampled: true
127        "#);
128    }
129
130    #[test]
131    fn test_span_with_kind() {
132        let ctx = TelemetryContext::new();
133        drop(span!("with-kind", kind: SpanKind::Server));
134        assert_spans_snapshot!(ctx, @r#"
135        - name: with-kind
136          span_kind: Server
137          is_sampled: true
138        "#);
139    }
140
141    #[test]
142    fn test_span_with_attributes() {
143        let ctx = TelemetryContext::new();
144        drop(span!("with-attrs", "key" = "value", "count" = 42i64));
145        assert_spans_snapshot!(ctx, @r#"
146        - name: with-attrs
147          span_kind: Internal
148          is_sampled: true
149          attributes:
150            count: "42"
151            key: value
152        "#);
153    }
154
155    #[test]
156    fn test_span_with_kind_and_attributes() {
157        let ctx = TelemetryContext::new();
158        drop(span!("full", kind: SpanKind::Client, "method" = "GET"));
159        assert_spans_snapshot!(ctx, @r#"
160        - name: full
161          span_kind: Client
162          is_sampled: true
163          attributes:
164            method: GET
165        "#);
166    }
167
168    // ============ Expression name tests ============
169
170    #[test]
171    fn test_span_expr_name_variable() {
172        let ctx = TelemetryContext::new();
173        let name = String::from("expr-name");
174        drop(span!(name));
175        assert_spans_snapshot!(ctx, @r#"
176        - name: expr-name
177          span_kind: Internal
178          is_sampled: true
179        "#);
180    }
181
182    #[test]
183    fn test_span_expr_name_format() {
184        let ctx = TelemetryContext::new();
185        let id = 42;
186        drop(span!(format!("span-{}", id)));
187        assert_spans_snapshot!(ctx, @r#"
188        - name: span-42
189          span_kind: Internal
190          is_sampled: true
191        "#);
192    }
193
194    #[test]
195    fn test_span_expr_name_with_kind() {
196        let ctx = TelemetryContext::new();
197        let name = "dynamic-name";
198        drop(span!(name.to_string(), kind: SpanKind::Server));
199        assert_spans_snapshot!(ctx, @r#"
200        - name: dynamic-name
201          span_kind: Server
202          is_sampled: true
203        "#);
204    }
205
206    #[test]
207    fn test_span_expr_name_with_attributes() {
208        let ctx = TelemetryContext::new();
209        let name = String::from("expr-attrs");
210        drop(span!(name, "key" = "value"));
211        assert_spans_snapshot!(ctx, @r#"
212        - name: expr-attrs
213          span_kind: Internal
214          is_sampled: true
215          attributes:
216            key: value
217        "#);
218    }
219
220    #[test]
221    fn test_span_expr_name_closure() {
222        let ctx = TelemetryContext::new();
223        let name = String::from("expr-closure");
224        span!(name, || {
225            // work
226        });
227        assert_spans_snapshot!(ctx, @r#"
228        - name: expr-closure
229          span_kind: Internal
230          is_sampled: true
231        "#);
232    }
233
234    #[tokio::test]
235    async fn test_span_expr_name_async() {
236        let ctx = TelemetryContext::new();
237        let id = 123;
238        span!(format!("async-{}", id), async {
239            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
240        })
241        .await;
242        assert_spans_snapshot!(ctx, @r#"
243        - name: async-123
244          span_kind: Internal
245          is_sampled: true
246        "#);
247    }
248
249    fn test_scope() -> &'static opentelemetry::InstrumentationScope {
250        static SCOPE: std::sync::LazyLock<opentelemetry::InstrumentationScope> =
251            std::sync::LazyLock::new(|| {
252                opentelemetry::InstrumentationScope::builder("my-component").build()
253            });
254        &SCOPE
255    }
256
257    #[tokio::test]
258    async fn test_span_in_async() {
259        let ctx = TelemetryContext::new();
260
261        let span = span!("async-operation");
262        tokio::time::sleep(std::time::Duration::from_millis(1)).await;
263        drop(span);
264
265        assert_spans_snapshot!(ctx, @r#"
266        - name: async-operation
267          span_kind: Internal
268          is_sampled: true
269        "#);
270    }
271
272    #[tokio::test]
273    async fn test_span_context_propagation() {
274        use opentelemetry::Context;
275        use opentelemetry::trace::{FutureExt as _, TraceContextExt};
276
277        let ctx = TelemetryContext::new();
278
279        let parent_span = span!("parent");
280        let parent_cx = Context::current_with_span(parent_span);
281
282        async {
283            tokio::spawn(
284                async {
285                    drop(span!("child"));
286                }
287                .with_current_context(),
288            )
289            .await
290            .unwrap();
291        }
292        .with_context(parent_cx)
293        .await;
294
295        assert_spans_snapshot!(ctx, @r#"
296        - name: child
297          span_kind: Internal
298          has_parent: true
299          is_sampled: true
300        - name: parent
301          span_kind: Internal
302          is_sampled: true
303        "#);
304    }
305
306    // ============ Closure form tests ============
307
308    #[test]
309    fn test_span_closure_simple() {
310        let ctx = TelemetryContext::new();
311        span!("closure-simple", || {
312            // work happens here
313        });
314        assert_spans_snapshot!(ctx, @r#"
315        - name: closure-simple
316          span_kind: Internal
317          is_sampled: true
318        "#);
319    }
320
321    #[test]
322    fn test_span_closure_with_kind() {
323        let ctx = TelemetryContext::new();
324        span!("closure-kind", kind: SpanKind::Server, || {
325            // work happens here
326        });
327        assert_spans_snapshot!(ctx, @r#"
328        - name: closure-kind
329          span_kind: Server
330          is_sampled: true
331        "#);
332    }
333
334    #[test]
335    fn test_span_closure_with_attributes() {
336        let ctx = TelemetryContext::new();
337        span!("closure-attrs", "key" = "value", || {
338            // work happens here
339        });
340        assert_spans_snapshot!(ctx, @r#"
341        - name: closure-attrs
342          span_kind: Internal
343          is_sampled: true
344          attributes:
345            key: value
346        "#);
347    }
348
349    #[test]
350    fn test_span_closure_with_kind_and_attributes() {
351        let ctx = TelemetryContext::new();
352        span!("closure-full", kind: SpanKind::Client, "method" = "GET", || {
353            // work happens here
354        });
355        assert_spans_snapshot!(ctx, @r#"
356        - name: closure-full
357          span_kind: Client
358          is_sampled: true
359          attributes:
360            method: GET
361        "#);
362    }
363
364    #[test]
365    fn test_span_closure_returns_value() {
366        let ctx = TelemetryContext::new();
367        let result = span!("closure-return", || 42);
368        assert_eq!(result, 42);
369        assert_spans_snapshot!(ctx, @r#"
370        - name: closure-return
371          span_kind: Internal
372          is_sampled: true
373        "#);
374    }
375
376    #[test]
377    fn test_span_move_closure() {
378        let ctx = TelemetryContext::new();
379        let value = String::from("captured");
380        let result = span!("move-closure", move || {
381            value // moved into closure
382        });
383        assert_eq!(result, "captured");
384        assert_spans_snapshot!(ctx, @r#"
385        - name: move-closure
386          span_kind: Internal
387          is_sampled: true
388        "#);
389    }
390
391    #[test]
392    fn test_span_closure_nested() {
393        let ctx = TelemetryContext::new();
394        span!("outer", || {
395            span!("inner", || {
396                // nested work
397            });
398        });
399        assert_spans_snapshot!(ctx, @r#"
400        - name: inner
401          span_kind: Internal
402          has_parent: true
403          is_sampled: true
404        - name: outer
405          span_kind: Internal
406          is_sampled: true
407        "#);
408    }
409
410    #[test]
411    fn test_span_closure_with_span_event() {
412        use crate::span_event;
413
414        let ctx = TelemetryContext::new();
415        span!("with-event", || {
416            span_event!("something-happened");
417        });
418        assert_spans_snapshot!(ctx, @r#"
419        - name: with-event
420          span_kind: Internal
421          is_sampled: true
422          events:
423            - name: something-happened
424        "#);
425    }
426
427    // ============ Async block form tests ============
428
429    #[tokio::test]
430    async fn test_span_async_block_simple() {
431        let ctx = TelemetryContext::new();
432        span!("async-simple", async {
433            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
434        })
435        .await;
436        assert_spans_snapshot!(ctx, @r#"
437        - name: async-simple
438          span_kind: Internal
439          is_sampled: true
440        "#);
441    }
442
443    #[tokio::test]
444    async fn test_span_async_block_with_kind() {
445        let ctx = TelemetryContext::new();
446        span!("async-kind", kind: SpanKind::Server, async {
447            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
448        })
449        .await;
450        assert_spans_snapshot!(ctx, @r#"
451        - name: async-kind
452          span_kind: Server
453          is_sampled: true
454        "#);
455    }
456
457    #[tokio::test]
458    async fn test_span_async_block_with_attributes() {
459        let ctx = TelemetryContext::new();
460        span!("async-attrs", "key" = "value", async {
461            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
462        })
463        .await;
464        assert_spans_snapshot!(ctx, @r#"
465        - name: async-attrs
466          span_kind: Internal
467          is_sampled: true
468          attributes:
469            key: value
470        "#);
471    }
472
473    #[tokio::test]
474    async fn test_span_async_block_with_kind_and_attributes() {
475        let ctx = TelemetryContext::new();
476        span!("async-full", kind: SpanKind::Client, "method" = "GET", async {
477            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
478        })
479        .await;
480        assert_spans_snapshot!(ctx, @r#"
481        - name: async-full
482          span_kind: Client
483          is_sampled: true
484          attributes:
485            method: GET
486        "#);
487    }
488
489    #[tokio::test]
490    async fn test_span_async_block_returns_value() {
491        let ctx = TelemetryContext::new();
492        let result = span!("async-return", async {
493            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
494            42
495        })
496        .await;
497        assert_eq!(result, 42);
498        assert_spans_snapshot!(ctx, @r#"
499        - name: async-return
500          span_kind: Internal
501          is_sampled: true
502        "#);
503    }
504
505    #[tokio::test]
506    async fn test_span_async_block_nested() {
507        let ctx = TelemetryContext::new();
508        span!("async-outer", async {
509            span!("async-inner", async {
510                tokio::time::sleep(std::time::Duration::from_millis(1)).await;
511            })
512            .await;
513        })
514        .await;
515        assert_spans_snapshot!(ctx, @r#"
516        - name: async-inner
517          span_kind: Internal
518          has_parent: true
519          is_sampled: true
520        - name: async-outer
521          span_kind: Internal
522          is_sampled: true
523        "#);
524    }
525
526    #[tokio::test]
527    async fn test_span_async_move_block() {
528        let ctx = TelemetryContext::new();
529        let value = String::from("captured");
530        let result = span!("async-move", async move {
531            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
532            value // moved into async block
533        })
534        .await;
535        assert_eq!(result, "captured");
536        assert_spans_snapshot!(ctx, @r#"
537        - name: async-move
538          span_kind: Internal
539          is_sampled: true
540        "#);
541    }
542
543    #[tokio::test]
544    async fn test_span_async_block_with_span_event() {
545        use crate::span_event;
546
547        let ctx = TelemetryContext::new();
548        span!("async-with-event", async {
549            span_event!("async-event");
550            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
551        })
552        .await;
553        assert_spans_snapshot!(ctx, @r#"
554        - name: async-with-event
555          span_kind: Internal
556          is_sampled: true
557          events:
558            - name: async-event
559        "#);
560    }
561
562    #[tokio::test]
563    async fn test_span_async_block_spawned_task() {
564        // Verify that spawned tasks correctly propagate context without
565        // needing .with_current_context() - the macro captures context at
566        // span creation time.
567        let ctx = TelemetryContext::new();
568        span!("parent", async {
569            tokio::spawn(span!("spawned-child", async {
570                tokio::time::sleep(std::time::Duration::from_millis(1)).await;
571            }))
572            .await
573            .unwrap();
574        })
575        .await;
576        assert_spans_snapshot!(ctx, @r#"
577        - name: spawned-child
578          span_kind: Internal
579          has_parent: true
580          is_sampled: true
581        - name: parent
582          span_kind: Internal
583          is_sampled: true
584        "#);
585    }
586
587    // ============ Optional attribute tests ============
588
589    #[test]
590    fn test_span_with_optional_attribute_some() {
591        let ctx = TelemetryContext::new();
592        let value: Option<i64> = Some(42);
593        drop(span!("opt-some", "count" = value));
594        assert_spans_snapshot!(ctx, @r#"
595        - name: opt-some
596          span_kind: Internal
597          is_sampled: true
598          attributes:
599            count: "42"
600        "#);
601    }
602
603    #[test]
604    fn test_span_with_optional_attribute_none() {
605        let ctx = TelemetryContext::new();
606        let value: Option<i64> = None;
607        drop(span!("opt-none", "count" = value));
608        assert_spans_snapshot!(ctx, @r#"
609        - name: opt-none
610          span_kind: Internal
611          is_sampled: true
612        "#);
613    }
614
615    #[test]
616    fn test_span_with_mixed_optional_attributes() {
617        let ctx = TelemetryContext::new();
618        let present: Option<&str> = Some("hello");
619        let absent: Option<i64> = None;
620        drop(span!(
621            "mixed-opts",
622            "present" = present,
623            "absent" = absent,
624            "always" = "here"
625        ));
626        assert_spans_snapshot!(ctx, @r#"
627        - name: mixed-opts
628          span_kind: Internal
629          is_sampled: true
630          attributes:
631            always: here
632            present: hello
633        "#);
634    }
635
636    // ============ span_attr! tests ============
637
638    #[test]
639    fn test_span_attr_single() {
640        let ctx = TelemetryContext::new();
641        span!("with-post-attr", || {
642            span_attr!("post.attr" = 123i64);
643        });
644        assert_spans_snapshot!(ctx, @r#"
645        - name: with-post-attr
646          span_kind: Internal
647          is_sampled: true
648          attributes:
649            post.attr: "123"
650        "#);
651    }
652
653    #[test]
654    fn test_span_attr_multiple() {
655        let ctx = TelemetryContext::new();
656        span!("with-multi-attr", || {
657            span_attr!("key1" = "value1", "key2" = 42i64);
658        });
659        assert_spans_snapshot!(ctx, @r#"
660        - name: with-multi-attr
661          span_kind: Internal
662          is_sampled: true
663          attributes:
664            key1: value1
665            key2: "42"
666        "#);
667    }
668
669    #[test]
670    fn test_span_attr_none_skipped() {
671        let ctx = TelemetryContext::new();
672        span!("with-skipped-attr", || {
673            span_attr!("skipped" = None::<i64>);
674        });
675        assert_spans_snapshot!(ctx, @r#"
676        - name: with-skipped-attr
677          span_kind: Internal
678          is_sampled: true
679        "#);
680    }
681
682    #[test]
683    fn test_span_attr_mixed_some_none() {
684        let ctx = TelemetryContext::new();
685        let present: Option<&str> = Some("hello");
686        let absent: Option<i64> = None;
687        span!("mixed-attr", || {
688            span_attr!("present" = present, "absent" = absent, "direct" = "value");
689        });
690        assert_spans_snapshot!(ctx, @r#"
691        - name: mixed-attr
692          span_kind: Internal
693          is_sampled: true
694          attributes:
695            direct: value
696            present: hello
697        "#);
698    }
699
700    #[test]
701    fn test_span_attr_trailing_comma() {
702        let ctx = TelemetryContext::new();
703        span!("trailing-comma", || {
704            span_attr!("key" = "value",);
705        });
706        assert_spans_snapshot!(ctx, @r#"
707        - name: trailing-comma
708          span_kind: Internal
709          is_sampled: true
710          attributes:
711            key: value
712        "#);
713    }
714
715    // ============ span_err! tests ============
716
717    #[test]
718    fn test_span_err_simple() {
719        let ctx = TelemetryContext::new();
720        span!("with-error", || {
721            span_err!("something went wrong");
722        });
723        assert_spans_snapshot!(ctx, @r#"
724        - name: with-error
725          span_kind: Internal
726          is_sampled: true
727          status: "Error: something went wrong"
728        "#);
729    }
730
731    #[test]
732    fn test_span_err_formatted() {
733        let ctx = TelemetryContext::new();
734        let id = 42;
735        let reason = "not found";
736        span!("with-formatted-error", || {
737            span_err!("failed to process id {}: {}", id, reason);
738        });
739        assert_spans_snapshot!(ctx, @r#"
740        - name: with-formatted-error
741          span_kind: Internal
742          is_sampled: true
743          status: "Error: failed to process id 42: not found"
744        "#);
745    }
746
747    // ============ span_ok! tests ============
748
749    #[test]
750    fn test_span_ok() {
751        let ctx = TelemetryContext::new();
752        span!("with-ok", || {
753            span_ok!();
754        });
755        assert_spans_snapshot!(ctx, @r#"
756        - name: with-ok
757          span_kind: Internal
758          is_sampled: true
759          status: Ok
760        "#);
761    }
762
763    // ============ span_result! (automatic status) tests ============
764
765    #[test]
766    fn test_span_closure_result_ok() {
767        let ctx = TelemetryContext::new();
768        let result: Result<i32, &str> = span!("result-ok", || Ok(42));
769        assert_eq!(result, Ok(42));
770        assert_spans_snapshot!(ctx, @r#"
771        - name: result-ok
772          span_kind: Internal
773          is_sampled: true
774          status: Ok
775        "#);
776    }
777
778    #[test]
779    fn test_span_closure_result_err() {
780        let ctx = TelemetryContext::new();
781        let result: Result<i32, &str> = span!("result-err", || Err("something failed"));
782        assert_eq!(result, Err("something failed"));
783        assert_spans_snapshot!(ctx, @r#"
784        - name: result-err
785          span_kind: Internal
786          is_sampled: true
787          status: "Error: something failed"
788        "#);
789    }
790
791    #[test]
792    fn test_span_closure_non_result() {
793        let ctx = TelemetryContext::new();
794        let value: i32 = span!("non-result", || 42);
795        assert_eq!(value, 42);
796        // Non-Result types should not set span status
797        assert_spans_snapshot!(ctx, @r#"
798        - name: non-result
799          span_kind: Internal
800          is_sampled: true
801        "#);
802    }
803
804    #[test]
805    fn test_span_closure_custom_result_alias() {
806        type MyResult<T> = Result<T, Box<dyn std::error::Error>>;
807
808        let ctx = TelemetryContext::new();
809        let result: MyResult<&str> = span!("custom-result", || Ok("success"));
810        assert!(result.is_ok());
811        assert_spans_snapshot!(ctx, @r#"
812        - name: custom-result
813          span_kind: Internal
814          is_sampled: true
815          status: Ok
816        "#);
817    }
818
819    #[tokio::test]
820    async fn test_span_async_block_result_ok() {
821        let ctx = TelemetryContext::new();
822        let result: Result<i32, &str> = span!("async-result-ok", async { Ok(42) }).await;
823        assert_eq!(result, Ok(42));
824        assert_spans_snapshot!(ctx, @r#"
825        - name: async-result-ok
826          span_kind: Internal
827          is_sampled: true
828          status: Ok
829        "#);
830    }
831
832    #[tokio::test]
833    async fn test_span_async_block_result_err() {
834        let ctx = TelemetryContext::new();
835        let result: Result<i32, &str> =
836            span!("async-result-err", async { Err("async failure") }).await;
837        assert_eq!(result, Err("async failure"));
838        assert_spans_snapshot!(ctx, @r#"
839        - name: async-result-err
840          span_kind: Internal
841          is_sampled: true
842          status: "Error: async failure"
843        "#);
844    }
845
846    #[tokio::test]
847    async fn test_span_async_block_non_result() {
848        let ctx = TelemetryContext::new();
849        let value: i32 = span!("async-non-result", async { 42 }).await;
850        assert_eq!(value, 42);
851        // Non-Result types should not set span status
852        assert_spans_snapshot!(ctx, @r#"
853        - name: async-non-result
854          span_kind: Internal
855          is_sampled: true
856        "#);
857    }
858
859    #[test]
860    fn test_span_closure_question_mark_sets_error_status() {
861        fn fallible() -> Result<(), &'static str> {
862            Err("inner error")
863        }
864
865        let ctx = TelemetryContext::new();
866        // With explicit closure, ? returns from the closure (not parent function)
867        let result: Result<(), &str> = span!("with-question-mark", || {
868            fallible()?;
869            Ok(())
870        });
871        assert_eq!(result, Err("inner error"));
872        assert_spans_snapshot!(ctx, @r#"
873        - name: with-question-mark
874          span_kind: Internal
875          is_sampled: true
876          status: "Error: inner error"
877        "#);
878    }
879
880    #[tokio::test]
881    async fn test_span_async_block_question_mark_sets_error_status() {
882        async fn fallible() -> Result<(), &'static str> {
883            Err("async inner error")
884        }
885
886        let ctx = TelemetryContext::new();
887        let result: Result<(), &str> = span!("async-with-question-mark", async {
888            fallible().await?;
889            Ok(())
890        })
891        .await;
892        assert_eq!(result, Err("async inner error"));
893        // The ? operator in async block returns from the async block
894        assert_spans_snapshot!(ctx, @r#"
895        - name: async-with-question-mark
896          span_kind: Internal
897          is_sampled: true
898          status: "Error: async inner error"
899        "#);
900    }
901
902    // ============ Explicit parent context tests ============
903
904    #[test]
905    fn test_span_with_explicit_parent_return_form() {
906        use opentelemetry::Context;
907        use opentelemetry::trace::TraceContextExt;
908
909        let ctx = TelemetryContext::new();
910
911        // Create a parent span and capture its context
912        let parent_span = span!("parent");
913        let parent_cx = Context::current_with_span(parent_span);
914
915        // Create a child span with explicit parent
916        drop(span!("child", parent: &parent_cx));
917
918        // Drop the parent context to end the parent span
919        drop(parent_cx);
920
921        assert_spans_snapshot!(ctx, @r#"
922        - name: child
923          span_kind: Internal
924          has_parent: true
925          is_sampled: true
926        - name: parent
927          span_kind: Internal
928          is_sampled: true
929        "#);
930    }
931
932    #[test]
933    fn test_span_with_explicit_parent_closure() {
934        use opentelemetry::Context;
935        use opentelemetry::trace::TraceContextExt;
936
937        let ctx = TelemetryContext::new();
938
939        let parent_span = span!("parent");
940        let parent_cx = Context::current_with_span(parent_span);
941
942        span!("child", parent: &parent_cx, || {
943            // work
944        });
945
946        drop(parent_cx);
947
948        assert_spans_snapshot!(ctx, @r#"
949        - name: child
950          span_kind: Internal
951          has_parent: true
952          is_sampled: true
953        - name: parent
954          span_kind: Internal
955          is_sampled: true
956        "#);
957    }
958
959    #[test]
960    fn test_span_with_explicit_parent_kind_attributes_and_scope() {
961        use opentelemetry::Context;
962        use opentelemetry::trace::TraceContextExt;
963
964        let ctx = TelemetryContext::new();
965
966        let parent_span = span!("parent");
967        let parent_cx = Context::current_with_span(parent_span);
968
969        span!(
970            "child",
971            scope: test_scope(),
972            parent: &parent_cx,
973            kind: SpanKind::Server,
974            "http.method" = "GET",
975            || {
976                // work
977            }
978        );
979        drop(parent_cx);
980
981        assert_spans_snapshot!(ctx, @r#"
982        - name: child
983          span_kind: Server
984          has_parent: true
985          is_sampled: true
986          attributes:
987            http.method: GET
988        - name: parent
989          span_kind: Internal
990          is_sampled: true
991        "#);
992
993        let spans = ctx.spans();
994        let child = spans.iter().find(|s| s.name == "child").unwrap();
995        assert_eq!(child.instrumentation_scope.name(), "my-component");
996    }
997
998    #[tokio::test]
999    async fn test_span_with_explicit_parent_async() {
1000        use opentelemetry::Context;
1001        use opentelemetry::trace::TraceContextExt;
1002
1003        let ctx = TelemetryContext::new();
1004
1005        let parent_span = span!("parent");
1006        let parent_cx = Context::current_with_span(parent_span);
1007
1008        span!("child", parent: &parent_cx, async {
1009            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1010        })
1011        .await;
1012
1013        drop(parent_cx);
1014
1015        assert_spans_snapshot!(ctx, @r#"
1016        - name: child
1017          span_kind: Internal
1018          has_parent: true
1019          is_sampled: true
1020        - name: parent
1021          span_kind: Internal
1022          is_sampled: true
1023        "#);
1024    }
1025
1026    #[tokio::test]
1027    async fn test_span_with_explicit_parent_spawned_task() {
1028        use opentelemetry::Context;
1029        use opentelemetry::trace::TraceContextExt;
1030
1031        let ctx = TelemetryContext::new();
1032
1033        // Create parent span and capture context BEFORE spawning
1034        let parent_span = span!("parent");
1035        let parent_cx = Context::current_with_span(parent_span);
1036
1037        // Spawn a task with explicit parent - useful when the parent
1038        // context needs to be passed across task boundaries
1039        tokio::spawn(span!("spawned-child", parent: &parent_cx, async move {
1040            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
1041        }))
1042        .await
1043        .unwrap();
1044
1045        drop(parent_cx);
1046
1047        assert_spans_snapshot!(ctx, @r#"
1048        - name: spawned-child
1049          span_kind: Internal
1050          has_parent: true
1051          is_sampled: true
1052        - name: parent
1053          span_kind: Internal
1054          is_sampled: true
1055        "#);
1056    }
1057}