Repository: scoutapp/scout_apm_python
Pull request #144 (2018-12-18 20:57:20)

New setting `ignore` to ignore path prefixes
Allows you to easily ignore /health and similar high-throughput, low
meaning endpoints. Any requests to paths that start with any of the
ignore list will not be sent, and hence not counted as Throughput, and
not averaged into average or 95th percentile response times, and no
traces will be captured.


Patch:
diff --git a/src/scout_apm/api/__init__.py b/src/scout_apm/api/__init__.py
index 467c7c7..02cfd0f 100644
--- a/src/scout_apm/api/__init__.py
+++ b/src/scout_apm/api/__init__.py
@@ -54,6 +54,10 @@ def install(*args, **kwargs):
     scout_apm.core.install(*args, **kwargs)
 
 
+def ignore_transaction():
+    TrackedRequest.instance().tag("ignore_transaction", True)
+
+
 class instrument(ContextDecorator):
     def __init__(self, operation, kind="Custom", tags={}):
         self.operation = text(kind) + "/" + text(operation)
diff --git a/src/scout_apm/bottle/__init__.py b/src/scout_apm/bottle/__init__.py
index 71bcd3d..082fd04 100644
--- a/src/scout_apm/bottle/__init__.py
+++ b/src/scout_apm/bottle/__init__.py
@@ -6,6 +6,7 @@ import scout_apm.core
 from scout_apm.api.context import Context
 from scout_apm.core.config import ScoutConfig
 from scout_apm.core.context import AgentContext
+from scout_apm.core.ignore import ignore_path
 from scout_apm.core.tracked_request import TrackedRequest
 
 
@@ -51,6 +52,9 @@ class ScoutPlugin(object):
 
                 tr.start_span(operation="Controller{}".format(path))
 
+                if ignore_path(path):
+                    tr.tag("ignore_transaction", True)
+
                 try:
                     Context.add("path", path)
                     Context.add("user_ip", request.remote_addr)
diff --git a/src/scout_apm/core/config.py b/src/scout_apm/core/config.py
index 0445484..56b60c7 100644
--- a/src/scout_apm/core/config.py
+++ b/src/scout_apm/core/config.py
@@ -64,6 +64,7 @@ class ScoutConfig(object):
             "framework",
             "framework_version",
             "hostname",
+            "ignore",
             "key",
             "log_level",
             "monitor",
@@ -265,4 +266,5 @@ CONVERSIONS = {
     "core_agent_launch": BooleanConversion,
     "monitor": BooleanConversion,
     "disabled_instruments": ListConversion,
+    "ignore": ListConversion,
 }
diff --git a/src/scout_apm/core/ignore.py b/src/scout_apm/core/ignore.py
new file mode 100644
index 0000000..c614c4f
--- /dev/null
+++ b/src/scout_apm/core/ignore.py
@@ -0,0 +1,9 @@
+from scout_apm.core.context import AgentContext
+
+
+def ignore_path(path):
+    ignored_paths = AgentContext.instance.config.value("ignore")
+    for ignored in ignored_paths:
+        if path.startswith(ignored):
+            return True
+    return False
diff --git a/src/scout_apm/core/tracked_request.py b/src/scout_apm/core/tracked_request.py
index e9f273a..1a73041 100644
--- a/src/scout_apm/core/tracked_request.py
+++ b/src/scout_apm/core/tracked_request.py
@@ -95,7 +95,8 @@ class TrackedRequest(ThreadLocalSingleton):
             self.end_time = datetime.utcnow()
         if self.is_real_request():
             self.tag("mem_delta", Memory.get_delta(self.memory_start))
-            RequestManager.instance().add_request(self)
+            if not self.is_ignored():
+                RequestManager.instance().add_request(self)
             Samplers.ensure_running()
 
         # This can fail if the Tracked Request was created directly,
@@ -105,6 +106,10 @@ class TrackedRequest(ThreadLocalSingleton):
         except Exception:
             pass
 
+    # A request is ignored if the tag "ignore_transaction" is set to True
+    def is_ignored(self):
+        return self.tags.get("ignore_transaction", False)
+
 
 class Span(object):
     def __init__(self, *args, **kwargs):
diff --git a/src/scout_apm/django/middleware.py b/src/scout_apm/django/middleware.py
index 447072e..0f48c75 100644
--- a/src/scout_apm/django/middleware.py
+++ b/src/scout_apm/django/middleware.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 import logging
 
 from scout_apm.api.context import Context
