We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Hide scout message about event draining
When `log_level` is set to `ERROR` and logs are redirected to a log file through `log_file_path`, scout still logs a message like: `Scout draining 1 event for up to 2.0 seconds` on STDOUT, is it possible to remove this message from STDOUT?
Thank you!

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/scout_apm/core/__init__.py`
Content:
```
1 # coding=utf-8
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 
4 import atexit
5 import logging
6 import os
7 import sys
8 
9 from scout_apm import instruments
10 from scout_apm.compat import kwargs_only
11 from scout_apm.core import objtrace
12 from scout_apm.core.config import scout_config
13 from scout_apm.core.core_agent_manager import CoreAgentManager
14 from scout_apm.core.metadata import report_app_metadata
15 from scout_apm.core.socket import CoreAgentSocketThread
16 
17 logger = logging.getLogger(__name__)
18 
19 
20 @kwargs_only
21 def install(config=None):
22     global shutdown_registered
23     if config is not None:
24         scout_config.set(**config)
25     scout_config.log()
26 
27     if os.name == "nt":
28         logger.info(
29             "APM Not Launching on PID: %s - Windows is not supported", os.getpid()
30         )
31         return False
32 
33     if not scout_config.value("monitor"):
34         logger.info(
35             "APM Not Launching on PID: %s - Configuration 'monitor' is not true",
36             os.getpid(),
37         )
38         return False
39 
40     instruments.ensure_all_installed()
41     objtrace.enable()
42 
43     logger.debug("APM Launching on PID: %s", os.getpid())
44     launched = CoreAgentManager().launch()
45 
46     report_app_metadata()
47     if launched:
48         # Stop the thread to avoid running threads pre-fork
49         CoreAgentSocketThread.ensure_stopped()
50 
51     if scout_config.value("shutdown_timeout_seconds") > 0.0 and not shutdown_registered:
52         atexit.register(shutdown)
53         shutdown_registered = True
54 
55     return True
56 
57 
58 shutdown_registered = False
59 
60 
61 def shutdown():
62     timeout_seconds = scout_config.value("shutdown_timeout_seconds")
63 
64     def callback(queue_size):
65         print(  # noqa: T001
66             (
67                 "Scout draining {queue_size} event{s} for up to"
68                 + " {timeout_seconds} seconds"
69             ).format(
70                 queue_size=queue_size,
71                 s=("" if queue_size == 1 else "s"),
72                 timeout_seconds=timeout_seconds,
73             ),
74             file=sys.stderr,
75         )
76 
77     CoreAgentSocketThread.wait_until_drained(
78         timeout_seconds=timeout_seconds, callback=callback
79     )
80
```

Path: `src/scout_apm/core/config.py`
Content:
```
1 # coding=utf-8
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 
4 import logging
5 import os
6 import warnings
7 
8 from scout_apm.compat import string_type
9 from scout_apm.core import platform_detection
10 
11 logger = logging.getLogger(__name__)
12 
13 
14 class ScoutConfig(object):
15     """
16     Configuration object for the ScoutApm agent.
17 
18     Contains a list of configuration "layers". When a configuration key is
19     looked up, each layer is asked in turn if it knows the value. The first one
20     to answer affirmatively returns the value.
21     """
22 
23     def __init__(self):
24         self.layers = [
25             Env(),
26             Python(),
27             Derived(self),
28             Defaults(),
29             Null(),
30         ]
31 
32     def value(self, key):
33         value = self.locate_layer_for_key(key).value(key)
34         if key in CONVERSIONS:
35             return CONVERSIONS[key](value)
36         return value
37 
38     def locate_layer_for_key(self, key):
39         for layer in self.layers:
40             if layer.has_config(key):
41                 return layer
42 
43         # Should be unreachable because Null returns None for all keys.
44         raise ValueError("key {!r} not found in any layer".format(key))
45 
46     def log(self):
47         logger.debug("Configuration Loaded:")
48         for key in self.known_keys():
49             layer = self.locate_layer_for_key(key)
50             logger.debug(
51                 "%-9s: %s = %s", layer.__class__.__name__, key, layer.value(key)
52             )
53 
54     def known_keys(self):
55         return [
56             "app_server",
57             "application_root",
58             "core_agent_dir",
59             "core_agent_download",
60             "core_agent_launch",
61             "core_agent_log_level",
62             "core_agent_permissions",
63             "core_agent_version",
64             "disabled_instruments",
65             "download_url",
66             "framework",
67             "framework_version",
68             "hostname",
69             "ignore",
70             "key",
71             "log_level",
72             "monitor",
73             "name",
74             "revision_sha",
75             "scm_subdirectory",
76             "shutdown_timeout_seconds",
77             "socket_path",
78         ]
79 
80     def core_agent_permissions(self):
81         try:
82             return int(str(self.value("core_agent_permissions")), 8)
83         except ValueError:
84             logger.exception(
85                 "Invalid core_agent_permissions value, using default of 0o700"
86             )
87             return 0o700
88 
89     @classmethod
90     def set(cls, **kwargs):
91         """
92         Sets a configuration value for the Scout agent. Values set here will
93         not override values set in ENV.
94         """
95         for key, value in kwargs.items():
96             SCOUT_PYTHON_VALUES[key] = value
97 
98     @classmethod
99     def unset(cls, *keys):
100         """
101         Removes a configuration value for the Scout agent.
102         """
103         for key in keys:
104             SCOUT_PYTHON_VALUES.pop(key, None)
105 
106     @classmethod
107     def reset_all(cls):
108         """
109         Remove all configuration settings set via `ScoutConfig.set(...)`.
110 
111         This is meant for use in testing.
112         """
113         SCOUT_PYTHON_VALUES.clear()
114 
115 
116 # Module-level data, the ScoutConfig.set(key="value") adds to this
117 SCOUT_PYTHON_VALUES = {}
118 
119 
120 class Python(object):
121     """
122     A configuration overlay that lets other parts of python set values.
123     """
124 
125     def has_config(self, key):
126         return key in SCOUT_PYTHON_VALUES
127 
128     def value(self, key):
129         return SCOUT_PYTHON_VALUES[key]
130 
131 
132 class Env(object):
133     """
134     Reads configuration from environment by prefixing the key
135     requested with "SCOUT_"
136 
137     Example: the `key` config looks for SCOUT_KEY
138     environment variable
139     """
140 
141     def has_config(self, key):
142         env_key = self.modify_key(key)
143         return env_key in os.environ
144 
145     def value(self, key):
146         env_key = self.modify_key(key)
147         return os.environ[env_key]
148 
149     def modify_key(self, key):
150         env_key = ("SCOUT_" + key).upper()
151         return env_key
152 
153 
154 class Derived(object):
155     """
156     A configuration overlay that calculates from other values.
157     """
158 
159     def __init__(self, config):
160         """
161         config argument is the overall ScoutConfig var, so we can lookup the
162         components of the derived info.
163         """
164         self.config = config
165 
166     def has_config(self, key):
167         return self.lookup_func(key) is not None
168 
169     def value(self, key):
170         return self.lookup_func(key)()
171 
172     def lookup_func(self, key):
173         """
174         Returns the derive_#{key} function, or None if it isn't defined
175         """
176         func_name = "derive_" + key
177         return getattr(self, func_name, None)
178 
179     def derive_socket_path(self):
180         return "{}/{}/scout-agent.sock".format(
181             self.config.value("core_agent_dir"),
182             self.config.value("core_agent_full_name"),
183         )
184 
185     def derive_core_agent_full_name(self):
186         triple = self.config.value("core_agent_triple")
187         if not platform_detection.is_valid_triple(triple):
188             warnings.warn("Invalid value for core_agent_triple: {}".format(triple))
189         return "{name}-{version}-{triple}".format(
190             name="scout_apm_core",
191             version=self.config.value("core_agent_version"),
192             triple=triple,
193         )
194 
195     def derive_core_agent_triple(self):
196         return platform_detection.get_triple()
197 
198 
199 class Defaults(object):
200     """
201     Provides default values for important configurations
202     """
203 
204     def __init__(self):
205         self.defaults = {
206             "app_server": "",
207             "application_root": "",
208             "core_agent_dir": "/tmp/scout_apm_core",
209             "core_agent_download": True,
210             "core_agent_launch": True,
211             "core_agent_log_level": "info",
212             "core_agent_permissions": 700,
213             "core_agent_version": "v1.2.6",  # can be an exact tag name, or 'latest'
214             "disabled_instruments": [],
215             "download_url": "https://s3-us-west-1.amazonaws.com/scout-public-downloads/apm_core_agent/release",  # noqa: E501
216             "framework": "",
217             "framework_version": "",
218             "hostname": None,
219             "key": "",
220             "monitor": False,
221             "name": "Python App",
222             "revision_sha": self._git_revision_sha(),
223             "scm_subdirectory": "",
224             "shutdown_timeout_seconds": 2.0,
225             "uri_reporting": "filtered_params",
226         }
227 
228     def _git_revision_sha(self):
229         # N.B. The environment variable SCOUT_REVISION_SHA may also be used,
230         # but that will be picked up by Env
231         return os.environ.get("HEROKU_SLUG_COMMIT", "")
232 
233     def has_config(self, key):
234         return key in self.defaults
235 
236     def value(self, key):
237         return self.defaults[key]
238 
239 
240 class Null(object):
241     """
242     Always answers that a key is present, but the value is None
243 
244     Used as the last step of the layered configuration.
245     """
246 
247     def has_config(self, key):
248         return True
249 
250     def value(self, key):
251         return None
252 
253 
254 def convert_to_bool(value):
255     if isinstance(value, bool):
256         return value
257     if isinstance(value, string_type):
258         return value.lower() in ("yes", "true", "t", "1")
259     # Unknown type - default to false?
260     return False
261 
262 
263 def convert_to_float(value):
264     try:
265         return float(value)
266     except ValueError:
267         return 0.0
268 
269 
270 def convert_to_list(value):
271     if isinstance(value, list):
272         return value
273     if isinstance(value, tuple):
274         return list(value)
275     if isinstance(value, string_type):
276         # Split on commas
277         return [item.strip() for item in value.split(",") if item]
278     # Unknown type - default to empty?
279     return []
280 
281 
282 CONVERSIONS = {
283     "core_agent_download": convert_to_bool,
284     "core_agent_launch": convert_to_bool,
285     "disabled_instruments": convert_to_list,
286     "ignore": convert_to_list,
287     "monitor": convert_to_bool,
288     "shutdown_timeout_seconds": convert_to_float,
289 }
290 
291 
292 scout_config = ScoutConfig()
293
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/scout_apm/core/__init__.py b/src/scout_apm/core/__init__.py
--- a/src/scout_apm/core/__init__.py
+++ b/src/scout_apm/core/__init__.py
@@ -62,17 +62,18 @@
     timeout_seconds = scout_config.value("shutdown_timeout_seconds")
 
     def callback(queue_size):
