Repository: scoutapp/scout_apm_python
Pull request #798 (2024-09-17 23:32:23)

Set operation for Job spans
## Changes
- Sets `TrackedRequest.operation` attribute when we create the `Job/` spans in the integrations. 
- This will facilitate linking Jobs back to relevant logs in Scout. 


Patch:
diff --git a/setup.py b/setup.py
index c95a98d..1db500b 100644
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,7 @@ else:
 
 setup(
     name="scout_apm",
-    version="3.2.0",
+    version="3.2.1",
     description="Scout Application Performance Monitoring Agent",
     long_description=long_description,
     long_description_content_type="text/markdown",
diff --git a/src/scout_apm/api/__init__.py b/src/scout_apm/api/__init__.py
index 1a17230..b3f810b 100644
--- a/src/scout_apm/api/__init__.py
+++ b/src/scout_apm/api/__init__.py
@@ -97,6 +97,7 @@ class Transaction(AsyncDecoratorMixin, ContextDecorator):
         operation = text(kind) + "/" + text(name)
 
         tracked_request = TrackedRequest.instance()
+        tracked_request.operation = operation
         tracked_request.is_real_request = True
         span = tracked_request.start_span(
             operation=operation, should_capture_backtrace=False
diff --git a/src/scout_apm/celery.py b/src/scout_apm/celery.py
index 0dcf07d..6c7d4d4 100644
--- a/src/scout_apm/celery.py
+++ b/src/scout_apm/celery.py
@@ -54,7 +54,9 @@ def task_prerun_callback(task=None, **kwargs):
         tracked_request.tag("routing_key", delivery_info.get("routing_key", "unknown"))
         tracked_request.tag("queue", delivery_info.get("queue", "unknown"))
 
-    tracked_request.start_span(operation=("Job/" + task.name))
+    operation = "Job/" + task.name
+    tracked_request.start_span(operation=operation)
+    tracked_request.operation = operation
 
 
 def task_postrun_callback(task=None, **kwargs):
diff --git a/src/scout_apm/dramatiq.py b/src/scout_apm/dramatiq.py
index f30edee..21f60e4 100644
--- a/src/scout_apm/dramatiq.py
+++ b/src/scout_apm/dramatiq.py
@@ -17,7 +17,9 @@ class ScoutMiddleware(dramatiq.Middleware):
         tracked_request = TrackedRequest.instance()
         tracked_request.tag("queue", message.queue_name)
         tracked_request.tag("message_id", message.message_id)
-        tracked_request.start_span(operation="Job/" + message.actor_name)
+        operation = "Job/" + message.actor_name
+        tracked_request.start_span(operation=operation)
+        tracked_request.operation = operation
 
     def after_process_message(self, broker, message, result=None, exception=None):
         if self._do_nothing:
diff --git a/src/scout_apm/huey.py b/src/scout_apm/huey.py
index f727cd0..eee6c2f 100644
--- a/src/scout_apm/huey.py
+++ b/src/scout_apm/huey.py
@@ -30,6 +30,7 @@ def scout_on_pre_execute(task):
 
     operation = "Job/{}.{}".format(task.__module__, task.__class__.__name__)
     tracked_request.start_span(operation=operation)
+    tracked_request.operation = operation
 
 
 def scout_on_post_execute(task, task_value, exception):
diff --git a/src/scout_apm/rq.py b/src/scout_apm/rq.py
index 591f673..d92e4dd 100644
--- a/src/scout_apm/rq.py
+++ b/src/scout_apm/rq.py
@@ -67,8 +67,9 @@ def wrap_perform(wrapped, instance, args, kwargs):
     tracked_request.tag("queue", instance.origin)
     queue_time = (dt.datetime.utcnow() - instance.enqueued_at).total_seconds()
     tracked_request.tag("queue_time", queue_time)
-
-    with tracked_request.span(operation="Job/{}".format(instance.func_name)):
+    operation = "Job/{}".format(instance.func_name)
+    tracked_request.operation = operation
+    with tracked_request.span(operation=operation):
         try:
             return wrapped(*args, **kwargs)
         except Exception:


Test patch:
diff --git a/tests/integration/test_api.py b/tests/integration/test_api.py
index 580fa1b..2142624 100644
--- a/tests/integration/test_api.py
+++ b/tests/integration/test_api.py
@@ -164,6 +164,7 @@ async def test_web_transaction_decorator_async(tracked_request):
     assert len(tracked_request.complete_spans) == 2
     assert tracked_request.complete_spans[0].operation == "Custom/Foo"
     assert tracked_request.complete_spans[1].operation == "Controller/Bar"
+    assert tracked_request.operation == "Controller/Bar"
 
 
 @pytest.mark.asyncio
@@ -183,6 +184,7 @@ async def test_web_transaction_decorator_async_misconfigured(tracked_request):
     assert len(tracked_request.active_spans) == 0
     assert len(tracked_request.complete_spans) == 1
     assert tracked_request.complete_spans[0].operation == "Controller/Bar"
+    assert tracked_request.operation == "Controller/Bar"
 
 
 def test_web_transaction_decorator_async_for_sync_function(tracked_request):
@@ -213,6 +215,7 @@ async def test_background_transaction_decorator_async(tracked_request):
     assert len(tracked_request.complete_spans) == 2
     assert tracked_request.complete_spans[0].operation == "Custom/Foo"
     assert tracked_request.complete_spans[1].operation == "Job/Bar"
+    assert tracked_request.operation == "Job/Bar"
 
 
 @pytest.mark.asyncio
@@ -232,6 +235,7 @@ async def test_background_transaction_decorator_async_misconfigured(tracked_requ
     assert len(tracked_request.active_spans) == 0
     assert len(tracked_request.complete_spans) == 1
     assert tracked_request.complete_spans[0].operation == "Job/Bar"
+    assert tracked_request.operation == "Job/Bar"
 
 
 def test_background_transaction_decorator_async_for_sync_function(tracked_request):
diff --git a/tests/integration/test_celery.py b/tests/integration/test_celery.py
index eef0c5d..7814be2 100644
--- a/tests/integration/test_celery.py
+++ b/tests/integration/test_celery.py
@@ -96,6 +96,7 @@ def test_hello_eager(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/tests.integration.test_celery.hello"
+    assert tracked_request.operation == "Job/tests.integration.test_celery.hello"
 
 
 def test_error_task(tracked_requests):
@@ -110,6 +111,7 @@ def test_error_task(tracked_requests):
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/tests.integration.test_celery.crash"
     assert tracked_request.tags["error"]
+    assert tracked_request.operation == "Job/tests.integration.test_celery.crash"
 
 
 def test_error_task_error_monitor(error_monitor_errors, mock_get_safe_settings):
@@ -255,6 +257,7 @@ def test_hello_worker(celery_app, celery_worker, tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/tests.integration.test_celery.hello"
+    assert tracked_request.operation == "Job/tests.integration.test_celery.hello"
 
 
 @skip_unless_celery_4_plus
@@ -274,6 +277,7 @@ def test_hello_worker_header_preset(celery_app, celery_worker, tracked_requests)
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/tests.integration.test_celery.hello"
     assert "scout.job_queue_time_ns" not in span.tags
+    assert tracked_request.operation == "Job/tests.integration.test_celery.hello"
 
 
 @skip_unless_celery_4_plus
diff --git a/tests/integration/test_dramatiq.py b/tests/integration/test_dramatiq.py
index a3b80d8..769491d 100644
--- a/tests/integration/test_dramatiq.py
+++ b/tests/integration/test_dramatiq.py
@@ -79,6 +79,7 @@ def test_hello(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/hello"
+    assert tracked_request.operation == "Job/hello"
 
 
 def test_fail(tracked_requests):
@@ -95,6 +96,7 @@ def test_fail(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/fail"
+    assert tracked_request.operation == "Job/fail"
 
 
 def test_not_installed(tracked_requests):
diff --git a/tests/integration/test_huey.py b/tests/integration/test_huey.py
index ae234ce..d5f9d78 100644
--- a/tests/integration/test_huey.py
+++ b/tests/integration/test_huey.py
@@ -67,6 +67,7 @@ def test_hello(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/tests.integration.test_huey.hello"
+    assert tracked_request.operation == "Job/tests.integration.test_huey.hello"
 
 
 def test_retry_once(tracked_requests):
@@ -82,6 +83,7 @@ def test_retry_once(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/tests.integration.test_huey.retry_once"
+    assert tracked_request.operation == "Job/tests.integration.test_huey.retry_once"
 
 
 def test_fail(tracked_requests):
@@ -97,6 +99,7 @@ def test_fail(tracked_requests):
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/tests.integration.test_huey.fail"
+    assert tracked_request.operation == "Job/tests.integration.test_huey.fail"
 
 
 def test_cancelled(tracked_requests):
diff --git a/tests/integration/test_rq.py b/tests/integration/test_rq.py
index 28603ab..9029715 100644
--- a/tests/integration/test_rq.py
+++ b/tests/integration/test_rq.py
@@ -88,6 +88,7 @@ def test_hello(redis_conn, tracked_requests):
         tracked_request.complete_spans[1].operation
         == "Job/tests.integration.test_rq.hello"
     )
+    assert tracked_request.operation == "Job/tests.integration.test_rq.hello"
 
 
 def test_fail(redis_conn, tracked_requests):
@@ -107,6 +108,7 @@ def test_fail(redis_conn, tracked_requests):
         tracked_request.complete_spans[1].operation
         == "Job/tests.integration.test_rq.fail"
     )
+    assert tracked_request.operation == "Job/tests.integration.test_rq.fail"
 
 
 def test_no_monitor(redis_conn, tracked_requests):