Repository: scoutapp/scout_apm_python
Pull request #496 (2020-03-02 16:48:20)

Use dict.items() in BatchCommand.from_tracked_request
Slight efficiency improvement.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9aff305..1bbaeed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
 
 ## Pending
 
+### Added
+
+- Added ability to read configuration from your Celery app. This can help share
+  configuration between Django and Celery. Pass your application as `app` to
+  have the configuration copied: `scout_apm.celery.install(app=app)`
+  ([Issue #477](https://github.com/scoutapp/scout_apm_python/issues/477)).
+
 ### Fixed
 
 - Made installation of the objtrace C extension optional, so that if it fails
diff --git a/src/scout_apm/celery.py b/src/scout_apm/celery.py
index e65aac1..e52c1c8 100644
--- a/src/scout_apm/celery.py
+++ b/src/scout_apm/celery.py
@@ -7,15 +7,16 @@ 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.config import scout_config
 from scout_apm.core.tracked_request import TrackedRequest
 
 
-def before_publish_callback(headers=None, properties=None, **kwargs):
+def before_task_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):
+def task_prerun_callback(task=None, **kwargs):
     tracked_request = TrackedRequest.instance()
     tracked_request.is_real_request = True
 
@@ -45,22 +46,39 @@ def prerun_callback(task=None, **kwargs):
     tracked_request.start_span(operation=("Job/" + task.name))
 
 
-def postrun_callback(task=None, **kwargs):
+def task_postrun_callback(task=None, **kwargs):
     tracked_request = TrackedRequest.instance()
     tracked_request.stop_span()
 
 
-def install():
+def install(app=None):
+    if app is not None:
+        copy_configuration(app)
+
     installed = scout_apm.core.install()
     if not installed:
         return
 
-    before_task_publish.connect(before_publish_callback)
-    task_prerun.connect(prerun_callback)
-    task_postrun.connect(postrun_callback)
+    before_task_publish.connect(before_task_publish_callback)
+    task_prerun.connect(task_prerun_callback)
+    task_postrun.connect(task_postrun_callback)
+
+
+def copy_configuration(app):
+    prefix = "scout_"
+    prefix_len = len(prefix)
+
+    to_set = {}
+    for key, value in app.conf.items():
+        key_lower = key.lower()
+        if key_lower.startswith(prefix) and len(key_lower) > prefix_len:
+            scout_key = key_lower[prefix_len:]
+            to_set[scout_key] = value
+
+    scout_config.set(**to_set)
 
 
 def uninstall():
-    before_task_publish.disconnect(before_publish_callback)
-    task_prerun.disconnect(prerun_callback)
-    task_postrun.disconnect(postrun_callback)
+    before_task_publish.disconnect(before_task_publish_callback)
+    task_prerun.disconnect(task_prerun_callback)
+    task_postrun.disconnect(task_postrun_callback)
diff --git a/src/scout_apm/compat.py b/src/scout_apm/compat.py
index 6327c12..ec4bb44 100644
--- a/src/scout_apm/compat.py
+++ b/src/scout_apm/compat.py
@@ -13,6 +13,18 @@ string_type = str if sys.version_info[0] >= 3 else basestring  # noqa: F821
 text_type = str if sys.version_info[0] >= 3 else unicode  # noqa: F821
 string_types = tuple({string_type, text_type})
 
+if sys.version_info >= (3,):
+
+    def iteritems(dictionary):
+        return dictionary.items()
+
+
+else:
+
+    def iteritems(dictionary):
+        return dictionary.iteritems()  # noqa: B301
+
+
 if sys.version_info >= (3, 2):
     from contextlib import ContextDecorator
 else:
diff --git a/src/scout_apm/core/commands.py b/src/scout_apm/core/commands.py
index 497c540..cb93ba0 100644
--- a/src/scout_apm/core/commands.py
+++ b/src/scout_apm/core/commands.py
@@ -3,6 +3,8 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 
 import logging
 
+from scout_apm.compat import iteritems
+
 logger = logging.getLogger(__name__)
 
 
@@ -184,13 +186,13 @@ class BatchCommand(object):
         commands.append(
             StartRequest(timestamp=request.start_time, request_id=request.request_id)
         )
-        for key in request.tags:
+        for key, value in iteritems(request.tags):
             commands.append(
                 TagRequest(
                     timestamp=request.start_time,
                     request_id=request.request_id,
                     tag=key,
-                    value=request.tags[key],
+                    value=value,
                 )
             )
 
@@ -205,14 +207,14 @@ class BatchCommand(object):
                 )
             )
 
-            for key in span.tags:
+            for key, value in iteritems(span.tags):
                 commands.append(
                     TagSpan(
                         timestamp=span.start_time,
                         request_id=request.request_id,
                         span_id=span.span_id,
                         tag=key,
-                        value=span.tags[key],
+                        value=value,
                     )
                 )
 


Test patch:
diff --git a/tests/compat.py b/tests/compat.py
index 80e72a3..7b9c199 100644
--- a/tests/compat.py
+++ b/tests/compat.py
@@ -3,6 +3,16 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 
 import sys
 
+if sys.version_info >= (3, 0):
+    from types import SimpleNamespace
+else:
+
+    class SimpleNamespace(object):
+        def __init__(self, **kwargs):
+            for key, value in kwargs.items():
+                setattr(self, key, value)
+
+
 if sys.version_info >= (3, 0):
     from unittest import mock
 else:
diff --git a/tests/integration/test_celery.py b/tests/integration/test_celery.py
index c2dcb53..2004de9 100644
--- a/tests/integration/test_celery.py
+++ b/tests/integration/test_celery.py
@@ -10,6 +10,8 @@ from celery.signals import setup_logging
 import scout_apm.celery
 from scout_apm.api import Config
 from scout_apm.compat import kwargs_only
+from scout_apm.core.config import scout_config
+from tests.compat import SimpleNamespace
 
 # http://docs.celeryproject.org/en/latest/userguide/testing.html#py-test
 skip_unless_celery_4_plus = pytest.mark.skipif(
@@ -27,13 +29,16 @@ def do_nothing(**kwargs):
 
 @contextmanager
 @kwargs_only
-def app_with_scout(app=None, config=None):
+def app_with_scout(celery_config=None, app=None, config=None):
     """
     Context manager that configures a Celery app with Scout installed.
     """
     if app is None:
         app = celery.Celery("tasks", broker="memory://")
 
+    if celery_config is not None:
+        app.config_from_object(celery_config)
+
     # Enable Scout by default in tests.
     if config is None:
         config = {"monitor": True}
@@ -47,7 +52,7 @@ def app_with_scout(app=None, config=None):
 
     # Setup according to https://docs.scoutapm.com/#celery
     Config.set(**config)
-    scout_apm.celery.install()
+    scout_apm.celery.install(app)
 
     try:
         yield app
@@ -57,6 +62,12 @@ def app_with_scout(app=None, config=None):
         Config.reset_all()
 
 
+def test_configuration_copied():
+    celery_config = SimpleNamespace(SCOUT_IGNORE=["/foobar/"])
+    with app_with_scout(celery_config=celery_config):
+        assert scout_config.value("ignore") == ["/foobar/"]
+
+
 def test_hello_eager(tracked_requests):
     with app_with_scout() as app:
         result = app.tasks["tests.integration.test_celery.hello"].apply()