-        print(  # noqa: T001
-            (
-                "Scout draining {queue_size} event{s} for up to"
-                + " {timeout_seconds} seconds"
-            ).format(
-                queue_size=queue_size,
-                s=("" if queue_size == 1 else "s"),
-                timeout_seconds=timeout_seconds,
-            ),
-            file=sys.stderr,
-        )
+        if scout_config.value("shutdown_message_enabled"):
+            print(  # noqa: T001
+                (
+                    "Scout draining {queue_size} event{s} for up to"
+                    + " {timeout_seconds} seconds"
+                ).format(
+                    queue_size=queue_size,
+                    s=("" if queue_size == 1 else "s"),
+                    timeout_seconds=timeout_seconds,
+                ),
+                file=sys.stderr,
+            )
 
     CoreAgentSocketThread.wait_until_drained(
         timeout_seconds=timeout_seconds, callback=callback
diff --git a/src/scout_apm/core/config.py b/src/scout_apm/core/config.py
--- a/src/scout_apm/core/config.py
+++ b/src/scout_apm/core/config.py
@@ -73,6 +73,7 @@
             "name",
             "revision_sha",
             "scm_subdirectory",
+            "shutdown_message_enabled",
             "shutdown_timeout_seconds",
             "socket_path",
         ]
@@ -221,6 +222,7 @@
             "name": "Python App",
             "revision_sha": self._git_revision_sha(),
             "scm_subdirectory": "",
