Skip to content

Commit 5c49c87

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

File tree

3 files changed

+444
-8
lines changed

3 files changed

+444
-8
lines changed

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

Lines changed: 84 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
@@ -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

instrumentation/opentelemetry-instrumentation-flask/tests/base_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from concurrent.futures import ThreadPoolExecutor, as_completed
15+
from concurrent.futures import ThreadPoolExecutor, as_completed # pylint: disable=E0611
1616
from random import randint
1717

1818
import flask

0 commit comments

Comments
 (0)