Skip to content

Commit bc265d5

Browse files
committed
chore: compatiable flask 3.1+
1 parent bd3c1f2 commit bc265d5

File tree

2 files changed

+524
-7
lines changed

2 files changed

+524
-7
lines changed

instrumentation/opentelemetry-instrumentation-flask/src/opentelemetry/instrumentation/flask/__init__.py

Lines changed: 153 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ def response_hook(span: Span, status: str, response_headers: List):
254254
---
255255
"""
256256

257+
import sys
257258
import weakref
258259
from logging import getLogger
259260
from time import time_ns
@@ -333,6 +334,68 @@ def get_default_span_name():
333334
return span_name
334335

335336

337+
def _ensure_streaming_context_cleanup(environ):
338+
"""
339+
Ensure proper context cleanup for streaming responses in Flask 3.1+.
340+
341+
This function checks if the response is a streaming response and ensures
342+
that context tokens are properly cleaned up to prevent token reuse issues.
343+
Only applies to Flask 3.1+ and Python 3.10+ for compatibility reasons.
344+
"""
345+
# Double-check Flask version - this should only run in Flask 3.1+
346+
# Use the same check method as other places for consistency
347+
if not (hasattr(flask, '__version__') and
348+
package_version.parse(flask.__version__) >= package_version.parse("3.1.0")):
349+
return
350+
351+
# Only enable streaming context cleanup for Python 3.10+ to avoid compatibility issues
352+
# with older Python versions that have different context management behavior
353+
if sys.version_info < (3, 10):
354+
return
355+
356+
activation = environ.get(_ENVIRON_ACTIVATION_KEY)
357+
token = environ.get(_ENVIRON_TOKEN)
358+
359+
if not activation or not token:
360+
return
361+
362+
# Additional safety check - only proceed if we haven't already cleaned up
363+
if (
364+
environ.get(_ENVIRON_ACTIVATION_KEY) is None
365+
or environ.get(_ENVIRON_TOKEN) is None
366+
):
367+
return
368+
369+
try:
370+
# Clean up the context token safely
371+
if token:
372+
try:
373+
context.detach(token)
374+
except RuntimeError as exc:
375+
# Token has already been used - this can happen in Flask 3.1+
376+
# with streaming responses, so we just log and continue
377+
_logger.debug("Token already detached, continuing: %s", exc)
378+
# If detach failed, don't proceed with activation cleanup
379+
return
380+
381+
# Clean up the activation
382+
if hasattr(activation, "__exit__"):
383+
try:
384+
activation.__exit__(None, None, None)
385+
except (RuntimeError, AttributeError) as exc:
386+
_logger.debug("Error during activation cleanup: %s", exc)
387+
388+
# Mark that we've handled the cleanup to prevent double cleanup in teardown
389+
environ[_ENVIRON_ACTIVATION_KEY] = None
390+
environ[_ENVIRON_TOKEN] = None
391+
392+
except (RuntimeError, ValueError, TypeError) as exc:
393+
# Log the error but don't raise it to avoid breaking the response
394+
_logger.debug(
395+
"Error during streaming context cleanup: %s", exc, exc_info=True
396+
)
397+
398+
336399
def _rewrapped_app(
337400
wsgi_app,
338401
active_requests_counter,
@@ -408,6 +471,43 @@ def _start_response(status, response_headers, *args, **kwargs):
408471
return start_response(status, response_headers, *args, **kwargs)
409472

410473
result = wsgi_app(wrapped_app_environ, _start_response)
474+
475+
# For Flask 3.1+, check if we need to handle streaming response context cleanup
476+
# Only run this logic in Flask 3.1+ and Python 3.10+ to avoid any interference with older versions
477+
# Use very conservative checks to ensure we never interfere with Flask < 3.1
478+
if (
479+
should_trace
480+
and hasattr(flask, '__version__') # Ensure Flask has version attribute
481+
and package_version.parse(flask.__version__)
482+
>= package_version.parse("3.1.0") # Only Flask 3.1+
483+
and sys.version_info >= (3, 10) # Only Python 3.10+
484+
):
485+
# Only call streaming context cleanup for actual streaming responses
486+
# Add additional safety checks to ensure we're really in Flask 3.1+
487+
try:
488+
# Additional safety check: verify we're in a Flask request context
489+
if (hasattr(flask, 'request') and
490+
hasattr(flask.request, 'response')):
491+
is_streaming = (
492+
hasattr(flask.request, "response")
493+
and flask.request.response
494+
and hasattr(flask.request.response, "stream")
495+
and flask.request.response.stream
496+
)
497+
if is_streaming:
498+
_ensure_streaming_context_cleanup(wrapped_app_environ)
499+
except (
500+
RuntimeError,
501+
ValueError,
502+
TypeError,
503+
AttributeError,
504+
) as exc:
505+
# Ensure our Flask 3.1+ logic never interferes with normal request processing
506+
_logger.debug(
507+
"Flask 3.1+ streaming context cleanup failed, continuing: %s",
508+
exc,
509+
)
510+
411511
if should_trace:
412512
duration_s = default_timer() - start
413513
if duration_histogram_old:
@@ -433,6 +533,7 @@ def _start_response(status, response_headers, *args, **kwargs):
433533
duration_histogram_new.record(
434534
max(duration_s, 0), duration_attrs_new
435535
)
536+
436537
active_requests_counter.add(-1, active_requests_count_attrs)
437538
return result
438539

@@ -537,6 +638,7 @@ def _teardown_request(exc):
537638
return
538639

539640
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
641+
token = flask.request.environ.get(_ENVIRON_TOKEN)
540642

541643
original_reqctx_ref = flask.request.environ.get(
542644
_ENVIRON_REQCTX_REF_KEY
@@ -554,15 +656,59 @@ def _teardown_request(exc):
554656
# like any decorated with `flask.copy_current_request_context`.
555657

556658
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)
659+
660+
try:
661+
# For Flask 3.1+, check if this is a streaming response that might
662+
# have already been cleaned up to prevent double cleanup
663+
# Only check for streaming in Flask 3.1+ and Python 3.10+ to avoid interference with older versions
664+
# Use very conservative checks to ensure we never interfere with Flask < 3.1
665+
is_flask_31_plus = (
666+
hasattr(flask, '__version__') # Ensure Flask has version attribute
667+
and package_version.parse(flask.__version__) >= package_version.parse("3.1.0")
668+
and sys.version_info >= (3, 10)
562669
)
563670

564-
if flask.request.environ.get(_ENVIRON_TOKEN, None):
565-
context.detach(flask.request.environ.get(_ENVIRON_TOKEN))
671+
is_streaming = False
672+
if is_flask_31_plus:
673+
try:
674+
# Additional safety check: verify we're in a Flask request context
675+
if (hasattr(flask, 'request') and
676+
hasattr(flask.request, 'response')):
677+
is_streaming = (
678+
hasattr(flask.request, "response")
679+
and flask.request.response
680+
and hasattr(flask.request.response, "stream")
681+
and flask.request.response.stream
682+
)
683+
except (RuntimeError, AttributeError):
684+
# Not in a proper Flask request context, don't check for streaming
685+
is_streaming = False
686+
687+
if is_flask_31_plus and is_streaming:
688+
# For streaming responses in Flask 3.1+, the context might have been
689+
# cleaned up already in _ensure_streaming_context_cleanup
690+
# Mark the activation and token as None to prevent double cleanup
691+
flask.request.environ[_ENVIRON_ACTIVATION_KEY] = None
692+
flask.request.environ[_ENVIRON_TOKEN] = None
693+
return
694+
695+
if exc is None:
696+
activation.__exit__(None, None, None)
697+
else:
698+
activation.__exit__(
699+
type(exc), exc, getattr(exc, "__traceback__", None)
700+
)
701+
702+
if token:
703+
context.detach(token)
704+
705+
except (RuntimeError, AttributeError, ValueError) as teardown_exc:
706+
# Log the error but don't raise it to avoid breaking the request handling
707+
_logger.debug(
708+
"Error during request teardown: %s",
709+
teardown_exc,
710+
exc_info=True,
711+
)
566712

567713
return _teardown_request
568714

0 commit comments

Comments
 (0)