Repository: DataDog/dd-trace-py

Issue:
Misleading error message when patching boto3 instead of boto2

### Which version of dd-trace-py are you using?

0.41.0

### How can we reproduce your problem?

This logic of only exporting the `patch` method if there are no missing modules:

https://github.com/DataDog/dd-trace-py/blob/20ca41bf398f9dcd753cf40943de78f124ec9516/ddtrace/contrib/boto/__init__.py#L18-L24


causes this code:

https://github.com/DataDog/dd-trace-py/blob/20ca41bf398f9dcd753cf40943de78f124ec9516/ddtrace/monkey.py#L202

to fail with this misleading error:
```
AttributeError: module 'ddtrace.contrib.boto' has no attribute 'patch'
```

### What is the result that you expected?

A message that doesn't make me think something is wrong with my configuration.



Discussion:

Resolving patch:
diff --git a/ddtrace/internal/runtime/container.py b/ddtrace/internal/runtime/container.py
--- a/ddtrace/internal/runtime/container.py
+++ b/ddtrace/internal/runtime/container.py
@@ -1,3 +1,4 @@
+import os
 import re
 
 from ..logger import get_logger
@@ -92,8 +93,15 @@ def get_container_info(pid="self"):
     :returns: The cgroup file info if found, or else None
     :rtype: :class:`CGroupInfo` | None
     """
+
+    cgroup_file = "/proc/{0}/cgroup".format(pid)
+
+    if not os.path.exists(cgroup_file):
+        # If the cgroup file does not exist then this is likely not a container
+        # which is a valid use-case so pass.
+        return
+
     try:
-        cgroup_file = "/proc/{0}/cgroup".format(pid)
         with open(cgroup_file, mode="r") as fp:
             for line in fp:
                 info = CGroupInfo.from_line(line)
@@ -101,5 +109,3 @@ def get_container_info(pid="self"):
                     return info
     except Exception:
         log.debug("Failed to parse cgroup file for pid %r", pid, exc_info=True)
-
-    return None
diff --git a/ddtrace/monkey.py b/ddtrace/monkey.py
--- a/ddtrace/monkey.py
+++ b/ddtrace/monkey.py
@@ -92,6 +92,10 @@ class PatchException(Exception):
     pass
 
 
+class ModuleNotFoundException(PatchException):
+    pass
+
+
 def _on_import_factory(module, raise_errors=True):
     """Factory to create an import hook for the provided module name"""
 
@@ -172,6 +176,10 @@ def patch_module(module, raise_errors=True):
     """
     try:
         return _patch_module(module)
+    except ModuleNotFoundException:
+        if raise_errors:
+            raise
+        return False
     except Exception:
         if raise_errors:
             raise
@@ -199,14 +207,15 @@ def _patch_module(module):
 
         try:
             imported_module = importlib.import_module(path)
-            imported_module.patch()
         except ImportError:
             # if the import fails, the integration is not available
             raise PatchException("integration '%s' not available" % path)
-        except AttributeError:
+        else:
             # if patch() is not available in the module, it means
             # that the library is not installed in the environment
-            raise PatchException("module '%s' not installed" % module)
+            if not hasattr(imported_module, "patch"):
+                raise ModuleNotFoundException("module '%s' not installed" % module)
 
-        _PATCHED_MODULES.add(module)
-        return True
+            imported_module.patch()
+            _PATCHED_MODULES.add(module)
+            return True


Test patch: