@@ -265,6 +265,44 @@ def make_handler():
265265 return exporter , handler , provider
266266
267267
268+ def enter_llama_span (
269+ handler : OTelCompatibleSpanHandler ,
270+ span_id : str ,
271+ parent_id : Optional [str ] = None ,
272+ ) -> None :
273+ handler .span_enter (id_ = span_id , bound_args = _bound , parent_id = parent_id )
274+
275+
276+ def exit_llama_span (handler : OTelCompatibleSpanHandler , span_id : str ) -> None :
277+ handler .span_exit (id_ = span_id , bound_args = _bound )
278+
279+
280+ def finished_spans_by_name (
281+ exporter : InMemorySpanExporter ,
282+ provider : TracerProvider ,
283+ ) -> dict [str , ReadableSpan ]:
284+ provider .force_flush ()
285+ spans = exporter .get_finished_spans ()
286+ spans_by_name = {span .name : span for span in spans }
287+ assert len (spans_by_name ) == len (spans )
288+ return spans_by_name
289+
290+
291+ def assert_parent (child : ReadableSpan , parent : ReadableSpan ) -> None :
292+ assert child .parent is not None
293+ assert child .parent .span_id == parent .context .span_id
294+ assert child .parent .trace_id == parent .context .trace_id
295+
296+
297+ def assert_trace_chain (
298+ spans_by_name : dict [str , ReadableSpan ],
299+ expected_chain : Sequence [str ],
300+ ) -> None :
301+ assert set (spans_by_name ) == set (expected_chain )
302+ for parent_name , child_name in zip (expected_chain , expected_chain [1 :]):
303+ assert_parent (spans_by_name [child_name ], spans_by_name [parent_name ])
304+
305+
268306def test_span_name_strips_uuid () -> None :
269307 exporter , handler , provider = make_handler ()
270308 handler .span_enter (id_ = "MyWorkflow.run-abc123-def" , bound_args = _bound )
@@ -357,17 +395,95 @@ def test_tags_not_mutated_by_new_span() -> None:
357395 assert tags == original_tags
358396
359397
398+ def test_span_enter_makes_otel_span_current_for_downstream_spans () -> None :
399+ # Expected trace:
400+ # root
401+ # downstream-child
402+ exporter , handler , provider = make_handler ()
403+ clean_token = context .attach (context .Context ())
404+ try :
405+ downstream_tracer = provider .get_tracer ("downstream" )
406+
407+ enter_llama_span (handler , "root-uuid" )
408+ root_otel_span = handler .all_spans ["root-uuid" ]
409+ assert trace .get_current_span () is root_otel_span
410+
411+ with downstream_tracer .start_as_current_span ("downstream-child" ):
412+ pass
413+
414+ exit_llama_span (handler , "root-uuid" )
415+
416+ spans = finished_spans_by_name (exporter , provider )
417+ assert_trace_chain (spans , ["root" , "downstream-child" ])
418+ assert trace .get_current_span () is not root_otel_span
419+ finally :
420+ context .detach (clean_token )
421+
422+
423+ def test_otel_context_is_source_of_truth_when_external_spans_interleave () -> None :
424+ # Expected trace:
425+ # external-root
426+ # llama_parent
427+ # external-child
428+ # llama_child
429+ exporter , handler , provider = make_handler ()
430+ clean_token = context .attach (context .Context ())
431+ tracer = provider .get_tracer ("external" )
432+
433+ try :
434+ with tracer .start_as_current_span ("external-root" ):
435+ enter_llama_span (handler , "llama_parent" )
436+ with tracer .start_as_current_span ("external-child" ):
437+ enter_llama_span (
438+ handler ,
439+ "llama_child" ,
440+ parent_id = "llama_parent" ,
441+ )
442+ exit_llama_span (handler , "llama_child" )
443+ exit_llama_span (handler , "llama_parent" )
444+
445+ spans = finished_spans_by_name (exporter , provider )
446+ assert_trace_chain (
447+ spans ,
448+ ["external-root" , "llama_parent" , "external-child" , "llama_child" ],
449+ )
450+ finally :
451+ context .detach (clean_token )
452+
453+
454+ def test_span_exit_ends_span_when_context_detach_fails (monkeypatch : Any ) -> None :
455+ exporter , handler , provider = make_handler ()
456+ clean_token = context .attach (context .Context ())
457+ original_detach = context .detach
458+
459+ try :
460+ handler .span_enter (id_ = "root-uuid" , bound_args = _bound )
461+
462+ def fail_detach (token : Any ) -> None :
463+ raise RuntimeError ("detach failed" )
464+
465+ monkeypatch .setattr (context , "detach" , fail_detach )
466+ handler .span_exit (id_ = "root-uuid" , bound_args = _bound )
467+ provider .force_flush ()
468+
469+ spans = exporter .get_finished_spans ()
470+ assert len (spans ) == 1
471+ assert spans [0 ].end_time is not None
472+ assert handler .all_spans == {}
473+ assert handler ._context_tokens == {}
474+ finally :
475+ monkeypatch .setattr (context , "detach" , original_detach )
476+ context .detach (clean_token )
477+
478+
360479def test_capture_propagation_context () -> None :
361480 """capture_propagation_context returns a dict with traceparent when a span is active."""
362481 exporter , handler , provider = make_handler ()
363482 # Create a span so there's an active trace context
364483 handler .span_enter (id_ = "root-uuid" , bound_args = _bound )
365- # Activate the OTel span in the current context so capture can see it
366- from opentelemetry .trace import set_span_in_context
367484
368485 otel_span = handler .all_spans ["root-uuid" ]
369- ctx = set_span_in_context (otel_span )
370- context .attach (ctx )
486+ assert trace .get_current_span () is otel_span
371487
372488 captured = handler .capture_propagation_context ()
373489 assert "otel" in captured
@@ -389,19 +505,15 @@ def test_capture_restore_propagation_roundtrip() -> None:
389505 - externally-set OTel context (e.g. baggage-like ambient values) propagates
390506 through the traceparent mechanism
391507 """
392- from opentelemetry .trace import set_span_in_context
393-
394508 # --- Process A: create root span with tags, capture context ---
395509 exporter_a , handler_a , provider_a = make_handler ()
396510
397511 tags_a = {"handler_id" : "h1" , "run_id" : "r1" , "myapp.custom" : "val_a" }
398512 original_tags_a = dict (tags_a )
399513 handler_a .span_enter (id_ = "root-uuid" , bound_args = _bound , tags = tags_a )
400514
401- # Activate the OTel span in ambient context (simulating what the Dispatcher
402- # would do before a serialization boundary)
403515 root_otel_span = handler_a .all_spans ["root-uuid" ]
404- context . attach ( set_span_in_context ( root_otel_span ))
516+ assert trace . get_current_span () is root_otel_span
405517
406518 # Capture propagation context — this is what gets serialized across the boundary
407519 captured_ctx = handler_a .capture_propagation_context ()
@@ -486,7 +598,6 @@ def test_dispatcher_propagation_roundtrip_with_tags() -> None:
486598 active_instrument_tags ,
487599 instrument_tags ,
488600 )
489- from opentelemetry .trace import set_span_in_context
490601
491602 exporter_a , handler_a , provider_a = make_handler ()
492603 exporter_b , handler_b , provider_b = make_handler ()
@@ -508,9 +619,8 @@ def test_dispatcher_propagation_roundtrip_with_tags() -> None:
508619 id_ = "root-uuid" , bound_args = _bound , tags = active_instrument_tags .get ()
509620 )
510621
511- # Activate OTel span in ambient context
512622 root_otel_span = handler_a .all_spans ["root-uuid" ]
513- context . attach ( set_span_in_context ( root_otel_span ))
623+ assert trace . get_current_span () is root_otel_span
514624
515625 captured = dispatcher_a .capture_propagation_context ()
516626
0 commit comments