diff --git a/CHANGELOG.md b/CHANGELOG.md index 003f8054f6..5197e7ecea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Breaking changes + +- Deprecation of pkg_resource in favor of importlib.metadata + ([#2181](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/2181)) + ## Version 1.27.0/0.48b0 () ### Added -- `opentelemetry-instrumentation-kafka-python` Instrument temporary fork, kafka-python-ng - inside kafka-python's instrumentation +- `opentelemetry-instrumentation-kafka-python` Instrument temporary fork, kafka-python-ng inside kafka-python's instrumentation ([#2537](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2537)) ### Breaking changes diff --git a/opentelemetry-instrumentation/pyproject.toml b/opentelemetry-instrumentation/pyproject.toml index edaf400419..a5e89bca17 100644 --- a/opentelemetry-instrumentation/pyproject.toml +++ b/opentelemetry-instrumentation/pyproject.toml @@ -26,8 +26,9 @@ classifiers = [ ] dependencies = [ "opentelemetry-api ~= 1.4", - "setuptools >= 16.0", "wrapt >= 1.0.0, < 2.0.0", + "importlib-metadata >= 6.0, < 8.0", + "packaging >= 18.0", ] [project.scripts] diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index a09334432d..8b7070b195 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -13,14 +13,13 @@ # limitations under the License. from argparse import REMAINDER, ArgumentParser +from importlib.metadata import entry_points from logging import getLogger from os import environ, execl, getcwd from os.path import abspath, dirname, pathsep from re import sub from shutil import which -from pkg_resources import iter_entry_points - from opentelemetry.instrumentation.version import __version__ _logger = getLogger(__name__) @@ -48,8 +47,8 @@ def run() -> None: argument_otel_environment_variable = {} - for entry_point in iter_entry_points( - "opentelemetry_environment_variables" + for entry_point in entry_points( + group="opentelemetry_environment_variables" ): environment_variable_module = entry_point.load() diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py index 27b57da3ef..5186ff7835 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py @@ -12,11 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +from importlib.metadata import EntryPoint, distributions, entry_points from logging import getLogger from os import environ -from pkg_resources import iter_entry_points - from opentelemetry.instrumentation.dependencies import ( get_dist_dependency_conflicts, ) @@ -31,9 +30,32 @@ _logger = getLogger(__name__) +class _EntryPointDistFinder: + def __int__(self): + self._mapping = None + + def dist_for(self, entry_point: EntryPoint): + dist = getattr(entry_point, "dist", None) + if dist: + return dist + + if self._mapping is None: + self._mapping = { + self._key_for(ep): dist + for ep in dist.entry_points + for dist in distributions() + } + + return self._mapping.get(self._key_for(entry_point)) + + @staticmethod + def _key_for(entry_point: EntryPoint): + return f"{entry_point.group}:{entry_point.name}:{entry_point.value}" + + def _load_distro() -> BaseDistro: distro_name = environ.get(OTEL_PYTHON_DISTRO, None) - for entry_point in iter_entry_points("opentelemetry_distro"): + for entry_point in entry_points(group="opentelemetry_distro"): try: # If no distro is specified, use first to come up. if distro_name is None or distro_name == entry_point.name: @@ -58,15 +80,16 @@ def _load_distro() -> BaseDistro: def _load_instrumentors(distro): package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []) + entry_point_finder = _EntryPointDistFinder() if isinstance(package_to_exclude, str): package_to_exclude = package_to_exclude.split(",") # to handle users entering "requests , flask" or "requests, flask" with spaces package_to_exclude = [x.strip() for x in package_to_exclude] - for entry_point in iter_entry_points("opentelemetry_pre_instrument"): + for entry_point in entry_points(group="opentelemetry_pre_instrument"): entry_point.load()() - for entry_point in iter_entry_points("opentelemetry_instrumentor"): + for entry_point in entry_points(group="opentelemetry_instrumentor"): if entry_point.name in package_to_exclude: _logger.debug( "Instrumentation skipped for library %s", entry_point.name @@ -74,7 +97,8 @@ def _load_instrumentors(distro): continue try: - conflict = get_dist_dependency_conflicts(entry_point.dist) + entry_point_dist = entry_point_finder.dist_for(entry_point) + conflict = get_dist_dependency_conflicts(entry_point_dist) if conflict: _logger.debug( "Skipping instrumentation %s: %s", @@ -90,14 +114,14 @@ def _load_instrumentors(distro): _logger.exception("Instrumenting of %s failed", entry_point.name) raise exc - for entry_point in iter_entry_points("opentelemetry_post_instrument"): + for entry_point in entry_points(group="opentelemetry_post_instrument"): entry_point.load()() def _load_configurators(): configurator_name = environ.get(OTEL_PYTHON_CONFIGURATOR, None) configured = None - for entry_point in iter_entry_points("opentelemetry_configurator"): + for entry_point in entry_points(group="opentelemetry_configurator"): if configured is not None: _logger.warning( "Configuration of %s not loaded, %s already loaded", diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py index 1cc28abca4..7ef3a4e778 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -15,6 +15,7 @@ import argparse import logging import sys +from importlib.metadata import PackageNotFoundError, version from subprocess import ( PIPE, CalledProcessError, @@ -23,7 +24,7 @@ check_call, ) -import pkg_resources +from packaging.requirements import Requirement from opentelemetry.instrumentation.bootstrap_gen import ( default_instrumentations, @@ -91,18 +92,19 @@ def _pip_check(): def _is_installed(req): - if req in sys.modules: - return True + req = Requirement(req) try: - pkg_resources.get_distribution(req) - except pkg_resources.DistributionNotFound: + dist_version = version(req.name) + except PackageNotFoundError: return False - except pkg_resources.VersionConflict as exc: + + if not req.specifier.filter(dist_version): logger.warning( - "instrumentation for package %s is available but version %s is installed. Skipping.", - exc.req, - exc.dist.as_requirement(), # pylint: disable=no-member + "instrumentation for package %s is available" + " but version %s is installed. Skipping.", + req, + dist_version, ) return False return True diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py index 2da0a3d18b..24fe669215 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py @@ -1,13 +1,8 @@ +from importlib.metadata import Distribution, PackageNotFoundError, version from logging import getLogger -from typing import Collection, Optional +from typing import Collection, Optional, Union -from pkg_resources import ( - Distribution, - DistributionNotFound, - RequirementParseError, - VersionConflict, - get_distribution, -) +from packaging.requirements import InvalidRequirement, Requirement logger = getLogger(__name__) @@ -27,36 +22,43 @@ def __str__(self): def get_dist_dependency_conflicts( dist: Distribution, ) -> Optional[DependencyConflict]: - 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)) + extra = "extra" + instruments = "instruments" + instruments_marker = {extra: instruments} + for dep in dist.requires: + if extra not in dep or instruments not in dep: + continue + + req = Requirement(dep) + if req.marker.evaluate(instruments_marker): + instrumentation_deps.append(req) return get_dependency_conflicts(instrumentation_deps) def get_dependency_conflicts( - deps: Collection[str], + deps: Collection[Union[str, Requirement]], ) -> Optional[DependencyConflict]: for dep in deps: + if isinstance(dep, Requirement): + req = dep + else: + try: + req = Requirement(dep) + except InvalidRequirement as exc: + logger.warning( + 'error parsing dependency, reporting as a conflict: "%s" - %s', + dep, + exc, + ) + return DependencyConflict(dep) + try: - 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, - ) + dist_version = version(req.name) + except PackageNotFoundError: return DependencyConflict(dep) + + if not req.specifier.contains(dist_version): + return DependencyConflict(dep, f"{req.name} {dist_version}") return None diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py index 93646bbb2f..297e21657f 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py @@ -18,10 +18,9 @@ """ from abc import ABC, abstractmethod +from importlib.metadata import EntryPoint from logging import getLogger -from pkg_resources import EntryPoint - from opentelemetry.instrumentation.instrumentor import BaseInstrumentor _LOG = getLogger(__name__) diff --git a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py index 5fc59b542d..9b0941bafa 100644 --- a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py +++ b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py @@ -30,7 +30,13 @@ class TestLoad(TestCase): "os.environ", {OTEL_PYTHON_CONFIGURATOR: "custom_configurator2"} ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.distributions" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.EntryPoint" ) def test_load_configurators( self, iter_mock @@ -61,7 +67,13 @@ def test_load_configurators( "os.environ", {OTEL_PYTHON_CONFIGURATOR: "custom_configurator2"} ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.distributions" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.EntryPoint" ) def test_load_configurators_no_ep( self, iter_mock @@ -74,7 +86,13 @@ def test_load_configurators_no_ep( "os.environ", {OTEL_PYTHON_CONFIGURATOR: "custom_configurator2"} ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.distributions" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.EntryPoint" ) def test_load_configurators_error(self, iter_mock): # Add multiple entry points but only specify the 2nd in the environment variable. @@ -101,7 +119,13 @@ def test_load_configurators_error(self, iter_mock): "opentelemetry.instrumentation.auto_instrumentation._load.isinstance" ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.distributions" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.EntryPoint" ) def test_load_distro(self, iter_mock, isinstance_mock): # Add multiple entry points but only specify the 2nd in the environment variable. @@ -134,7 +158,13 @@ def test_load_distro(self, iter_mock, isinstance_mock): "opentelemetry.instrumentation.auto_instrumentation._load.DefaultDistro" ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.distributions" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.EntryPoint" ) def test_load_distro_not_distro( self, iter_mock, default_distro_mock, isinstance_mock @@ -166,7 +196,13 @@ def test_load_distro_not_distro( "opentelemetry.instrumentation.auto_instrumentation._load.DefaultDistro" ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.distributions" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.EntryPoint" ) def test_load_distro_no_ep(self, iter_mock, default_distro_mock): iter_mock.return_value = () @@ -181,7 +217,13 @@ def test_load_distro_no_ep(self, iter_mock, default_distro_mock): "opentelemetry.instrumentation.auto_instrumentation._load.isinstance" ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.distributions" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.EntryPoint" ) def test_load_distro_error(self, iter_mock, isinstance_mock): ep_mock1 = Mock() @@ -211,7 +253,13 @@ def test_load_distro_error(self, iter_mock, isinstance_mock): "opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts" ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.distributions" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.EntryPoint" ) def test_load_instrumentors(self, iter_mock, dep_mock): # Mock opentelemetry_pre_instrument entry points @@ -285,7 +333,13 @@ def test_load_instrumentors(self, iter_mock, dep_mock): "opentelemetry.instrumentation.auto_instrumentation._load.get_dist_dependency_conflicts" ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.distributions" + ) + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.EntryPoint" ) def test_load_instrumentors_dep_conflict( self, iter_mock, dep_mock diff --git a/opentelemetry-instrumentation/tests/test_dependencies.py b/opentelemetry-instrumentation/tests/test_dependencies.py index 04bcf476ea..6b0caea548 100644 --- a/opentelemetry-instrumentation/tests/test_dependencies.py +++ b/opentelemetry-instrumentation/tests/test_dependencies.py @@ -14,7 +14,8 @@ # pylint: disable=protected-access -import pkg_resources +from importlib.metadata import Distribution, requires +from packaging.requirements import Requirement, InvalidRequirement import pytest from opentelemetry.instrumentation.dependencies import ( @@ -29,9 +30,23 @@ class TestDependencyConflicts(TestBase): def test_get_dependency_conflicts_empty(self): self.assertIsNone(get_dependency_conflicts([])) + def test_get_dependency_conflicts_no_conflict_requirement(self): + req = Requirement("pytest") + self.assertIsNone(get_dependency_conflicts([req])) + def test_get_dependency_conflicts_no_conflict(self): self.assertIsNone(get_dependency_conflicts(["pytest"])) + def test_get_dependency_conflicts_not_installed_requirement(self): + req = Requirement("this-package-does-not-exist") + conflict = get_dependency_conflicts([req]) + self.assertTrue(conflict is not None) + self.assertTrue(isinstance(conflict, DependencyConflict)) + self.assertEqual( + str(conflict), + 'DependencyConflict: requested: "this-package-does-not-exist" but found: "None"', + ) + def test_get_dependency_conflicts_not_installed(self): conflict = get_dependency_conflicts(["this-package-does-not-exist"]) self.assertTrue(conflict is not None) @@ -53,16 +68,12 @@ def test_get_dependency_conflicts_mismatched_version(self): def test_get_dist_dependency_conflicts(self): def mock_requires(extras=()): if "instruments" in extras: - return [ - pkg_resources.Requirement( + return requires( 'test-pkg ~= 1.0; extra == "instruments"' ) - ] return [] - dist = pkg_resources.Distribution( - project_name="test-instrumentation", version="1.0" - ) + dist = Distribution() dist.requires = mock_requires conflict = get_dist_dependency_conflicts(dist) diff --git a/opentelemetry-instrumentation/tests/test_distro.py b/opentelemetry-instrumentation/tests/test_distro.py index 399b3f8a65..add23a3f39 100644 --- a/opentelemetry-instrumentation/tests/test_distro.py +++ b/opentelemetry-instrumentation/tests/test_distro.py @@ -15,7 +15,7 @@ from unittest import TestCase -from pkg_resources import EntryPoint +from importlib.metadata import EntryPoint from opentelemetry.instrumentation.distro import BaseDistro from opentelemetry.instrumentation.instrumentor import BaseInstrumentor @@ -51,7 +51,7 @@ def test_load_instrumentor(self): distro = MockDistro() instrumentor = MockInstrumetor() - entry_point = MockEntryPoint(MockInstrumetor) + entry_point = MockEntryPoint(MockInstrumetor, value="opentelemetry", group="opentelemetry_distro") self.assertFalse(instrumentor._is_instrumented_by_opentelemetry) distro.load_instrumentor(entry_point)