Repository: scoutapp/scout_apm_python

Issue:
Capture Celery Time-in-queue



Discussion:

Resolving patch:
diff --git a/src/scout_apm/celery.py b/src/scout_apm/celery.py
--- a/src/scout_apm/celery.py
+++ b/src/scout_apm/celery.py
@@ -1,16 +1,34 @@
 # coding=utf-8
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-from celery.signals import task_postrun, task_prerun
+import datetime as dt
+
+from celery.signals import before_task_publish, task_postrun, task_prerun
 
 import scout_apm.core
+from scout_apm.compat import datetime_to_timestamp
 from scout_apm.core.tracked_request import TrackedRequest
 
 
+def before_publish_callback(headers=None, properties=None, **kwargs):
+    if "scout_task_start" not in headers:
+        headers["scout_task_start"] = datetime_to_timestamp(dt.datetime.utcnow())
+
+
 def prerun_callback(task=None, **kwargs):
     tracked_request = TrackedRequest.instance()
     tracked_request.mark_real_request()
 
+    start = getattr(task.request, "scout_task_start", None)
+    if start is not None:
+        now = datetime_to_timestamp(dt.datetime.utcnow())
+        try:
+            queue_time = now - start
+        except TypeError:
+            pass
+        else:
+            tracked_request.tag("queue_time", queue_time)
+
     delivery_info = task.request.delivery_info
     tracked_request.tag("is_eager", delivery_info.get("is_eager", False))
     tracked_request.tag("exchange", delivery_info.get("exchange", "unknown"))
@@ -30,10 +48,12 @@ def install():
     if not installed:
         return
 
+    before_task_publish.connect(before_publish_callback)
     task_prerun.connect(prerun_callback)
     task_postrun.connect(postrun_callback)
 
 
 def uninstall():
+    before_task_publish.disconnect(before_publish_callback)
     task_prerun.disconnect(prerun_callback)
     task_postrun.disconnect(postrun_callback)


Test patch:
diff --git a/tests/integration/test_celery.py b/tests/integration/test_celery.py
--- a/tests/integration/test_celery.py
+++ b/tests/integration/test_celery.py
@@ -84,12 +84,34 @@ def test_hello_worker(celery_app, celery_worker, tracked_requests):
     assert tracked_request.tags["exchange"] == ""
     assert tracked_request.tags["routing_key"] == "celery"
     assert tracked_request.tags["queue"] == "unknown"
+    assert (
+        0.0 <= tracked_request.tags["queue_time"] < 60.0
+    )  # Assume test took <60 seconds
     assert tracked_request.active_spans == []
     assert len(tracked_request.complete_spans) == 1
     span = tracked_request.complete_spans[0]
     assert span.operation == "Job/tests.integration.test_celery.hello"
 
 
+@skip_unless_celery_4_plus
+def test_hello_worker_header_preset(celery_app, celery_worker, tracked_requests):
+    with app_with_scout(app=celery_app) as app:
+        result = (
+            app.tasks["tests.integration.test_celery.hello"]
+            .apply_async(headers={"scout_task_start": "an evil string"})
+            .get()
+        )
+
+    assert result == "Hello World!"
+    assert len(tracked_requests) == 1
+    tracked_request = tracked_requests[0]
+    assert tracked_request.active_spans == []
+    assert len(tracked_request.complete_spans) == 1
+    span = tracked_request.complete_spans[0]
+    assert span.operation == "Job/tests.integration.test_celery.hello"
+    assert "queue_time" not in span.tags
+
+
 def test_no_monitor(tracked_requests):
     # With an empty config, "monitor" defaults to False.
     with app_with_scout(config={}) as app: