Repository: scoutapp/scout_apm_python (python)

Issue:
Track when an exception occurs in a Celery task
Similar to how we do this in other libraries
`tracked_request.tag("error", "true")`


Pull request description:
Track when exceptions occur in Celery on TrackedRequest.
Fixes #649

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9603c25..5ef7a30 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@
 - Improved debug logging for requests. It now includes some summary
   information about the request.
   ([Issue #654](https://github.com/scoutapp/scout_apm_python/issues/654))
+- Set ``TrackedRequest.tags("error", "true")`` when Celery tasks fail.
+  ([Issue #649](https://github.com/scoutapp/scout_apm_python/issues/649))
 
 ## [2.19.0] 2021-05-04
 
diff --git a/src/scout_apm/celery.py b/src/scout_apm/celery.py
index 7bc305c..1512b29 100644
--- a/src/scout_apm/celery.py
+++ b/src/scout_apm/celery.py
@@ -3,7 +3,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 
 import datetime as dt
 
-from celery.signals import before_task_publish, task_postrun, task_prerun
+from celery.signals import before_task_publish, task_failure, task_postrun, task_prerun
 
 import scout_apm.core
 from scout_apm.compat import datetime_to_timestamp
@@ -52,6 +52,11 @@ def task_postrun_callback(task=None, **kwargs):
     tracked_request.stop_span()
 
 
+def task_failure_callback(task_id=None, **kwargs):
+    tracked_request = TrackedRequest.instance()
+    tracked_request.tag("error", "true")
+
+
 def install(app=None):
     if app is not None:
         copy_configuration(app)
@@ -62,6 +67,7 @@ def install(app=None):
 
     before_task_publish.connect(before_task_publish_callback)
     task_prerun.connect(task_prerun_callback)
+    task_failure.connect(task_failure_callback)
     task_postrun.connect(task_postrun_callback)
 
 


Test patch:
diff --git a/tests/integration/test_celery.py b/tests/integration/test_celery.py
index 2a4790a..6281c67 100644
--- a/tests/integration/test_celery.py
+++ b/tests/integration/test_celery.py
@@ -50,6 +50,10 @@ def app_with_scout(celery_config=None, app=None, config=None):
     def hello():
         return "Hello World!"
 
+    @app.task
+    def crash():
+        raise ValueError("Boom!")
+
     # Setup according to https://docs.scoutapm.com/#celery
     Config.set(**config)
     scout_apm.celery.install(app)
@@ -92,6 +96,20 @@ def test_hello_eager(tracked_requests):
     assert span.operation == "Job/tests.integration.test_celery.hello"
 
 
+def test_error_task(tracked_requests):
+    with app_with_scout() as app:
+        result = app.tasks["tests.integration.test_celery.crash"].apply()
+
+    assert isinstance(result.result, ValueError)
+    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.crash"
+    assert tracked_request.tags["error"]
+
+
 @skip_unless_celery_4_plus
 def test_hello_worker(celery_app, celery_worker, tracked_requests):
     with app_with_scout(app=celery_app) as app: