Repository: scoutapp/scout_apm_python
Pull request #395 (2019-11-25 10:28:34)

Track Flask preprocess_request span
Following #390 I realized it's possible to track this middleware-ish hook which might be of value to our Flask users.

adamchainz: It might be better to move to tracking the preprocessors as separate span around the actual request processing.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c291dcc..7954706 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,7 @@
   ([PR #375](https://github.com/scoutapp/scout_apm_python/pull/375)).
 - Track 5XX responses as errors from all web frameworks
   ([Issue #383](https://github.com/scoutapp/scout_apm_python/issues/383)).
-- Track Flask responses generated by preprocessor functions
+- Track Flask's request preprocessors as a span `PreprocessRequest`
   ([PR #390](https://github.com/scoutapp/scout_apm_python/pull/390)).
 - Track Jinja2 asynchronous template rendering
   ([PR #398](https://github.com/scoutapp/scout_apm_python/pull/398)).
diff --git a/src/scout_apm/flask/__init__.py b/src/scout_apm/flask/__init__.py
index 2118792..3cfc505 100644
--- a/src/scout_apm/flask/__init__.py
+++ b/src/scout_apm/flask/__init__.py
@@ -3,7 +3,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 
 import wrapt
 from flask import current_app
-from flask.globals import _request_ctx_stack
+from flask.globals import request
 
 import scout_apm.core
 from scout_apm.core.config import scout_config
@@ -18,6 +18,7 @@ class ScoutApm(object):
         app.full_dispatch_request = self.wrapped_full_dispatch_request(
             app.full_dispatch_request
         )
+        app.preprocess_request = self.wrapped_preprocess_request(app.preprocess_request)
 
     @wrapt.decorator
     def wrapped_full_dispatch_request(self, wrapped, instance, args, kwargs):
@@ -30,7 +31,6 @@ class ScoutApm(object):
         if self._do_nothing:
             return wrapped(*args, **kwargs)
 
-        request = _request_ctx_stack.top.request
         # Pass on routing exceptions (normally 404's)
         if request.routing_exception is not None:
             return wrapped(*args, **kwargs)
@@ -43,9 +43,11 @@ class ScoutApm(object):
 
         tracked_request = TrackedRequest.instance()
         tracked_request.is_real_request = True
+        request._scout_tracked_request = tracked_request
         span = tracked_request.start_span(
             operation=operation, should_capture_backtrace=False
         )
+        request._scout_view_span = span
         span.tag("name", name)
 
         werkzeug_track_request_data(request, tracked_request)
@@ -62,6 +64,27 @@ class ScoutApm(object):
         finally:
             tracked_request.stop_span()
 
+    @wrapt.decorator
+    def wrapped_preprocess_request(self, wrapped, instance, args, kwargs):
+        tracked_request = getattr(request, "_scout_tracked_request", None)
+        if tracked_request is None:
+            return wrapped(*args, **kwargs)
+
+        # Unlike middleware in other frameworks, using request preprocessors is
+        # less common in Flask, so only add a span if there is any in use
+        have_before_request_funcs = (
+            None in instance.before_request_funcs
+            or request.blueprint in instance.before_request_funcs
+        )
+        if not have_before_request_funcs:
+            return wrapped(*args, **kwargs)
+
+        tracked_request.start_span("PreprocessRequest", should_capture_backtrace=False)
+        try:
+            return wrapped(*args, **kwargs)
+        finally:
+            tracked_request.stop_span()
+
     def extract_flask_settings(self):
         """
         Copies SCOUT_* settings in the app into Scout's config lookup


Test patch:
diff --git a/tests/integration/test_flask.py b/tests/integration/test_flask.py
index d5e4655..1018ca4 100644
--- a/tests/integration/test_flask.py
+++ b/tests/integration/test_flask.py
@@ -227,7 +227,7 @@ def test_automatic_options(tracked_requests):
     ]
 
 
-def test_preprocessor_function_responses_tracked(tracked_requests):
+def test_preprocessor_response(tracked_requests):
     with app_with_scout() as app:
 
         @app.before_request
@@ -239,11 +239,10 @@ def test_preprocessor_function_responses_tracked(tracked_requests):
     assert response.status_int == 418
     assert response.text == "I'm a teapot"
     assert len(tracked_requests) == 1
-    # We only pull the name from routing information. Perhaps there should be
-    # a way of detecting preprocessor naming.
     spans = tracked_requests[0].complete_spans
     assert [s.operation for s in spans] == [
-        "Controller/tests.integration.test_flask.home"
+        "PreprocessRequest",
+        "Controller/tests.integration.test_flask.home",
     ]