Repository: open-telemetry/opentelemetry-python-contrib

Issue:
opentelemetry-instrument command fails if incompatible instrumentation is found
If an instrumentation is installed for a library that is not found in the environment, the instrument command raises the following exception:


```
❯ opentelemetry-instrument python main.py
Instrumenting of flask failed
Traceback (most recent call last):
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py", line 71, in _load_instrumentors
    conflict = get_dist_dependency_conflicts(entry_point.dist)
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/opentelemetry/instrumentation/dependencies.py", line 33, in get_dist_dependency_conflicts
    return get_dependency_conflicts(deps)
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/opentelemetry/instrumentation/dependencies.py", line 41, in get_dependency_conflicts
    get_distribution(str(dep))
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/pkg_resources/__init__.py", line 482, in get_distribution
    dist = get_provider(dist)
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/pkg_resources/__init__.py", line 358, in get_provider
    return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
IndexError: list index out of range
Failed to auto initialize opentelemetry
Traceback (most recent call last):
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py", line 111, in initialize
    _load_instrumentors(distro)
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py", line 85, in _load_instrumentors
    raise exc
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py", line 71, in _load_instrumentors
    conflict = get_dist_dependency_conflicts(entry_point.dist)
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/opentelemetry/instrumentation/dependencies.py", line 33, in get_dist_dependency_conflicts
    return get_dependency_conflicts(deps)
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/opentelemetry/instrumentation/dependencies.py", line 41, in get_dependency_conflicts
    get_distribution(str(dep))
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/pkg_resources/__init__.py", line 482, in get_distribution
    dist = get_provider(dist)
  File "/Users/olone/playground/splunk-otel-py/venv/lib/python3.8/site-packages/pkg_resources/__init__.py", line 358, in get_provider
    return working_set.find(moduleOrReq) or require(str(moduleOrReq))[0]
IndexError: list index out of range
```

bootstrap command does not install any instrumentations for libraries that are not present in the environment so this would only happen if someone manually installed an instrumentation package for a library they're not using. So this is not a deal breaker and doesn't require an immediate hotfix. That said, this IS a bug as the intended behavior of instrument command is to silently ignore such instrumentations.


Discussion:

Resolving patch:
diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py
--- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py
+++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py
@@ -1,12 +1,16 @@
+from logging import getLogger
 from typing import Collection, Optional
 
 from pkg_resources import (
     Distribution,
     DistributionNotFound,
+    RequirementParseError,
     VersionConflict,
     get_distribution,
 )
 
+logger = getLogger(__file__)
+
 
 class DependencyConflict:
     required: str = None
@@ -25,12 +29,19 @@ def __str__(self):
 def get_dist_dependency_conflicts(
     dist: Distribution,
 ) -> Optional[DependencyConflict]:
-    deps = [
-        dep
-        for dep in dist.requires(("instruments",))
-        if dep not in dist.requires()
-    ]
-    return get_dependency_conflicts(deps)
+    main_deps = dist.requires()
+    instrumentation_deps = []
+    for dep in dist.requires(("instruments",)):
+        if dep not in main_deps:
+            # we set marker to none so string representation of the dependency looks like
+            #    requests ~= 1.0
+            # instead of
+            #    requests ~= 1.0; extra = "instruments"
+            # which does not work with `get_distribution()`
+            dep.marker = None
+            instrumentation_deps.append(str(dep))
+
+    return get_dependency_conflicts(instrumentation_deps)
 
 
 def get_dependency_conflicts(
@@ -38,9 +49,16 @@ def get_dependency_conflicts(
 ) -> Optional[DependencyConflict]:
     for dep in deps:
         try:
-            get_distribution(str(dep))
+            get_distribution(dep)
         except VersionConflict as exc:
             return DependencyConflict(dep, exc.dist)
         except DistributionNotFound:
             return DependencyConflict(dep)
+        except RequirementParseError as exc:
+            logger.warning(
+                'error parsing dependency, reporting as a conflict: "%s" - %s',
+                dep,
+                exc,
+            )
+            return DependencyConflict(dep)
     return None


Test patch:
diff --git a/opentelemetry-instrumentation/tests/test_dependencies.py b/opentelemetry-instrumentation/tests/test_dependencies.py
--- a/opentelemetry-instrumentation/tests/test_dependencies.py
+++ b/opentelemetry-instrumentation/tests/test_dependencies.py
@@ -14,11 +14,13 @@
 
 # pylint: disable=protected-access
 
+import pkg_resources
 import pytest
 
 from opentelemetry.instrumentation.dependencies import (
     DependencyConflict,
     get_dependency_conflicts,
+    get_dist_dependency_conflicts,
 )
 from opentelemetry.test.test_base import TestBase
 
@@ -37,7 +39,6 @@ def test_get_dependency_conflicts_not_installed(self):
         conflict = get_dependency_conflicts(["this-package-does-not-exist"])
         self.assertTrue(conflict is not None)
         self.assertTrue(isinstance(conflict, DependencyConflict))
-        print(conflict)
         self.assertEqual(
             str(conflict),
             'DependencyConflict: requested: "this-package-does-not-exist" but found: "None"',
@@ -47,10 +48,32 @@ def test_get_dependency_conflicts_mismatched_version(self):
         conflict = get_dependency_conflicts(["pytest == 5000"])
         self.assertTrue(conflict is not None)
         self.assertTrue(isinstance(conflict, DependencyConflict))
-        print(conflict)
         self.assertEqual(
             str(conflict),
             'DependencyConflict: requested: "pytest == 5000" but found: "pytest {0}"'.format(
                 pytest.__version__
             ),
         )
+
+    def test_get_dist_dependency_conflicts(self):
+        def mock_requires(extras=()):
+            if "instruments" in extras:
+                return [
+                    pkg_resources.Requirement(
+                        'test-pkg ~= 1.0; extra == "instruments"'
+                    )
+                ]
+            return []
+
+        dist = pkg_resources.Distribution(
+            project_name="test-instrumentation", version="1.0"
+        )
+        dist.requires = mock_requires
+
+        conflict = get_dist_dependency_conflicts(dist)
+        self.assertTrue(conflict is not None)
+        self.assertTrue(isinstance(conflict, DependencyConflict))
+        self.assertEqual(
+            str(conflict),
+            'DependencyConflict: requested: "test-pkg~=1.0" but found: "None"',
+        )