+from scout_apm.core.ignore import ignore_path
 from scout_apm.core.remote_ip import RemoteIp
 from scout_apm.core.tracked_request import TrackedRequest
 
@@ -68,6 +69,9 @@ class ViewTimingMiddleware(object):
         Capture details about the view_func that is about to execute
         """
         try:
+            if ignore_path(request.path):
+                TrackedRequest.instance().tag("ignore_transaction", True)
+
             view_name = request.resolver_match._func_path
             span = TrackedRequest.instance().current_span()
             if span is not None:
diff --git a/src/scout_apm/flask/__init__.py b/src/scout_apm/flask/__init__.py
index 65027ee..b8cef81 100644
--- a/src/scout_apm/flask/__init__.py
+++ b/src/scout_apm/flask/__init__.py
@@ -5,6 +5,7 @@ from flask.globals import _request_ctx_stack
 
 import scout_apm.core
 from scout_apm.core.config import ScoutConfig
+from scout_apm.core.ignore import ignore_path
 from scout_apm.core.monkey import CallableProxy
 from scout_apm.core.tracked_request import TrackedRequest
 
@@ -103,6 +104,9 @@ class ScoutApm(object):
                 for key in detail:
                     span.tag(key, detail[key])
 
+                if ignore_path(detail.get("path", "")):
+                    tr.tag("ignore_transaction", True)
+
                 # And the custom View stuff
                 #  request = args[0]
 
diff --git a/src/scout_apm/pyramid/__init__.py b/src/scout_apm/pyramid/__init__.py
index 93e8886..06f76c1 100644
--- a/src/scout_apm/pyramid/__init__.py
+++ b/src/scout_apm/pyramid/__init__.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 import scout_apm.core
 from scout_apm.api.context import Context
 from scout_apm.core.config import ScoutConfig
+from scout_apm.core.ignore import ignore_path
 from scout_apm.core.tracked_request import TrackedRequest
 
 
@@ -26,6 +27,9 @@ def instruments(handler, registry):
             tr = TrackedRequest.instance()
             span = tr.start_span(operation="Controller/Pyramid")
 
+            if ignore_path(request.path):
+                tr.tag("ignore_transaction", True)
+
             # Capture what we can from the request, but never fail
             try:
                 Context.add("path", request.path)


Test patch:
diff --git a/tests/integration/test_api.py b/tests/integration/test_api.py
index 819049b..d1b852b 100644
--- a/tests/integration/test_api.py
+++ b/tests/integration/test_api.py
@@ -7,6 +7,7 @@ from scout_apm.api import (
     Config,
     Context,
     WebTransaction,
+    ignore_transaction,
     instrument,
 )
 from scout_apm.core.tracked_request import TrackedRequest
@@ -258,3 +259,13 @@ def test_config():
         Config.set(revision_sha="4de21f8ea228a082d4f039c0c991ee41dfb6f9d8")
     finally:
         Config.reset_all()
+
+
+def test_ignore_transaction():
+    tr = TrackedRequest.instance()
+
+    ignore_transaction()
+
+    tr.finish()
+
+    assert tr.tags["ignore_transaction"]
diff --git a/tests/unit/core/test_ignore.py b/tests/unit/core/test_ignore.py
new file mode 100644
index 0000000..96f5cc8
--- /dev/null
+++ b/tests/unit/core/test_ignore.py
@@ -0,0 +1,14 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from scout_apm.core.config import ScoutConfig
+from scout_apm.core.ignore import ignore_path
+
+
+def test_loads_class_instrument():
+    ScoutConfig.set(ignore=["/health"])
+
+    assert ignore_path("/health")
+    assert ignore_path("/health/foo")
+    assert not ignore_path("/users")
+
+    ScoutConfig.reset_all()
diff --git a/tests/unit/core/test_tracked_request.py b/tests/unit/core/test_tracked_request.py
index 48dc4e0..a727c5c 100644
--- a/tests/unit/core/test_tracked_request.py
+++ b/tests/unit/core/test_tracked_request.py
@@ -153,3 +153,9 @@ def test_finish_does_captures_memory_on_real_requests(tr):
     tr.finish()
 
     assert "mem_delta" in tr.tags
+
+
+def test_is_ignored(tr):
+    assert not tr.is_ignored()
+    tr.tag("ignore_transaction", True)
+    assert tr.is_ignored()