Repository: scoutapp/scout_apm_python

Issue:
Make AppMetadata.data more robust
Currently we use all-or-nothing semantics to find and send the app metadata. This is pretty useless as a single failure prevents all the other data being gathered. Instead we could do [more limited try/except clauses](https://adamj.eu/tech/2019/10/02/limit-your-try-clauses-in-python/) to allow us to send *something* even if some fields are empty.

Core Agent API docs: https://github.com/scoutapp/core-agent-api#application-metadata


Discussion:

Resolving patch:
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
@@ -9,7 +9,7 @@
 from scout_apm.core.config import scout_config
 from scout_apm.core.core_agent_manager import CoreAgentManager
 from scout_apm.core.instrument_manager import InstrumentManager
-from scout_apm.core.metadata import AppMetadata
+from scout_apm.core.metadata import report_app_metadata
 from scout_apm.core.socket import CoreAgentSocket
 
 logger = logging.getLogger(__name__)
@@ -41,7 +41,7 @@ def install(config=None):
     logger.debug("APM Launching on PID: %s", os.getpid())
     launched = CoreAgentManager().launch()
 
-    AppMetadata.report()
+    report_app_metadata()
     if launched:
         # Stop the thread to avoid running threads pre-fork
         CoreAgentSocket.instance().stop()
diff --git a/src/scout_apm/core/metadata.py b/src/scout_apm/core/metadata.py
--- a/src/scout_apm/core/metadata.py
+++ b/src/scout_apm/core/metadata.py
@@ -2,7 +2,6 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
 import datetime as dt
-import logging
 import sys
 from os import getpid
 
@@ -10,59 +9,51 @@
 from scout_apm.core.config import scout_config
 from scout_apm.core.socket import CoreAgentSocket
 
-logger = logging.getLogger(__name__)
 
-
-class AppMetadata(object):
-    @classmethod
-    def report(cls):
-        event = ApplicationEvent(
+def report_app_metadata():
+    CoreAgentSocket.instance().send(
+        ApplicationEvent(
             event_type="scout.metadata",
-            event_value=cls.data(),
+            event_value=get_metadata(),
             source="Pid: " + str(getpid()),
             timestamp=dt.datetime.utcnow(),
         )
-        CoreAgentSocket.instance().send(event)
-
-    @classmethod
-    def data(cls):
-        try:
-            data = {
-                "language": "python",
-                "language_version": "{}.{}.{}".format(*sys.version_info[:3]),
-                # Deprecated: (see #327)
-                "version": "{}.{}.{}".format(*sys.version_info[:3]),
-                "server_time": dt.datetime.utcnow().isoformat() + "Z",
-                "framework": scout_config.value("framework"),
-                "framework_version": scout_config.value("framework_version"),
-                "environment": "",
-                "app_server": scout_config.value("app_server"),
-                "hostname": scout_config.value("hostname"),
-                "database_engine": "",  # Detected
-                "database_adapter": "",  # Raw
-                "application_name": "",  # Environment.application_name,
-                "libraries": cls.get_python_packages_versions(),
-                "paas": "",
-                "application_root": scout_config.value("application_root"),
-                "scm_subdirectory": scout_config.value("scm_subdirectory"),
-                "git_sha": scout_config.value("revision_sha"),
-            }
-        except Exception as e:
-            logger.debug("Exception in AppMetadata: %r", e)
-            data = {}
-
-        return data
-
-    @classmethod
-    def get_python_packages_versions(cls):
-        try:
-            import pkg_resources
-        except ImportError:
-            return []
-
-        return list(
-            sorted(
-                (distribution.project_name, distribution.version)
-                for distribution in pkg_resources.working_set
-            )
+    )
+
+
+def get_metadata():
+    data = {
+        "language": "python",
+        "language_version": "{}.{}.{}".format(*sys.version_info[:3]),
+        "server_time": dt.datetime.utcnow().isoformat() + "Z",
+        "framework": scout_config.value("framework"),
+        "framework_version": scout_config.value("framework_version"),
+        "environment": "",
+        "app_server": scout_config.value("app_server"),
+        "hostname": scout_config.value("hostname"),
+        "database_engine": "",
+        "database_adapter": "",
+        "application_name": "",
+        "libraries": get_python_packages_versions(),
+        "paas": "",
+        "application_root": scout_config.value("application_root"),
+        "scm_subdirectory": scout_config.value("scm_subdirectory"),
+        "git_sha": scout_config.value("revision_sha"),
+    }
+    # Deprecated - see #327:
+    data["version"] = data["language_version"]
+    return data
+
+
+def get_python_packages_versions():
+    try:
+        import pkg_resources
+    except ImportError:
+        return []
+
+    return list(
+        sorted(
+            (distribution.project_name, distribution.version)
+            for distribution in pkg_resources.working_set
         )
+    )


Test patch:
diff --git a/tests/unit/core/test_metadata.py b/tests/unit/core/test_metadata.py
--- a/tests/unit/core/test_metadata.py
+++ b/tests/unit/core/test_metadata.py
@@ -1,16 +1,16 @@
 # coding=utf-8
 from __future__ import absolute_import, division, print_function, unicode_literals
 
-import py
+import pytest
 
-from scout_apm.core.metadata import AppMetadata
+from scout_apm.core.metadata import report_app_metadata
 from tests.compat import mock
 from tests.tools import pretend_package_unavailable
 
 
 @mock.patch("scout_apm.core.socket.CoreAgentSocket.send")
 def test_report_app_metadata(send):
-    AppMetadata().report()
+    report_app_metadata()
 
     assert send.call_count == 1
     (command,), kwargs = send.call_args
@@ -18,31 +18,16 @@ def test_report_app_metadata(send):
 
     message = command.message()
     assert message["ApplicationEvent"]["event_type"] == "scout.metadata"
-    # py.test is installed, since it's running tests right now.
-    assert ("py", py.version) in message["ApplicationEvent"]["event_value"]["libraries"]
-
-
-@mock.patch("scout_apm.core.socket.CoreAgentSocket.send")
-def test_report_app_metadata_error_getting_data(send):
-    with mock.patch(
-        "scout_apm.core.metadata.AppMetadata.get_python_packages_versions",
-        side_effect=RuntimeError,
-    ):
-        AppMetadata().report()
-
-    assert send.call_count == 1
-    (command,), kwargs = send.call_args
-    assert kwargs == {}
-
-    message = command.message()
-    assert message["ApplicationEvent"]["event_type"] == "scout.metadata"
-    assert message["ApplicationEvent"]["event_value"] == {}
+    data = message["ApplicationEvent"]["event_value"]
+    assert data["language"] == "python"
+    # pytest is installed, since it's running tests right now.
+    assert ("pytest", pytest.__version__) in data["libraries"]
 
 
 @mock.patch("scout_apm.core.socket.CoreAgentSocket.send")
 def test_report_app_metadata_no_pkg_resources(send):
     with pretend_package_unavailable("pkg_resources"):
-        AppMetadata().report()
+        report_app_metadata()
 
     assert send.call_count == 1
     (command,), kwargs = send.call_args