diff --git a/CHANGELOG.md b/CHANGELOG.md index 2923ebc791..bbd166f1b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix version of Flask dependency `werkzeug` ([#1980](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1980)) -- `opentelemetry-resource-detector-azure` Using new Cloud Resource ID attribute. - ([#1976](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1976)) - +- `opentelemetry-instrumentation`, `opentelemetry-instrumentation-aiohttp-client` Use importlib-metadata for entry points instead of pkg_resources + ([#2124](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2124)) ## Version 1.20.0/0.41b0 (2023-09-01) diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py index 6af9d41900..1d5781f3ef 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-client/tests/test_aiohttp_client_integration.py @@ -22,9 +22,9 @@ import aiohttp import aiohttp.test_utils +import importlib_metadata import yarl from http_server_mock import HttpServerMock -from pkg_resources import iter_entry_points from opentelemetry import context from opentelemetry import trace as trace_api @@ -574,8 +574,8 @@ def response_hook( class TestLoadingAioHttpInstrumentor(unittest.TestCase): def test_loading_instrumentor(self): - entry_points = iter_entry_points( - "opentelemetry_instrumentor", "aiohttp-client" + entry_points = importlib_metadata.entry_points( + group="opentelemetry_instrumentor", name="aiohttp-client" ) instrumentor = next(entry_points).load()() diff --git a/instrumentation/opentelemetry-instrumentation-wsgi/tests/__init__.py b/instrumentation/opentelemetry-instrumentation-wsgi/tests/__init__.py index 8a3124243a..2abbc624cf 100644 --- a/instrumentation/opentelemetry-instrumentation-wsgi/tests/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-wsgi/tests/__init__.py @@ -1,4 +1,4 @@ -# Copyright The OpenTelemetry Authors +# Copyright The Open Telemetry Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import pkg_resources # IMPORTANT: Only the wsgi module needs this because it is always the first # package that uses the `{rootdir}/*/tests/` path and gets installed by @@ -20,4 +19,5 @@ # Naming the tests module as a namespace package ensures that # relative imports will resolve properly for subsequent test packages, # as it enables searching for a composite of multiple test modules. -pkg_resources.declare_namespace(__name__) + +# No need for additional code here for Python 3.3+ diff --git a/opentelemetry-distro/tests/test_distro.py b/opentelemetry-distro/tests/test_distro.py index dd8f9c4cc4..e709ed1a2f 100644 --- a/opentelemetry-distro/tests/test_distro.py +++ b/opentelemetry-distro/tests/test_distro.py @@ -15,8 +15,7 @@ import os from unittest import TestCase - -from pkg_resources import DistributionNotFound, require +import importlib_metadata from opentelemetry.distro import OpenTelemetryDistro from opentelemetry.environment_variables import ( @@ -25,12 +24,11 @@ ) from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_PROTOCOL - class TestDistribution(TestCase): def test_package_available(self): try: - require(["opentelemetry-distro"]) - except DistributionNotFound: + importlib_metadata.distribution("opentelemetry-distro") + except importlib_metadata.PackageNotFoundError: self.fail("opentelemetry-distro not installed") def test_default_configuration(self): diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py index 5758ef1834..52a50f253e 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/__init__.py @@ -21,7 +21,7 @@ from re import sub from shutil import which -from pkg_resources import iter_entry_points +from importlib_metadata import entry_points from opentelemetry.instrumentation.version import __version__ @@ -50,8 +50,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..f23a6d2d05 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py @@ -15,7 +15,7 @@ from logging import getLogger from os import environ -from pkg_resources import iter_entry_points +from importlib_metadata import entry_points from opentelemetry.instrumentation.dependencies import ( get_dist_dependency_conflicts, @@ -33,7 +33,7 @@ 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: @@ -63,10 +63,10 @@ def _load_instrumentors(distro): # 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 @@ -90,14 +90,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 6fa36f0463..cf2da1f22b 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/bootstrap.py @@ -19,7 +19,8 @@ import subprocess import sys -import pkg_resources +from importlib_metadata import PackageNotFoundError, distribution +from packaging import version from opentelemetry.instrumentation.bootstrap_gen import ( default_instrumentations, @@ -83,21 +84,45 @@ def _pip_check(): raise RuntimeError(f"Dependency conflict found: {pip_check}") +# def _is_installed(req): +# if req in sys.modules: +# return True + +# try: +# dist = distribution(req).version +# except PackageNotFoundError: +# return False +# except pkg_resources.VersionConflict as exc: +# logger.warning( +# "instrumentation for package %s is available but version %s is installed. Skipping.", +# exc.req, +# exc.dist.as_requirement(), # pylint: disable=no-member +# ) +# return False +# return True + + def _is_installed(req): if req in sys.modules: return True try: - pkg_resources.get_distribution(req) - except pkg_resources.DistributionNotFound: - return False - except pkg_resources.VersionConflict as exc: - logger.warning( - "instrumentation for package %s is available but version %s is installed. Skipping.", - exc.req, - exc.dist.as_requirement(), # pylint: disable=no-member - ) + dist = distribution(req) + # Assuming 'req' is in format 'package==version' + # Modify this as per the format of your 'req' string + required_version = req.split("==")[1] if "==" in req else None + if required_version and version.parse(dist.version) != version.parse( + required_version + ): + logger.warning( + "Instrumentation for package %s is available but version %s is installed. Skipping.", + req, + dist.version, + ) + return False + except PackageNotFoundError: return False + return True diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py index 2da0a3d18b..66d93c8995 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py @@ -1,13 +1,8 @@ from logging import getLogger from typing import Collection, Optional - -from pkg_resources import ( - Distribution, - DistributionNotFound, - RequirementParseError, - VersionConflict, - get_distribution, -) +import importlib_metadata +from packaging.requirements import Requirement +from packaging.version import parse as parse_version logger = getLogger(__name__) @@ -24,22 +19,10 @@ def __str__(self): return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"' -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)) - - return get_dependency_conflicts(instrumentation_deps) +def _check_version(conflict_requirement, installed_version): + if not conflict_requirement.specifier.contains(installed_version, prereleases=True): + return f"{conflict_requirement.name} {installed_version}" + return None def get_dependency_conflicts( @@ -47,12 +30,15 @@ def get_dependency_conflicts( ) -> Optional[DependencyConflict]: for dep in deps: try: - get_distribution(dep) - except VersionConflict as exc: - return DependencyConflict(dep, exc.dist) - except DistributionNotFound: + requirement = Requirement(dep) + distribution = importlib_metadata.distribution(requirement.name) + installed_version = parse_version(distribution.version) + conflict_version = _check_version(requirement, installed_version) + if conflict_version: + return DependencyConflict(dep, conflict_version) + except importlib_metadata.PackageNotFoundError: return DependencyConflict(dep) - except RequirementParseError as exc: + except Exception as exc: logger.warning( 'error parsing dependency, reporting as a conflict: "%s" - %s', dep, @@ -60,3 +46,28 @@ def get_dependency_conflicts( ) return DependencyConflict(dep) return None + + +def get_dist_dependency_conflicts( + dist_name: str, # Assuming dist_name is the name of the distribution +) -> Optional[DependencyConflict]: + try: + distribution = importlib_metadata.distribution(dist_name) + except importlib_metadata.PackageNotFoundError: + return DependencyConflict(dist_name) + + conflicts = [] + for req in distribution.requires or []: + try: + requirement = Requirement(req) + dep_dist = importlib_metadata.distribution(requirement.name) + installed_version = parse_version(dep_dist.version) + if not requirement.specifier.contains(installed_version, prereleases=True): + conflicts.append(f"{requirement.name} {installed_version}") + except importlib_metadata.PackageNotFoundError: + conflicts.append(requirement.name) + + if conflicts: + # Return the first conflict found for simplicity + return DependencyConflict(str(conflicts[0])) + return None diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py index cc1c99c1e0..efd1039801 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/distro.py @@ -20,7 +20,7 @@ from abc import ABC, abstractmethod from logging import getLogger -from pkg_resources import EntryPoint +from importlib_metadata import EntryPoint from opentelemetry.instrumentation.instrumentor import BaseInstrumentor diff --git a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py index 1e2a851e48..c6ac6a6dc2 100644 --- a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py +++ b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py @@ -30,7 +30,7 @@ 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" ) def test_load_configurators(self, iter_mock): # Add multiple entry points but only specify the 2nd in the environment variable. @@ -59,7 +59,7 @@ def test_load_configurators(self, iter_mock): "os.environ", {OTEL_PYTHON_CONFIGURATOR: "custom_configurator2"} ) @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.iter_entry_points" + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" ) def test_load_configurators_no_ep( self, @@ -73,7 +73,7 @@ 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" ) def test_load_configurators_error(self, iter_mock): # Add multiple entry points but only specify the 2nd in the environment variable. @@ -100,7 +100,7 @@ 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" ) def test_load_distro(self, iter_mock, isinstance_mock): # Add multiple entry points but only specify the 2nd in the environment variable. @@ -133,7 +133,7 @@ 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" ) def test_load_distro_not_distro( self, iter_mock, default_distro_mock, isinstance_mock @@ -165,7 +165,7 @@ 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" ) def test_load_distro_no_ep(self, iter_mock, default_distro_mock): iter_mock.return_value = () @@ -180,7 +180,7 @@ 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" ) def test_load_distro_error(self, iter_mock, isinstance_mock): ep_mock1 = Mock() @@ -210,7 +210,7 @@ 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" ) def test_load_instrumentors(self, iter_mock, dep_mock): # Mock opentelemetry_pre_instrument entry points @@ -283,7 +283,7 @@ 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" ) def test_load_instrumentors_dep_conflict(self, iter_mock, dep_mock): ep_mock1 = Mock() diff --git a/opentelemetry-instrumentation/tests/test_dependencies.py b/opentelemetry-instrumentation/tests/test_dependencies.py index 04bcf476ea..14abb9a5af 100644 --- a/opentelemetry-instrumentation/tests/test_dependencies.py +++ b/opentelemetry-instrumentation/tests/test_dependencies.py @@ -13,18 +13,15 @@ # limitations under the License. # pylint: disable=protected-access - -import pkg_resources import pytest - +import importlib_metadata +import unittest.mock as mock from opentelemetry.instrumentation.dependencies import ( DependencyConflict, get_dependency_conflicts, - get_dist_dependency_conflicts, ) from opentelemetry.test.test_base import TestBase - class TestDependencyConflicts(TestBase): def test_get_dependency_conflicts_empty(self): self.assertIsNone(get_dependency_conflicts([])) @@ -50,25 +47,20 @@ def test_get_dependency_conflicts_mismatched_version(self): f'DependencyConflict: requested: "pytest == 5000" but found: "pytest {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 [] + @mock.patch('importlib_metadata.distribution') + def test_get_dist_dependency_conflicts(self, mock_distribution): + # Mock a distribution return value + mock_dist = mock.MagicMock() + mock_dist.metadata = {'Name': 'test-pkg', 'Version': '1.0'} + mock_dist.requires = lambda: ['test-pkg ~= 1.0'] - dist = pkg_resources.Distribution( - project_name="test-instrumentation", version="1.0" - ) - dist.requires = mock_requires + mock_distribution.return_value = mock_dist - conflict = get_dist_dependency_conflicts(dist) - self.assertTrue(conflict is not None) - self.assertTrue(isinstance(conflict, DependencyConflict)) + conflict = get_dependency_conflicts(['test-pkg ~= 1.0']) + + self.assertIsNotNone(conflict) + self.assertIsInstance(conflict, DependencyConflict) self.assertEqual( str(conflict), - 'DependencyConflict: requested: "test-pkg~=1.0" but found: "None"', + 'DependencyConflict: requested: "test-pkg ~= 1.0" but found: "None"', ) diff --git a/opentelemetry-instrumentation/tests/test_distro.py b/opentelemetry-instrumentation/tests/test_distro.py index 399b3f8a65..c66211488b 100644 --- a/opentelemetry-instrumentation/tests/test_distro.py +++ b/opentelemetry-instrumentation/tests/test_distro.py @@ -13,15 +13,15 @@ # limitations under the License. # type: ignore -from unittest import TestCase +from unittest import TestCase,mock -from pkg_resources import EntryPoint +from importlib_metadata import EntryPoint from opentelemetry.instrumentation.distro import BaseDistro from opentelemetry.instrumentation.instrumentor import BaseInstrumentor -class MockInstrumetor(BaseInstrumentor): +class MockInstrumentor(BaseInstrumentor): def instrumentation_dependencies(self): return [] @@ -47,12 +47,19 @@ def _configure(self, **kwargs): class TestDistro(TestCase): def test_load_instrumentor(self): - # pylint: disable=protected-access distro = MockDistro() - instrumentor = MockInstrumetor() - entry_point = MockEntryPoint(MockInstrumetor) + # Create an instance of MockInstrumentor + instrumentor_instance = MockInstrumentor() - self.assertFalse(instrumentor._is_instrumented_by_opentelemetry) - distro.load_instrumentor(entry_point) - self.assertTrue(instrumentor._is_instrumented_by_opentelemetry) + # Create a mock EntryPoint. The load method should return a callable + # that returns the instrumentor instance + mock_entry_point = mock.Mock() + mock_entry_point.load.return_value = lambda: instrumentor_instance + + self.assertFalse(instrumentor_instance._is_instrumented_by_opentelemetry) + + # Use the mock entry point in your test + distro.load_instrumentor(mock_entry_point) + + self.assertTrue(instrumentor_instance._is_instrumented_by_opentelemetry)