2222
2323import langfuse
2424from langfuse import LangfuseSpan as LangfuseClientSpan
25- from langfuse .types import TraceContext , TraceMetadata
25+ from langfuse import propagate_attributes
26+ from langfuse .types import TraceContext
2627
2728logger = logging .getLogger (__name__ )
2829
@@ -61,7 +62,6 @@ def __init__(self, context_manager: AbstractContextManager) -> None:
6162 Initialize a LangfuseSpan instance.
6263
6364 :param context_manager: The context manager from Langfuse created with
64- `langfuse.get_client().start_as_current_span` or
6565 `langfuse.get_client().start_as_current_observation`.
6666 """
6767 self ._span = context_manager .__enter__ ()
@@ -297,33 +297,17 @@ def create_span(self, context: SpanContext) -> LangfuseSpan:
297297 trace_context : TraceContext | None = cast (
298298 TraceContext , {"trace_id" : tracing_ctx .get ("trace_id" )} if tracing_ctx .get ("trace_id" ) else None
299299 )
300- # Create a new trace when there's no parent span
300+
301301 span_context_manager = self .tracer .start_as_current_observation (
302302 trace_context = trace_context ,
303303 name = context .trace_name ,
304304 version = tracing_ctx .get ("version" ),
305305 as_type = root_span_type ,
306306 )
307-
308- # Create LangfuseSpan which will handle entering the context manager
309307 span = LangfuseSpan (span_context_manager )
310308
311- # Build trace metadata from context
312- trace_metadata : TraceMetadata = {
313- "name" : context .trace_name ,
314- "user_id" : tracing_ctx .get ("user_id" ),
315- "session_id" : tracing_ctx .get ("session_id" ),
316- "version" : tracing_ctx .get ("version" ),
317- "metadata" : None ,
318- "tags" : tracing_ctx .get ("tags" ),
319- "public" : context .public ,
320- "release" : None ,
321- }
322-
323- # Filter out None values and apply trace attributes
324- trace_attrs = {k : v for k , v in trace_metadata .items () if v is not None }
325- if trace_attrs :
326- span ._span .update_trace (** trace_attrs )
309+ if context .public :
310+ span .raw_span ().set_trace_as_public ()
327311
328312 return span
329313
@@ -347,7 +331,7 @@ def create_span(self, context: SpanContext) -> LangfuseSpan:
347331 )
348332 )
349333 else :
350- return LangfuseSpan (self .tracer .start_as_current_span (name = context .name ))
334+ return LangfuseSpan (self .tracer .start_as_current_observation (name = context .name ))
351335
352336 def handle (self , span : LangfuseSpan , component_type : str | None ) -> None :
353337 """Process and enrich a span after component execution."""
@@ -356,7 +340,7 @@ def handle(self, span: LangfuseSpan, component_type: str | None) -> None:
356340 if at_pipeline_level :
357341 coerced_input = tracing_utils .coerce_tag_value (span .get_data ().get (_PIPELINE_INPUT_KEY ))
358342 coerced_output = tracing_utils .coerce_tag_value (span .get_data ().get (_PIPELINE_OUTPUT_KEY ))
359- span .raw_span ().update_trace (input = coerced_input , output = coerced_output )
343+ span .raw_span ().update (input = coerced_input , output = coerced_output )
360344 # special case for ToolInvoker (to update the span name to be: `original_component_name - [tool_names]`)
361345 if component_type == "ToolInvoker" :
362346 tool_names : list [str ] = []
@@ -468,6 +452,7 @@ def trace(
468452 tags = tags or {}
469453 span_name = tags .get (_COMPONENT_NAME_KEY , operation_name )
470454 component_type = tags .get (_COMPONENT_TYPE_KEY )
455+ is_root_span = not (parent_span or self .current_span ())
471456
472457 # Create a new span context
473458 span_context = SpanContext (
@@ -482,74 +467,91 @@ def trace(
482467 public = self ._public ,
483468 )
484469
485- # Create span using the handler
486- span = self ._span_handler .create_span (span_context )
470+ tracing_ctx = tracing_context_var .get ({})
487471
488- # Build new span hierarchy: copy existing stack, add new span, save for restoration
489- prev_stack = span_stack_var .get ()
490- new_stack = (prev_stack or []).copy ()
491- new_stack .append (span )
492- token = span_stack_var .set (new_stack )
472+ # we need to properly exit context managers
473+ with contextlib .ExitStack () as stack :
474+ if is_root_span :
475+ # propagate_attributes sets trace_name and other trace-level metadata on the root
476+ # span and ALL child spans created within this context (full trace lifetime)
477+ stack .enter_context (
478+ propagate_attributes (
479+ trace_name = self ._name ,
480+ user_id = tracing_ctx .get ("user_id" ) if tracing_ctx .get ("user_id" ) is not None else None ,
481+ session_id = tracing_ctx .get ("session_id" ) if tracing_ctx .get ("session_id" ) is not None else None ,
482+ version = tracing_ctx .get ("version" ) if tracing_ctx .get ("version" ) is not None else None ,
483+ tags = tracing_ctx .get ("tags" ) if tracing_ctx .get ("tags" ) is not None else None ,
484+ )
485+ )
493486
494- span .set_tags (tags )
487+ # Create span using the handler
488+ span = self ._span_handler .create_span (span_context )
495489
496- try :
497- yield span
498- except Exception :
499- # Exception occurred - capture exception info and pass to __exit__
500- # This allows Langfuse/OpenTelemetry to properly mark the span with ERROR level
501- exc_info = sys .exc_info ()
502- try :
503- # Process span data (may fail with nested pipeline exceptions)
504- self ._span_handler .handle (span , component_type )
505-
506- # End span with exception info (may fail if span data is corrupted)
507- raw_span = span .raw_span ()
508- if span ._context_manager is not None :
509- # Pass actual exception info to mark span as failed with ERROR level
510- span ._context_manager .__exit__ (* exc_info )
511- elif hasattr (raw_span , "end" ):
512- # Only call end() if it's not a context manager
513- raw_span .end ()
514- except Exception as cleanup_error :
515- # Log cleanup errors but don't let them corrupt context
516- logger .warning (
517- "Error during span cleanup for {operation_name}: {cleanup_error}" ,
518- operation_name = operation_name ,
519- cleanup_error = cleanup_error ,
520- )
490+ # Build new span hierarchy: copy existing stack, add new span, save for restoration
491+ prev_stack = span_stack_var .get ()
492+ new_stack = (prev_stack or []).copy ()
493+ new_stack .append (span )
494+ token = span_stack_var .set (new_stack )
521495
522- # Re-raise the original exception
523- raise
524- else :
525- # No exception - clean exit with success status
526- # This preserves any manually-set log levels (WARNING, DEBUG)
527- try :
528- # Process span data
529- self ._span_handler .handle (span , component_type )
530-
531- # End span successfully
532- raw_span = span .raw_span ()
533- # In v3, we need to properly exit context managers
534- if span ._context_manager is not None :
535- # No exception - pass None to indicate success
536- span ._context_manager .__exit__ (None , None , None )
537- elif hasattr (raw_span , "end" ):
538- # Only call end() if it's not a context manager
539- raw_span .end ()
540- except Exception as cleanup_error :
541- # Log cleanup errors but don't let them corrupt context
542- logger .warning (
543- "Error during span cleanup for {operation_name}: {cleanup_error}" ,
544- operation_name = operation_name ,
545- cleanup_error = cleanup_error ,
546- )
547- finally :
548- # Restore previous span stack using saved token
549- span_stack_var .reset (token )
496+ span .set_tags (tags )
550497
551- if self .enforce_flush :
552- self .flush ()
498+ try :
499+ yield span
500+ except Exception :
501+ # Exception occurred - capture exception info and pass to __exit__
502+ # This allows Langfuse/OpenTelemetry to properly mark the span with ERROR level
503+ exc_info = sys .exc_info ()
504+ try :
505+ # Process span data (may fail with nested pipeline exceptions)
506+ self ._span_handler .handle (span , component_type )
507+
508+ # End span with exception info (may fail if span data is corrupted)
509+ raw_span = span .raw_span ()
510+ if span ._context_manager is not None :
511+ # Pass actual exception info to mark span as failed with ERROR level
512+ span ._context_manager .__exit__ (* exc_info )
513+ elif hasattr (raw_span , "end" ):
514+ # Only call end() if it's not a context manager
515+ raw_span .end ()
516+ except Exception as cleanup_error :
517+ # Log cleanup errors but don't let them corrupt context
518+ logger .warning (
519+ "Error during span cleanup for {operation_name}: {cleanup_error}" ,
520+ operation_name = operation_name ,
521+ cleanup_error = cleanup_error ,
522+ )
523+
524+ # Re-raise the original exception
525+ raise
526+ else :
527+ # No exception - clean exit with success status
528+ # This preserves any manually-set log levels (WARNING, DEBUG)
529+ try :
530+ # Process span data
531+ self ._span_handler .handle (span , component_type )
532+
533+ # End span successfully
534+ raw_span = span .raw_span ()
535+ # In v3, we need to properly exit context managers
536+ if span ._context_manager is not None :
537+ # No exception - pass None to indicate success
538+ span ._context_manager .__exit__ (None , None , None )
539+ elif hasattr (raw_span , "end" ):
540+ # Only call end() if it's not a context manager
541+ raw_span .end ()
542+ except Exception as cleanup_error :
543+ # Log cleanup errors but don't let them corrupt context
544+ logger .warning (
545+ "Error during span cleanup for {operation_name}: {cleanup_error}" ,
546+ operation_name = operation_name ,
547+ cleanup_error = cleanup_error ,
548+ )
549+ finally :
550+ # Restore previous span stack using saved token
551+ span_stack_var .reset (token )
552+
553+ if self .enforce_flush :
554+ self .flush ()
553555
554556 def flush (self ) -> None :
555557 """Flush all pending spans to Langfuse."""
0 commit comments