Repository: scoutapp/scout_apm_python
Pull request #796 (2024-09-09 23:15:05)

add operation to TrackedRequest
## Changes
- Adds `operation` as a property to the TrackedRequest object in the supported frameworks. 
- This will be used by the scout logging agent to pull the operation of the current TrackedRequest into the log context. 


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 21ad6e6..b231953 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -53,7 +53,7 @@ jobs:
 
   build_macos_wheels:
     name: Build macos wheels (cross-compiles arm64)
-    runs-on: macos-11
+    runs-on: macos-13
     steps:
       - uses: actions/checkout@v3
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78517b9..8ca165a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## Pending
 
+## [3.2.0] 2024-09-12
+### Added
+- "Operation" attribute added to TrackedRequest class to better support development of [scout_apm_python_logging](https://github.com/scoutapp/scout_apm_python_logging)
+
 ## [3.1.0] 2023-12-18
 ### Added
 - Updates Core Agent to v1.5.0
diff --git a/setup.py b/setup.py
index bbbcc8f..c95a98d 100644
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,7 @@ else:
 
 setup(
     name="scout_apm",
-    version="3.1.0",
+    version="3.2.0",
     description="Scout Application Performance Monitoring Agent",
     long_description=long_description,
     long_description_content_type="text/markdown",
diff --git a/src/scout_apm/async_/starlette.py b/src/scout_apm/async_/starlette.py
index e386d50..3b88f7e 100644
--- a/src/scout_apm/async_/starlette.py
+++ b/src/scout_apm/async_/starlette.py
@@ -40,6 +40,7 @@ class ScoutMiddleware:
                     endpoint.__module__,
                     endpoint.__qualname__,
                 )
+                tracked_request.operation = controller_span.operation
             else:
                 # Mark the request as not real
                 tracked_request.is_real_request = False
@@ -90,11 +91,11 @@ def install_background_instrumentation():
         tracked_request = TrackedRequest.instance()
         tracked_request.is_real_request = True
 
-        with tracked_request.span(
-            operation="Job/{}.{}".format(
-                instance.func.__module__, instance.func.__qualname__
-            )
-        ):
+        operation = "Job/{}.{}".format(
+            instance.func.__module__, instance.func.__qualname__
+        )
+        tracked_request.operation = operation
+        with tracked_request.span(operation=operation):
             return await wrapped(*args, **kwargs)
 
     BackgroundTask.__call__ = wrapped_background_call(BackgroundTask.__call__)
diff --git a/src/scout_apm/bottle.py b/src/scout_apm/bottle.py
index b70720a..049adb7 100644
--- a/src/scout_apm/bottle.py
+++ b/src/scout_apm/bottle.py
@@ -71,10 +71,10 @@ def wrap_callback(wrapped, instance, args, kwargs):
         "x-request-start", ""
     )
     track_request_queue_time(queue_time, tracked_request)
+    operation = "Controller{}".format(controller_name)
 
-    with tracked_request.span(
-        operation="Controller{}".format(controller_name), should_capture_backtrace=False
-    ):
+    with tracked_request.span(operation=operation):
+        tracked_request.operation = operation
         try:
             value = wrapped(*args, **kwargs)
         except Exception:
diff --git a/src/scout_apm/core/tracked_request.py b/src/scout_apm/core/tracked_request.py
index a3683a8..c692a70 100644
--- a/src/scout_apm/core/tracked_request.py
+++ b/src/scout_apm/core/tracked_request.py
@@ -35,6 +35,7 @@ class TrackedRequest(object):
         "n_plus_one_tracker",
         "hit_max",
         "sent",
+        "operation",
     )
 
     # Stop adding new spans at this point, to avoid exhausting memory
@@ -58,6 +59,7 @@ class TrackedRequest(object):
         self.n_plus_one_tracker = NPlusOneTracker()
         self.hit_max = False
         self.sent = False
+        self.operation = None
         logger.debug("Starting request: %s", self.request_id)
 
     def __repr__(self):
diff --git a/src/scout_apm/django/middleware.py b/src/scout_apm/django/middleware.py
index 4a7066c..2a674e0 100644
--- a/src/scout_apm/django/middleware.py
+++ b/src/scout_apm/django/middleware.py
@@ -122,6 +122,7 @@ class ViewTimingMiddleware(object):
         span = tracked_request.current_span()
         if span is not None:
             span.operation = get_controller_name(request)
