@@ -254,6 +254,7 @@ def response_hook(span: Span, status: str, response_headers: List):
254254---
255255"""
256256
257+ import sys
257258import weakref
258259from logging import getLogger
259260from time import time_ns
@@ -299,6 +300,11 @@ def response_hook(span: Span, status: str, response_headers: List):
299300
300301_logger = getLogger (__name__ )
301302
303+ # Global constants for Flask 3.1+ streaming context cleanup
304+ _IS_FLASK_31_PLUS = hasattr (flask , "__version__" ) and package_version .parse (
305+ flask .__version__
306+ ) >= package_version .parse ("3.1.0" )
307+
302308_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
303309_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
304310_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
@@ -408,6 +414,11 @@ def _start_response(status, response_headers, *args, **kwargs):
408414 return start_response (status , response_headers , * args , ** kwargs )
409415
410416 result = wsgi_app (wrapped_app_environ , _start_response )
417+
418+ # Note: Streaming response context cleanup is now handled in the Flask teardown function
419+ # (_wrapped_teardown_request) to ensure proper cleanup following Logfire's recommendations
420+ # for OpenTelemetry generator context management
421+
411422 if should_trace :
412423 duration_s = default_timer () - start
413424 if duration_histogram_old :
@@ -433,6 +444,7 @@ def _start_response(status, response_headers, *args, **kwargs):
433444 duration_histogram_new .record (
434445 max (duration_s , 0 ), duration_attrs_new
435446 )
447+
436448 active_requests_counter .add (- 1 , active_requests_count_attrs )
437449 return result
438450
@@ -537,6 +549,7 @@ def _teardown_request(exc):
537549 return
538550
539551 activation = flask .request .environ .get (_ENVIRON_ACTIVATION_KEY )
552+ token = flask .request .environ .get (_ENVIRON_TOKEN )
540553
541554 original_reqctx_ref = flask .request .environ .get (
542555 _ENVIRON_REQCTX_REF_KEY
@@ -554,15 +567,79 @@ def _teardown_request(exc):
554567 # like any decorated with `flask.copy_current_request_context`.
555568
556569 return
557- if exc is None :
558- activation .__exit__ (None , None , None )
559- else :
560- activation .__exit__ (
561- type (exc ), exc , getattr (exc , "__traceback__" , None )
570+
571+ try :
572+ # For Flask 3.1+, check if this is a streaming response that might
573+ # have already been cleaned up to prevent double cleanup
574+ # Only check for streaming in Flask 3.1+ and Python 3.10+ to avoid interference with older versions
575+ is_flask_31_plus = _IS_FLASK_31_PLUS and sys .version_info >= (
576+ 3 ,
577+ 10 ,
562578 )
563579
564- if flask .request .environ .get (_ENVIRON_TOKEN , None ):
565- context .detach (flask .request .environ .get (_ENVIRON_TOKEN ))
580+ is_streaming = False
581+ if is_flask_31_plus :
582+ try :
583+ # Additional safety check: verify we're in a Flask request context
584+ if hasattr (flask , "request" ) and hasattr (
585+ flask .request , "response"
586+ ):
587+ is_streaming = (
588+ hasattr (flask .request , "response" )
589+ and flask .request .response
590+ and hasattr (flask .request .response , "stream" )
591+ and flask .request .response .stream
592+ )
593+ except (RuntimeError , AttributeError ):
594+ # Not in a proper Flask request context, don't check for streaming
595+ is_streaming = False
596+
597+ if is_flask_31_plus and is_streaming :
598+ # For Flask 3.1+ streaming responses, ensure OpenTelemetry contexts are cleaned up
599+ # This addresses the generator context leak issues documented by Logfire
600+ # (open-telemetry/opentelemetry-python#2606)
601+ try :
602+ context .detach (token )
603+ if hasattr (activation , "__exit__" ):
604+ activation .__exit__ (None , None , None )
605+
606+ # Mark as cleaned up
607+ flask .request .environ [_ENVIRON_ACTIVATION_KEY ] = None
608+ flask .request .environ [_ENVIRON_TOKEN ] = None
609+
610+ _logger .debug (
611+ "Streaming response context cleanup completed in teardown function"
612+ )
613+
614+ except (
615+ RuntimeError ,
616+ ValueError ,
617+ TypeError ,
618+ AttributeError ,
619+ ) as cleanup_exc :
620+ _logger .debug (
621+ "Teardown streaming context cleanup failed: %s" ,
622+ cleanup_exc ,
623+ )
624+ return
625+
626+ if exc is None :
627+ activation .__exit__ (None , None , None )
628+ else :
629+ activation .__exit__ (
630+ type (exc ), exc , getattr (exc , "__traceback__" , None )
631+ )
632+
633+ if token :
634+ context .detach (token )
635+
636+ except (RuntimeError , AttributeError , ValueError ) as teardown_exc :
637+ # Log the error but don't raise it to avoid breaking the request handling
638+ _logger .debug (
639+ "Error during request teardown: %s" ,
640+ teardown_exc ,
641+ exc_info = True ,
642+ )
566643
567644 return _teardown_request
568645
0 commit comments