Skip to content

Commit f9f2ad4

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

File tree

2 files changed

+502
-8
lines changed

2 files changed

+502
-8
lines changed

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

Lines changed: 136 additions & 8 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,66 @@ 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+
if package_version.parse(flask_version) < package_version.parse("3.1.0"):
347+
return
348+
349+
# Only enable streaming context cleanup for Python 3.10+ to avoid compatibility issues
350+
# with older Python versions that have different context management behavior
351+
if sys.version_info < (3, 10):
352+
return
353+
354+
activation = environ.get(_ENVIRON_ACTIVATION_KEY)
355+
token = environ.get(_ENVIRON_TOKEN)
356+
357+
if not activation or not token:
358+
return
359+
360+
# Additional safety check - only proceed if we haven't already cleaned up
361+
if (
362+
environ.get(_ENVIRON_ACTIVATION_KEY) is None
363+
or environ.get(_ENVIRON_TOKEN) is None
364+
):
365+
return
366+
367+
try:
368+
# Clean up the context token safely
369+
if token:
370+
try:
371+
context.detach(token)
372+
except RuntimeError as exc:
373+
# Token has already been used - this can happen in Flask 3.1+
374+
# with streaming responses, so we just log and continue
375+
_logger.debug("Token already detached, continuing: %s", exc)
376+
# If detach failed, don't proceed with activation cleanup
377+
return
378+
379+
# Clean up the activation
380+
if hasattr(activation, "__exit__"):
381+
try:
382+
activation.__exit__(None, None, None)
383+
except (RuntimeError, AttributeError) as exc:
384+
_logger.debug("Error during activation cleanup: %s", exc)
385+
386+
# Mark that we've handled the cleanup to prevent double cleanup in teardown
387+
environ[_ENVIRON_ACTIVATION_KEY] = None
388+
environ[_ENVIRON_TOKEN] = None
389+
390+
except (RuntimeError, ValueError, TypeError) as exc:
391+
# Log the error but don't raise it to avoid breaking the response
392+
_logger.debug(
393+
"Error during streaming context cleanup: %s", exc, exc_info=True
394+
)
395+
396+
336397
def _rewrapped_app(
337398
wsgi_app,
338399
active_requests_counter,
@@ -408,6 +469,37 @@ def _start_response(status, response_headers, *args, **kwargs):
408469
return start_response(status, response_headers, *args, **kwargs)
409470

410471
result = wsgi_app(wrapped_app_environ, _start_response)
472+
473+
# For Flask 3.1+, check if we need to handle streaming response context cleanup
474+
# Only run this logic in Flask 3.1+ and Python 3.10+ to avoid any interference with older versions
475+
if (
476+
should_trace
477+
and package_version.parse(flask_version)
478+
>= package_version.parse("3.1.0")
479+
and sys.version_info >= (3, 10)
480+
):
481+
# Only call streaming context cleanup for actual streaming responses
482+
is_streaming = (
483+
hasattr(flask.request, "response")
484+
and flask.request.response
485+
and hasattr(flask.request.response, "stream")
486+
and flask.request.response.stream
487+
)
488+
if is_streaming:
489+
try:
490+
_ensure_streaming_context_cleanup(wrapped_app_environ)
491+
except (
492+
RuntimeError,
493+
ValueError,
494+
TypeError,
495+
AttributeError,
496+
) as exc:
497+
# Ensure our Flask 3.1+ logic never interferes with normal request processing
498+
_logger.debug(
499+
"Flask 3.1+ streaming context cleanup failed, continuing: %s",
500+
exc,
501+
)
502+
411503
if should_trace:
412504
duration_s = default_timer() - start
413505
if duration_histogram_old:
@@ -433,6 +525,7 @@ def _start_response(status, response_headers, *args, **kwargs):
433525
duration_histogram_new.record(
434526
max(duration_s, 0), duration_attrs_new
435527
)
528+
436529
active_requests_counter.add(-1, active_requests_count_attrs)
437530
return result
438531

@@ -537,6 +630,7 @@ def _teardown_request(exc):
537630
return
538631

539632
activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
633+
token = flask.request.environ.get(_ENVIRON_TOKEN)
540634

541635
original_reqctx_ref = flask.request.environ.get(
542636
_ENVIRON_REQCTX_REF_KEY
@@ -554,15 +648,49 @@ def _teardown_request(exc):
554648
# like any decorated with `flask.copy_current_request_context`.
555649

556650
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)
562-
)
563651

564-
if flask.request.environ.get(_ENVIRON_TOKEN, None):
565-
context.detach(flask.request.environ.get(_ENVIRON_TOKEN))
652+
try:
653+
# For Flask 3.1+, check if this is a streaming response that might
654+
# have already been cleaned up to prevent double cleanup
655+
# Only check for streaming in Flask 3.1+ and Python 3.10+ to avoid interference with older versions
656+
is_flask_31_plus = package_version.parse(
657+
flask_version
658+
) >= package_version.parse("3.1.0") and sys.version_info >= (3, 10)
659+
660+
is_streaming = False
661+
if is_flask_31_plus:
662+
is_streaming = (
663+
hasattr(flask.request, "response")
664+
and flask.request.response
665+
and hasattr(flask.request.response, "stream")
666+
and flask.request.response.stream
667+
)
668+
669+
if is_flask_31_plus and is_streaming:
670+
# For streaming responses in Flask 3.1+, the context might have been
671+
# cleaned up already in _ensure_streaming_context_cleanup
672+
# Mark the activation and token as None to prevent double cleanup
673+
flask.request.environ[_ENVIRON_ACTIVATION_KEY] = None
674+
flask.request.environ[_ENVIRON_TOKEN] = None
675+
return
676+
677+
if exc is None:
678+
activation.__exit__(None, None, None)
679+
else:
680+
activation.__exit__(
681+
type(exc), exc, getattr(exc, "__traceback__", None)
682+
)
683+
684+
if token:
685+
context.detach(token)
686+
687+
except (RuntimeError, AttributeError, ValueError) as teardown_exc:
688+
# Log the error but don't raise it to avoid breaking the request handling
689+
_logger.debug(
690+
"Error during request teardown: %s",
691+
teardown_exc,
692+
exc_info=True,
693+
)
566694

567695
return _teardown_request
568696

0 commit comments

Comments
 (0)