Repository: scoutapp/scout_apm_python

Issue:
Support Queue timing in Python agent
Checking the request headers for officially supported frameworks like Django and Flask should be straight forward.

We should also have a generic API interface for recording Queue time in other cases.


Discussion:
@cschneid Should we make this a layer as a child of the parent span?
I think that will work. The Ruby agent creates a span named: `"QueueTime/Request"`

@adamchainz Can you look into implementing? You can create a new child span under the span we start in the Django middleware: https://github.com/scoutapp/scout_apm_python/blob/master/src/scout_apm/django/middleware.py#L25

Please also add for Flask, Pyramid, and Bottle.

Here's the headers we look for and how the Ruby agent does it: https://github.com/scoutapp/scout_apm_ruby/blob/master/lib/scout_apm/layer_converters/request_queue_time_converter.rb#L5
Sure thing!

Resolving patch:
diff --git a/src/scout_apm/core/cli/core_agent_manager.py b/src/scout_apm/core/cli/core_agent_manager.py
--- a/src/scout_apm/core/cli/core_agent_manager.py
+++ b/src/scout_apm/core/cli/core_agent_manager.py
@@ -4,38 +4,31 @@
 import argparse
 import logging
 
+from scout_apm.core import AgentContext, CoreAgentManager
 
-def download(**kwargs):
-    from scout_apm.core import CoreAgentManager
-
-    core_agent_manager = CoreAgentManager()
-    core_agent_manager.download()
-
-
-def launch(**kwargs):
-    from scout_apm.core import CoreAgentManager
-
-    core_agent_manager = CoreAgentManager()
-    core_agent_manager.launch()
+logger = logging.getLogger(__name__)
 
 
-def main(**kwargs):
+def main(argv=None):
     parser = argparse.ArgumentParser()
     parser.add_argument(
         "-v", "--verbose", help="increase output verbosity", action="count"
     )
 
-    subparsers = parser.add_subparsers(dest="subparser")
+    subparsers = parser.add_subparsers(
+        title="subcommands", description="valid subcommands", dest="subparser"
+    )
     subparsers.add_parser("download")
     subparsers.add_parser("launch")
 
-    args = parser.parse_args()
+    args = parser.parse_args(argv)
 
     if args.verbose is not None:
         if args.verbose >= 2:
-            logging.basicConfig(level=getattr(logging, "DEBUG", None))
-        elif args.verbose == 1:
-            logging.basicConfig(level=getattr(logging, "INFO", None))
+            logging.basicConfig(level=logging.DEBUG)
+        else:
+            logging.basicConfig(level=logging.INFO)
 
-    kwargs = vars(args)
-    globals()[kwargs.pop("subparser")](**kwargs)
+    AgentContext.build()
+    core_agent_manager = CoreAgentManager()
+    getattr(core_agent_manager, args.subparser)()


Test patch:
diff --git a/tests/unit/core/cli/test_core_agent_manager.py b/tests/unit/core/cli/test_core_agent_manager.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/core/cli/test_core_agent_manager.py
@@ -0,0 +1,39 @@
+# coding=utf-8
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import logging
+
+import pytest
+
+from scout_apm.core.cli.core_agent_manager import main
+from tests.compat import mock
+
+
+@pytest.fixture(autouse=True)
+def mock_CoreAgentManager():
+    # Always mock out the actual CoreAgentManager class for these tests, to
+    # keep them quick
+    path = "scout_apm.core.cli.core_agent_manager.CoreAgentManager"
+    with mock.patch(path) as mock_obj:
+        yield mock_obj
+
+
+@mock.patch("scout_apm.core.cli.core_agent_manager.logging.basicConfig")
+@pytest.mark.parametrize(
+    "args, expected_level", [(["-v"], logging.INFO), (["-v", "-v"], logging.DEBUG)]
+)
+def test_logging_verbose(mock_basicConfig, args, expected_level):
+    # Have to use a mock for basicConfig since logging is already configured
+    # during tests
+    main(args + ["download"])
+
+    mock_basicConfig.assert_called_with(level=expected_level)
+
+
+@pytest.mark.parametrize("command", ["download", "launch"])
+def test_command(mock_CoreAgentManager, command):
+    main([command])
+
+    instance = mock_CoreAgentManager.return_value
+    method = getattr(instance, command)
+    method.assert_called_with()