Repository: scoutapp/scout_apm_python
Pull request #402 (2019-11-27 09:23:06)

Hoist shared fixture core_agent_manager
This is used in multiple files so can live in the root conftest.


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/conftest.py b/tests/conftest.py
index 29d3060..c770baf 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 
 import logging
 import os
+import subprocess
 import sys
 
 import pytest
@@ -10,7 +11,8 @@ import wrapt
 from webtest import TestApp
 
 from scout_apm.core import socket as scout_apm_core_socket
-from scout_apm.core.config import SCOUT_PYTHON_VALUES
+from scout_apm.core.config import SCOUT_PYTHON_VALUES, scout_config
+from scout_apm.core.core_agent_manager import CoreAgentManager
 from scout_apm.core.tracked_request import TrackedRequest
 from tests.compat import TemporaryDirectory
 
@@ -111,6 +113,40 @@ def core_agent_dir():
         yield temp_dir
 
 
+@pytest.fixture
+def core_agent_manager(core_agent_dir):
+    # Shorten path to socket to prevent core-agent from failing with:
+    #   Error opening listener on socket: Custom { kind: InvalidInput,
+    #   error: StringError("path must be shorter than SUN_LEN") }
+    socket_path = "{}/test.sock".format(core_agent_dir)
+    scout_config.set(core_agent_dir=core_agent_dir, socket_path=socket_path)
+    core_agent_manager = CoreAgentManager()
+    try:
+        yield core_agent_manager
+    finally:
+        assert not is_running(core_agent_manager)
+        scout_config.reset_all()
+
+
+def is_running(core_agent_manager):
+    if core_agent_manager.core_agent_bin_path is None:
+        return False
+    agent_binary = [core_agent_manager.core_agent_bin_path, "probe"]
+    socket_path = core_agent_manager.socket_path()
+    probe = subprocess.check_output(agent_binary + socket_path)
+    if b"Agent found" in probe:
+        return True
+    if b"Agent Not Running" in probe:
+        return False
+    raise AssertionError("cannot tell if the core agent is running")
+
+
+def shutdown(core_agent_manager):
+    agent_binary = [core_agent_manager.core_agent_bin_path, "shutdown"]
+    socket_path = core_agent_manager.socket_path()
+    subprocess.check_call(agent_binary + socket_path)
+
+
 # Make all timeouts shorter so that tests exercising them run faster.
 @pytest.fixture(autouse=True, scope="session")
 def short_timeouts():
diff --git a/tests/integration/core/test_core_agent_manager.py b/tests/integration/core/test_core_agent_manager.py
index 0db2495..5f9487e 100644
--- a/tests/integration/core/test_core_agent_manager.py
+++ b/tests/integration/core/test_core_agent_manager.py
@@ -2,49 +2,11 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import logging
-import subprocess
 import time
 
-import pytest
-
 from scout_apm.core.config import scout_config
-from scout_apm.core.core_agent_manager import CoreAgentManager
 from tests.compat import mock
-
-
-@pytest.fixture
-def core_agent_manager(core_agent_dir):
-    # Shorten path to socket to prevent core-agent from failing with:
-    #   Error opening listener on socket: Custom { kind: InvalidInput,
-    #   error: StringError("path must be shorter than SUN_LEN") }
-    socket_path = "{}/test.sock".format(core_agent_dir)
-    scout_config.set(core_agent_dir=core_agent_dir, socket_path=socket_path)
-    core_agent_manager = CoreAgentManager()
-    try:
-        yield core_agent_manager
-    finally:
-        assert not is_running(core_agent_manager)
-        scout_config.reset_all()
-
-
-def is_running(core_agent_manager):
-    if core_agent_manager.core_agent_bin_path is None:
-        return False
-    agent_binary = [core_agent_manager.core_agent_bin_path, "probe"]
-    socket_path = core_agent_manager.socket_path()
-    probe = subprocess.check_output(agent_binary + socket_path)
-    if b"Agent found" in probe:
-        return True
-    if b"Agent Not Running" in probe:
-        return False
-    raise AssertionError("cannot tell if the core agent is running")
-
-
-def shutdown(core_agent_manager):
-    agent_binary = [core_agent_manager.core_agent_bin_path, "shutdown"]
-    socket_path = core_agent_manager.socket_path()
-    subprocess.check_call(agent_binary + socket_path)
-
+from tests.conftest import is_running, shutdown
 
 # Tests must execute in the order in which they are defined.
 
diff --git a/tests/integration/core/test_socket.py b/tests/integration/core/test_socket.py
index 1a62376..436c74a 100644
--- a/tests/integration/core/test_socket.py
+++ b/tests/integration/core/test_socket.py
@@ -7,15 +7,11 @@ import pytest
 
 from scout_apm.core.socket import CoreAgentSocket
 from tests.compat import mock
-from tests.integration.core.test_core_agent_manager import (  # noqa: F401,F811
-    core_agent_manager,
-    is_running,
-    shutdown,
-)
+from tests.conftest import is_running, shutdown
 
 
-@pytest.fixture  # noqa: F811
-def running_agent(core_agent_manager):  # noqa: F811
+@pytest.fixture
+def running_agent(core_agent_manager):
     assert not is_running(core_agent_manager)
     assert core_agent_manager.launch()
     time.sleep(0.01)  # wait for agent to start running
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",
     ]