+            tracked_request.operation = span.operation
 
     def process_exception(self, request, exception):
         """
diff --git a/src/scout_apm/falcon.py b/src/scout_apm/falcon.py
index 2b4ca43..23f20ed 100644
--- a/src/scout_apm/falcon.py
+++ b/src/scout_apm/falcon.py
@@ -106,6 +106,7 @@ class ScoutMiddleware(object):
         span = tracked_request.start_span(
             operation=operation, should_capture_backtrace=False
         )
+        tracked_request.operation = operation
         req.context.scout_resource_span = span
 
     def _name_operation(self, req, responder, resource):
diff --git a/src/scout_apm/flask/__init__.py b/src/scout_apm/flask/__init__.py
index d77ddfc..a96348a 100644
--- a/src/scout_apm/flask/__init__.py
+++ b/src/scout_apm/flask/__init__.py
@@ -47,6 +47,7 @@ class ScoutApm(object):
 
         tracked_request = TrackedRequest.instance()
         tracked_request.is_real_request = True
+        tracked_request.operation = operation
         request._scout_tracked_request = tracked_request
 
         werkzeug_track_request_data(request, tracked_request)


Test patch:
diff --git a/tests/integration/test_bottle.py b/tests/integration/test_bottle.py
index 9825443..30cb9de 100644
--- a/tests/integration/test_bottle.py
+++ b/tests/integration/test_bottle.py
@@ -32,7 +32,8 @@ def app_with_scout(config=None, catchall=False):
     # Disable running the agent.
     config["scout.core_agent_launch"] = False
 
-    app = Bottle(catchall=catchall)
+    app = Bottle()
+    app.config["catchall"] = catchall
 
     @app.route("/")
     def home():
@@ -169,6 +170,7 @@ def test_hello(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Controller/hello/"
+    assert tracked_request.operation == "Controller/hello/"
 
 
 def test_not_found(tracked_requests):
@@ -190,6 +192,7 @@ def test_server_error(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_requests[0].complete_spans[0]
     assert span.operation == "Controller/crash/"
+    assert tracked_request.operation == "Controller/crash/"
 
 
 def test_return_error(tracked_requests):
@@ -203,6 +206,7 @@ def test_return_error(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_requests[0].complete_spans[0]
     assert span.operation == "Controller/return-error/"
+    assert tracked_request.operation == "Controller/return-error/"
 
 
 def test_named(tracked_requests):
diff --git a/tests/integration/test_django.py b/tests/integration/test_django.py
index bd9f04d..4d80236 100644
--- a/tests/integration/test_django.py
+++ b/tests/integration/test_django.py
@@ -201,6 +201,9 @@ def test_hello(tracked_requests):
         "Controller/tests.integration.django_app.hello",
         "Middleware",
     ]
+    assert (
+        tracked_requests[0].operation == "Controller/tests.integration.django_app.hello"
+    )
 
 
 def test_not_found(tracked_requests):
diff --git a/tests/integration/test_flask.py b/tests/integration/test_flask.py
index cea0403..c7c267e 100644
--- a/tests/integration/test_flask.py
+++ b/tests/integration/test_flask.py
@@ -80,6 +80,7 @@ def test_home(tracked_requests):
     assert tracked_request.tags["path"] == "/"
     span = tracked_request.complete_spans[0]
     assert span.operation == "Controller/tests.integration.test_flask.home"
+    assert tracked_request.operation == "Controller/tests.integration.test_flask.home"
 
 
 def test_home_ignored(tracked_requests):
@@ -154,6 +155,7 @@ def test_hello(tracked_requests):
     assert tracked_request.tags["path"] == "/hello/"
     span = tracked_request.complete_spans[0]
     assert span.operation == "Controller/tests.integration.test_flask.hello"
+    assert tracked_request.operation == "Controller/tests.integration.test_flask.hello"
 
 
 def test_hello_options(tracked_requests):
@@ -168,6 +170,7 @@ def test_hello_options(tracked_requests):
     assert tracked_request.tags["path"] == "/hello/"
     span = tracked_request.complete_spans[0]
     assert span.operation == "Controller/tests.integration.test_flask.hello"
+    assert tracked_request.operation == "Controller/tests.integration.test_flask.hello"
 
 
 def test_not_found(tracked_requests):
@@ -190,6 +193,7 @@ def test_server_error(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Controller/tests.integration.test_flask.crash"
+    assert tracked_request.operation == "Controller/tests.integration.test_flask.crash"
 
 
 def test_return_error(tracked_requests):
@@ -204,6 +208,10 @@ def test_return_error(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Controller/tests.integration.test_flask.return_error"
+    assert (
+        tracked_request.operation
+        == "Controller/tests.integration.test_flask.return_error"
+    )
 
 
 def test_automatic_options(tracked_requests):
@@ -217,6 +225,9 @@ def test_automatic_options(tracked_requests):
     assert [s.operation for s in spans] == [
         "Controller/tests.integration.test_flask.home"
     ]
+    assert (
+        tracked_requests[0].operation == "Controller/tests.integration.test_flask.home"
+    )
 
 
 def test_preprocessor_response(tracked_requests):
diff --git a/tests/integration/test_starlette.py b/tests/integration/test_starlette.py
index a2c5283..503f890 100644
--- a/tests/integration/test_starlette.py
+++ b/tests/integration/test_starlette.py
@@ -130,9 +130,11 @@ async def test_home(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     assert tracked_request.tags["path"] == "/"
     span = tracked_request.complete_spans[0]
-    assert span.operation == (
+    expected_operation = (
         "Controller/tests.integration.test_starlette." + "app_with_scout.<locals>.home"
     )
+    assert tracked_request.operation == expected_operation
+    assert span.operation == expected_operation
 
 
 @pytest.mark.asyncio
@@ -193,10 +195,12 @@ async def test_hello(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     assert tracked_request.tags["path"] == "/hello/"
     span = tracked_request.complete_spans[0]
-    assert span.operation == (
+    expected_operation = (
         "Controller/tests.integration.test_starlette."
         + "app_with_scout.<locals>.HelloEndpoint"
     )
+    assert tracked_request.operation == expected_operation
+    assert span.operation == expected_operation
 
 
 @pytest.mark.asyncio