+            "shutdown_message_enabled": True,
             "shutdown_timeout_seconds": 2.0,
             "uri_reporting": "filtered_params",
         }
@@ -285,6 +287,7 @@
     "disabled_instruments": convert_to_list,
     "ignore": convert_to_list,
     "monitor": convert_to_bool,
+    "shutdown_message_enabled": convert_to_bool,
     "shutdown_timeout_seconds": convert_to_float,
 }

{"golden_diff": "diff --git a/src/scout_apm/core/__init__.py b/src/scout_apm/core/__init__.py\n--- a/src/scout_apm/core/__init__.py\n+++ b/src/scout_apm/core/__init__.py\n@@ -62,17 +62,18 @@\n     timeout_seconds = scout_config.value(\"shutdown_timeout_seconds\")\n \n     def callback(queue_size):\n-        print(  # noqa: T001\n-            (\n-                \"Scout draining {queue_size} event{s} for up to\"\n-                + \" {timeout_seconds} seconds\"\n-            ).format(\n-                queue_size=queue_size,\n-                s=(\"\" if queue_size == 1 else \"s\"),\n-                timeout_seconds=timeout_seconds,\n-            ),\n-            file=sys.stderr,\n-        )\n+        if scout_config.value(\"shutdown_message_enabled\"):\n+            print(  # noqa: T001\n+                (\n+                    \"Scout draining {queue_size} event{s} for up to\"\n+                    + \" {timeout_seconds} seconds\"\n+                ).format(\n+                    queue_size=queue_size,\n+                    s=(\"\" if queue_size == 1 else \"s\"),\n+                    timeout_seconds=timeout_seconds,\n+                ),\n+                file=sys.stderr,\n+            )\n \n     CoreAgentSocketThread.wait_until_drained(\n         timeout_seconds=timeout_seconds, callback=callback\ndiff --git a/src/scout_apm/core/config.py b/src/scout_apm/core/config.py\n--- a/src/scout_apm/core/config.py\n+++ b/src/scout_apm/core/config.py\n@@ -73,6 +73,7 @@\n             \"name\",\n             \"revision_sha\",\n             \"scm_subdirectory\",\n+            \"shutdown_message_enabled\",\n             \"shutdown_timeout_seconds\",\n             \"socket_path\",\n         ]\n@@ -221,6 +222,7 @@\n             \"name\": \"Python App\",\n             \"revision_sha\": self._git_revision_sha(),\n             \"scm_subdirectory\": \"\",\n+            \"shutdown_message_enabled\": True,\n             \"shutdown_timeout_seconds\": 2.0,\n             \"uri_reporting\": \"filtered_params\",\n         }\n@@ -285,6 +287,7 @@\n     \"disabled_instruments\": convert_to_list,\n     \"ignore\": convert_to_list,\n     \"monitor\": convert_to_bool,\n+    \"shutdown_message_enabled\": convert_to_bool,\n     \"shutdown_timeout_seconds\": convert_to_float,\n }\n", "issue": "Hide scout message about event draining\nWhen `log_level` is set to `ERROR` and logs are redirected to a log file through `log_file_path`, scout still logs a message like: `Scout draining 1 event for up to 2.0 seconds` on STDOUT, is it possible to remove this message from STDOUT?\r\nThank you!\n", "before_files": [{"content": "# coding=utf-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport atexit\nimport logging\nimport os\nimport sys\n\nfrom scout_apm import instruments\nfrom scout_apm.compat import kwargs_only\nfrom scout_apm.core import objtrace\nfrom scout_apm.core.config import scout_config\nfrom scout_apm.core.core_agent_manager import CoreAgentManager\nfrom scout_apm.core.metadata import report_app_metadata\nfrom scout_apm.core.socket import CoreAgentSocketThread\n\nlogger = logging.getLogger(__name__)\n\n\n@kwargs_only\ndef install(config=None):\n    global shutdown_registered\n    if config is not None:\n        scout_config.set(**config)\n    scout_config.log()\n\n    if os.name == \"nt\":\n        logger.info(\n            \"APM Not Launching on PID: %s - Windows is not supported\", os.getpid()\n        )\n        return False\n\n    if not scout_config.value(\"monitor\"):\n        logger.info(\n            \"APM Not Launching on PID: %s - Configuration 'monitor' is not true\",\n            os.getpid(),\n        )\n        return False\n\n    instruments.ensure_all_installed()\n    objtrace.enable()\n\n    logger.debug(\"APM Launching on PID: %s\", os.getpid())\n    launched = CoreAgentManager().launch()\n\n    report_app_metadata()\n    if launched:\n        # Stop the thread to avoid running threads pre-fork\n        CoreAgentSocketThread.ensure_stopped()\n\n    if scout_config.value(\"shutdown_timeout_seconds\") > 0.0 and not shutdown_registered:\n        atexit.register(shutdown)\n        shutdown_registered = True\n\n    return True\n\n\nshutdown_registered = False\n\n\ndef shutdown():\n    timeout_seconds = scout_config.value(\"shutdown_timeout_seconds\")\n\n    def callback(queue_size):\n        print(  # noqa: T001\n            (\n                \"Scout draining {queue_size} event{s} for up to\"\n                + \" {timeout_seconds} seconds\"\n            ).format(\n                queue_size=queue_size,\n                s=(\"\" if queue_size == 1 else \"s\"),\n                timeout_seconds=timeout_seconds,\n            ),\n            file=sys.stderr,\n        )\n\n    CoreAgentSocketThread.wait_until_drained(\n        timeout_seconds=timeout_seconds, callback=callback\n    )\n", "path": "src/scout_apm/core/__init__.py"}, {"content": "# coding=utf-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport logging\nimport os\nimport warnings\n\nfrom scout_apm.compat import string_type\nfrom scout_apm.core import platform_detection\n\nlogger = logging.getLogger(__name__)\n\n\nclass ScoutConfig(object):\n    \"\"\"\n    Configuration object for the ScoutApm agent.\n\n    Contains a list of configuration \"layers\". When a configuration key is\n    looked up, each layer is asked in turn if it knows the value. The first one\n    to answer affirmatively returns the value.\n    \"\"\"\n\n    def __init__(self):\n        self.layers = [\n            Env(),\n            Python(),\n            Derived(self),\n            Defaults(),\n            Null(),\n        ]\n\n    def value(self, key):\n        value = self.locate_layer_for_key(key).value(key)\n        if key in CONVERSIONS:\n            return CONVERSIONS[key](value)\n        return value\n\n    def locate_layer_for_key(self, key):\n        for layer in self.layers:\n            if layer.has_config(key):\n                return layer\n\n        # Should be unreachable because Null returns None for all keys.\n        raise ValueError(\"key {!r} not found in any layer\".format(key))\n\n    def log(self):\n        logger.debug(\"Configuration Loaded:\")\n        for key in self.known_keys():\n            layer = self.locate_layer_for_key(key)\n            logger.debug(\n                \"%-9s: %s = %s\", layer.__class__.__name__, key, layer.value(key)\n            )\n\n    def known_keys(self):\n        return [\n            \"app_server\",\n            \"application_root\",\n            \"core_agent_dir\",\n            \"core_agent_download\",\n            \"core_agent_launch\",\n            \"core_agent_log_level\",\n            \"core_agent_permissions\",\n            \"core_agent_version\",\n            \"disabled_instruments\",\n            \"download_url\",\n            \"framework\",\n            \"framework_version\",\n            \"hostname\",\n            \"ignore\",\n            \"key\",\n            \"log_level\",\n            \"monitor\",\n            \"name\",\n            \"revision_sha\",\n            \"scm_subdirectory\",\n            \"shutdown_timeout_seconds\",\n            \"socket_path\",\n        ]\n\n    def core_agent_permissions(self):\n        try:\n            return int(str(self.value(\"core_agent_permissions\")), 8)\n        except ValueError:\n            logger.exception(\n                \"Invalid core_agent_permissions value, using default of 0o700\"\n            )\n            return 0o700\n\n    @classmethod\n    def set(cls, **kwargs):\n        \"\"\"\n        Sets a configuration value for the Scout agent. Values set here will\n        not override values set in ENV.\n        \"\"\"\n        for key, value in kwargs.items():\n            SCOUT_PYTHON_VALUES[key] = value\n\n    @classmethod\n    def unset(cls, *keys):\n        \"\"\"\n        Removes a configuration value for the Scout agent.\n        \"\"\"\n        for key in keys:\n            SCOUT_PYTHON_VALUES.pop(key, None)\n\n    @classmethod\n    def reset_all(cls):\n        \"\"\"\n        Remove all configuration settings set via `ScoutConfig.set(...)`.\n\n        This is meant for use in testing.\n        \"\"\"\n        SCOUT_PYTHON_VALUES.clear()\n\n\n# Module-level data, the ScoutConfig.set(key=\"value\") adds to this\nSCOUT_PYTHON_VALUES = {}\n\n\nclass Python(object):\n    \"\"\"\n    A configuration overlay that lets other parts of python set values.\n    \"\"\"\n\n    def has_config(self, key):\n        return key in SCOUT_PYTHON_VALUES\n\n    def value(self, key):\n        return SCOUT_PYTHON_VALUES[key]\n\n\nclass Env(object):\n    \"\"\"\n    Reads configuration from environment by prefixing the key\n    requested with \"SCOUT_\"\n\n    Example: the `key` config looks for SCOUT_KEY\n    environment variable\n    \"\"\"\n\n    def has_config(self, key):\n        env_key = self.modify_key(key)\n        return env_key in os.environ\n\n    def value(self, key):\n        env_key = self.modify_key(key)\n        return os.environ[env_key]\n\n    def modify_key(self, key):\n        env_key = (\"SCOUT_\" + key).upper()\n        return env_key\n\n\nclass Derived(object):\n    \"\"\"\n    A configuration overlay that calculates from other values.\n    \"\"\"\n\n    def __init__(self, config):\n        \"\"\"\n        config argument is the overall ScoutConfig var, so we can lookup the\n        components of the derived info.\n        \"\"\"\n        self.config = config\n\n    def has_config(self, key):\n        return self.lookup_func(key) is not None\n\n    def value(self, key):\n        return self.lookup_func(key)()\n\n    def lookup_func(self, key):\n        \"\"\"\n        Returns the derive_#{key} function, or None if it isn't defined\n        \"\"\"\n        func_name = \"derive_\" + key\n        return getattr(self, func_name, None)\n\n    def derive_socket_path(self):\n        return \"{}/{}/scout-agent.sock\".format(\n            self.config.value(\"core_agent_dir\"),\n            self.config.value(\"core_agent_full_name\"),\n        )\n\n    def derive_core_agent_full_name(self):\n        triple = self.config.value(\"core_agent_triple\")\n        if not platform_detection.is_valid_triple(triple):\n            warnings.warn(\"Invalid value for core_agent_triple: {}\".format(triple))\n        return \"{name}-{version}-{triple}\".format(\n            name=\"scout_apm_core\",\n            version=self.config.value(\"core_agent_version\"),\n            triple=triple,\n        )\n\n    def derive_core_agent_triple(self):\n        return platform_detection.get_triple()\n\n\nclass Defaults(object):\n    \"\"\"\n    Provides default values for important configurations\n    \"\"\"\n\n    def __init__(self):\n        self.defaults = {\n            \"app_server\": \"\",\n            \"application_root\": \"\",\n            \"core_agent_dir\": \"/tmp/scout_apm_core\",\n            \"core_agent_download\": True,\n            \"core_agent_launch\": True,\n            \"core_agent_log_level\": \"info\",\n            \"core_agent_permissions\": 700,\n            \"core_agent_version\": \"v1.2.6\",  # can be an exact tag name, or 'latest'\n            \"disabled_instruments\": [],\n            \"download_url\": \"https://s3-us-west-1.amazonaws.com/scout-public-downloads/apm_core_agent/release\",  # noqa: E501\n            \"framework\": \"\",\n            \"framework_version\": \"\",\n            \"hostname\": None,\n            \"key\": \"\",\n            \"monitor\": False,\n            \"name\": \"Python App\",\n            \"revision_sha\": self._git_revision_sha(),\n            \"scm_subdirectory\": \"\",\n            \"shutdown_timeout_seconds\": 2.0,\n            \"uri_reporting\": \"filtered_params\",\n        }\n\n    def _git_revision_sha(self):\n        # N.B. The environment variable SCOUT_REVISION_SHA may also be used,\n        # but that will be picked up by Env\n        return os.environ.get(\"HEROKU_SLUG_COMMIT\", \"\")\n\n    def has_config(self, key):\n        return key in self.defaults\n\n    def value(self, key):\n        return self.defaults[key]\n\n\nclass Null(object):\n    \"\"\"\n    Always answers that a key is present, but the value is None\n\n    Used as the last step of the layered configuration.\n    \"\"\"\n\n    def has_config(self, key):\n        return True\n\n    def value(self, key):\n        return None\n\n\ndef convert_to_bool(value):\n    if isinstance(value, bool):\n        return value\n    if isinstance(value, string_type):\n        return value.lower() in (\"yes\", \"true\", \"t\", \"1\")\n    # Unknown type - default to false?\n    return False\n\n\ndef convert_to_float(value):\n    try:\n        return float(value)\n    except ValueError:\n        return 0.0\n\n\ndef convert_to_list(value):\n    if isinstance(value, list):\n        return value\n    if isinstance(value, tuple):\n        return list(value)\n    if isinstance(value, string_type):\n        # Split on commas\n        return [item.strip() for item in value.split(\",\") if item]\n    # Unknown type - default to empty?\n    return []\n\n\nCONVERSIONS = {\n    \"core_agent_download\": convert_to_bool,\n    \"core_agent_launch\": convert_to_bool,\n    \"disabled_instruments\": convert_to_list,\n    \"ignore\": convert_to_list,\n    \"monitor\": convert_to_bool,\n    \"shutdown_timeout_seconds\": convert_to_float,\n}\n\n\nscout_config = ScoutConfig()\n", "path": "src/scout_apm/core/config.py"}], "after_files": [{"content": "# coding=utf-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport atexit\nimport logging\nimport os\nimport sys\n\nfrom scout_apm import instruments\nfrom scout_apm.compat import kwargs_only\nfrom scout_apm.core import objtrace\nfrom scout_apm.core.config import scout_config\nfrom scout_apm.core.core_agent_manager import CoreAgentManager\nfrom scout_apm.core.metadata import report_app_metadata\nfrom scout_apm.core.socket import CoreAgentSocketThread\n\nlogger = logging.getLogger(__name__)\n\n\n@kwargs_only\ndef install(config=None):\n    global shutdown_registered\n    if config is not None:\n        scout_config.set(**config)\n    scout_config.log()\n\n    if os.name == \"nt\":\n        logger.info(\n            \"APM Not Launching on PID: %s - Windows is not supported\", os.getpid()\n        )\n        return False\n\n    if not scout_config.value(\"monitor\"):\n        logger.info(\n            \"APM Not Launching on PID: %s - Configuration 'monitor' is not true\",\n            os.getpid(),\n        )\n        return False\n\n    instruments.ensure_all_installed()\n    objtrace.enable()\n\n    logger.debug(\"APM Launching on PID: %s\", os.getpid())\n    launched = CoreAgentManager().launch()\n\n    report_app_metadata()\n    if launched:\n        # Stop the thread to avoid running threads pre-fork\n        CoreAgentSocketThread.ensure_stopped()\n\n    if scout_config.value(\"shutdown_timeout_seconds\") > 0.0 and not shutdown_registered:\n        atexit.register(shutdown)\n        shutdown_registered = True\n\n    return True\n\n\nshutdown_registered = False\n\n\ndef shutdown():\n    timeout_seconds = scout_config.value(\"shutdown_timeout_seconds\")\n\n    def callback(queue_size):\n        if scout_config.value(\"shutdown_message_enabled\"):\n            print(  # noqa: T001\n                (\n                    \"Scout draining {queue_size} event{s} for up to\"\n                    + \" {timeout_seconds} seconds\"\n                ).format(\n                    queue_size=queue_size,\n                    s=(\"\" if queue_size == 1 else \"s\"),\n                    timeout_seconds=timeout_seconds,\n                ),\n                file=sys.stderr,\n            )\n\n    CoreAgentSocketThread.wait_until_drained(\n        timeout_seconds=timeout_seconds, callback=callback\n    )\n", "path": "src/scout_apm/core/__init__.py"}, {"content": "# coding=utf-8\nfrom __future__ import absolute_import, division, print_function, unicode_literals\n\nimport logging\nimport os\nimport warnings\n\nfrom scout_apm.compat import string_type\nfrom scout_apm.core import platform_detection\n\nlogger = logging.getLogger(__name__)\n\n\nclass ScoutConfig(object):\n    \"\"\"\n    Configuration object for the ScoutApm agent.\n\n    Contains a list of configuration \"layers\". When a configuration key is\n    looked up, each layer is asked in turn if it knows the value. The first one\n    to answer affirmatively returns the value.\n    \"\"\"\n\n    def __init__(self):\n        self.layers = [\n            Env(),\n            Python(),\n            Derived(self),\n            Defaults(),\n            Null(),\n        ]\n\n    def value(self, key):\n        value = self.locate_layer_for_key(key).value(key)\n        if key in CONVERSIONS:\n            return CONVERSIONS[key](value)\n        return value\n\n    def locate_layer_for_key(self, key):\n        for layer in self.layers:\n            if layer.has_config(key):\n                return layer\n\n        # Should be unreachable because Null returns None for all keys.\n        raise ValueError(\"key {!r} not found in any layer\".format(key))\n\n    def log(self):\n        logger.debug(\"Configuration Loaded:\")\n        for key in self.known_keys():\n            layer = self.locate_layer_for_key(key)\n            logger.debug(\n                \"%-9s: %s = %s\", layer.__class__.__name__, key, layer.value(key)\n            )\n\n    def known_keys(self):\n        return [\n            \"app_server\",\n            \"application_root\",\n            \"core_agent_dir\",\n            \"core_agent_download\",\n            \"core_agent_launch\",\n            \"core_agent_log_level\",\n            \"core_agent_permissions\",\n            \"core_agent_version\",\n            \"disabled_instruments\",\n            \"download_url\",\n            \"framework\",\n            \"framework_version\",\n            \"hostname\",\n            \"ignore\",\n            \"key\",\n            \"log_level\",\n            \"monitor\",\n            \"name\",\n            \"revision_sha\",\n            \"scm_subdirectory\",\n            \"shutdown_message_enabled\",\n            \"shutdown_timeout_seconds\",\n            \"socket_path\",\n        ]\n\n    def core_agent_permissions(self):\n        try:\n            return int(str(self.value(\"core_agent_permissions\")), 8)\n        except ValueError:\n            logger.exception(\n                \"Invalid core_agent_permissions value, using default of 0o700\"\n            )\n            return 0o700\n\n    @classmethod\n    def set(cls, **kwargs):\n        \"\"\"\n        Sets a configuration value for the Scout agent. Values set here will\n        not override values set in ENV.\n        \"\"\"\n        for key, value in kwargs.items():\n            SCOUT_PYTHON_VALUES[key] = value\n\n    @classmethod\n    def unset(cls, *keys):\n        \"\"\"\n        Removes a configuration value for the Scout agent.\n        \"\"\"\n        for key in keys:\n            SCOUT_PYTHON_VALUES.pop(key, None)\n\n    @classmethod\n    def reset_all(cls):\n        \"\"\"\n        Remove all configuration settings set via `ScoutConfig.set(...)`.\n\n        This is meant for use in testing.\n        \"\"\"\n        SCOUT_PYTHON_VALUES.clear()\n\n\n# Module-level data, the ScoutConfig.set(key=\"value\") adds to this\nSCOUT_PYTHON_VALUES = {}\n\n\nclass Python(object):\n    \"\"\"\n    A configuration overlay that lets other parts of python set values.\n    \"\"\"\n\n    def has_config(self, key):\n        return key in SCOUT_PYTHON_VALUES\n\n    def value(self, key):\n        return SCOUT_PYTHON_VALUES[key]\n\n\nclass Env(object):\n    \"\"\"\n    Reads configuration from environment by prefixing the key\n    requested with \"SCOUT_\"\n\n    Example: the `key` config looks for SCOUT_KEY\n    environment variable\n    \"\"\"\n\n    def has_config(self, key):\n        env_key = self.modify_key(key)\n        return env_key in os.environ\n\n    def value(self, key):\n        env_key = self.modify_key(key)\n        return os.environ[env_key]\n\n    def modify_key(self, key):\n        env_key = (\"SCOUT_\" + key).upper()\n        return env_key\n\n\nclass Derived(object):\n    \"\"\"\n    A configuration overlay that calculates from other values.\n    \"\"\"\n\n    def __init__(self, config):\n        \"\"\"\n        config argument is the overall ScoutConfig var, so we can lookup the\n        components of the derived info.\n        \"\"\"\n        self.config = config\n\n    def has_config(self, key):\n        return self.lookup_func(key) is not None\n\n    def value(self, key):\n        return self.lookup_func(key)()\n\n    def lookup_func(self, key):\n        \"\"\"\n        Returns the derive_#{key} function, or None if it isn't defined\n        \"\"\"\n        func_name = \"derive_\" + key\n        return getattr(self, func_name, None)\n\n    def derive_socket_path(self):\n        return \"{}/{}/scout-agent.sock\".format(\n            self.config.value(\"core_agent_dir\"),\n            self.config.value(\"core_agent_full_name\"),\n        )\n\n    def derive_core_agent_full_name(self):\n        triple = self.config.value(\"core_agent_triple\")\n        if not platform_detection.is_valid_triple(triple):\n            warnings.warn(\"Invalid value for core_agent_triple: {}\".format(triple))\n        return \"{name}-{version}-{triple}\".format(\n            name=\"scout_apm_core\",\n            version=self.config.value(\"core_agent_version\"),\n            triple=triple,\n        )\n\n    def derive_core_agent_triple(self):\n        return platform_detection.get_triple()\n\n\nclass Defaults(object):\n    \"\"\"\n    Provides default values for important configurations\n    \"\"\"\n\n    def __init__(self):\n        self.defaults = {\n            \"app_server\": \"\",\n            \"application_root\": \"\",\n            \"core_agent_dir\": \"/tmp/scout_apm_core\",\n            \"core_agent_download\": True,\n            \"core_agent_launch\": True,\n            \"core_agent_log_level\": \"info\",\n            \"core_agent_permissions\": 700,\n            \"core_agent_version\": \"v1.2.6\",  # can be an exact tag name, or 'latest'\n            \"disabled_instruments\": [],\n            \"download_url\": \"https://s3-us-west-1.amazonaws.com/scout-public-downloads/apm_core_agent/release\",  # noqa: E501\n            \"framework\": \"\",\n            \"framework_version\": \"\",\n            \"hostname\": None,\n            \"key\": \"\",\n            \"monitor\": False,\n            \"name\": \"Python App\",\n            \"revision_sha\": self._git_revision_sha(),\n            \"scm_subdirectory\": \"\",\n            \"shutdown_message_enabled\": True,\n            \"shutdown_timeout_seconds\": 2.0,\n            \"uri_reporting\": \"filtered_params\",\n        }\n\n    def _git_revision_sha(self):\n        # N.B. The environment variable SCOUT_REVISION_SHA may also be used,\n        # but that will be picked up by Env\n        return os.environ.get(\"HEROKU_SLUG_COMMIT\", \"\")\n\n    def has_config(self, key):\n        return key in self.defaults\n\n    def value(self, key):\n        return self.defaults[key]\n\n\nclass Null(object):\n    \"\"\"\n    Always answers that a key is present, but the value is None\n\n    Used as the last step of the layered configuration.\n    \"\"\"\n\n    def has_config(self, key):\n        return True\n\n    def value(self, key):\n        return None\n\n\ndef convert_to_bool(value):\n    if isinstance(value, bool):\n        return value\n    if isinstance(value, string_type):\n        return value.lower() in (\"yes\", \"true\", \"t\", \"1\")\n    # Unknown type - default to false?\n    return False\n\n\ndef convert_to_float(value):\n    try:\n        return float(value)\n    except ValueError:\n        return 0.0\n\n\ndef convert_to_list(value):\n    if isinstance(value, list):\n        return value\n    if isinstance(value, tuple):\n        return list(value)\n    if isinstance(value, string_type):\n        # Split on commas\n        return [item.strip() for item in value.split(\",\") if item]\n    # Unknown type - default to empty?\n    return []\n\n\nCONVERSIONS = {\n    \"core_agent_download\": convert_to_bool,\n    \"core_agent_launch\": convert_to_bool,\n    \"disabled_instruments\": convert_to_list,\n    \"ignore\": convert_to_list,\n    \"monitor\": convert_to_bool,\n    \"shutdown_message_enabled\": convert_to_bool,\n    \"shutdown_timeout_seconds\": convert_to_float,\n}\n\n\nscout_config = ScoutConfig()\n", "path": "src/scout_apm/core/config.py"}]}