44
55import contextlib
66import os
7+ import sys
78from abc import ABC , abstractmethod
89from collections import Counter
910from collections .abc import Iterator
@@ -299,7 +300,9 @@ def create_span(self, context: SpanContext) -> LangfuseSpan:
299300 )
300301 # Create a new trace when there's no parent span
301302 span_context_manager = self .tracer .start_as_current_observation (
302- name = context .trace_name , version = tracing_ctx .get ("version" ), as_type = root_span_type
303+ name = context .trace_name ,
304+ version = tracing_ctx .get ("version" ),
305+ as_type = root_span_type ,
303306 )
304307
305308 # Create LangfuseSpan which will handle entering the context manager
@@ -466,16 +469,44 @@ def trace(
466469
467470 try :
468471 yield span
469- finally :
470- # Always clean up context, even if nested operations fail
472+ except Exception :
473+ # Exception occurred - capture exception info and pass to __exit__
474+ # This allows Langfuse/OpenTelemetry to properly mark the span with ERROR level
475+ exc_info = sys .exc_info ()
471476 try :
472477 # Process span data (may fail with nested pipeline exceptions)
473478 self ._span_handler .handle (span , component_type )
474479
475- # End span (may fail if span data is corrupted)
480+ # End span with exception info (may fail if span data is corrupted)
481+ raw_span = span .raw_span ()
482+ if span ._context_manager is not None :
483+ # Pass actual exception info to mark span as failed with ERROR level
484+ span ._context_manager .__exit__ (* exc_info )
485+ elif hasattr (raw_span , "end" ):
486+ # Only call end() if it's not a context manager
487+ raw_span .end ()
488+ except Exception as cleanup_error :
489+ # Log cleanup errors but don't let them corrupt context
490+ logger .warning (
491+ "Error during span cleanup for {operation_name}: {cleanup_error}" ,
492+ operation_name = operation_name ,
493+ cleanup_error = cleanup_error ,
494+ )
495+
496+ # Re-raise the original exception
497+ raise
498+ else :
499+ # No exception - clean exit with success status
500+ # This preserves any manually-set log levels (WARNING, DEBUG)
501+ try :
502+ # Process span data
503+ self ._span_handler .handle (span , component_type )
504+
505+ # End span successfully
476506 raw_span = span .raw_span ()
477507 # In v3, we need to properly exit context managers
478508 if span ._context_manager is not None :
509+ # No exception - pass None to indicate success
479510 span ._context_manager .__exit__ (None , None , None )
480511 elif hasattr (raw_span , "end" ):
481512 # Only call end() if it's not a context manager
@@ -487,9 +518,9 @@ def trace(
487518 operation_name = operation_name ,
488519 cleanup_error = cleanup_error ,
489520 )
490- finally :
491- # Restore previous span stack using saved token - ensures proper cleanup
492- span_stack_var .reset (token )
521+ finally :
522+ # Restore previous span stack using saved token
523+ span_stack_var .reset (token )
493524
494525 if self .enforce_flush :
495526 self .flush ()
0 commit comments