diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e5d5c74bd..42ec9138cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +### Fixed + +- `opentelemetry-instrumentation` Catch `ModuleNotFoundError` when the library is not installed + and log as debug instead of exception + ([#3423](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3423)) + ## Version 1.32.0/0.53b0 (2025-04-10) ### Added diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py index 5084879faa..4bbd95b41c 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py @@ -82,6 +82,14 @@ def _load_instrumentors(distro): exc.conflict, ) continue + except ModuleNotFoundError as exc: + # ModuleNotFoundError is raised when the library is not installed + # and the instrumentation is not required to be loaded. + # See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3421 + _logger.debug( + "Skipping instrumentation %s: %s", entry_point.name, exc.msg + ) + continue except ImportError: # in scenarios using the kubernetes operator to do autoinstrumentation some # instrumentors (usually requiring binary extensions) may fail to load diff --git a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py index f4d4891739..37af270301 100644 --- a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py +++ b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py @@ -326,11 +326,12 @@ def test_load_instrumentors_dep_conflict(self, iter_mock, mock_logger): # pylin ] ) + @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") @patch( "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" ) def test_load_instrumentors_import_error_does_not_stop_everything( - self, iter_mock + self, iter_mock, mock_logger ): ep_mock1 = Mock(name="instr1") ep_mock2 = Mock(name="instr2") @@ -354,6 +355,12 @@ def test_load_instrumentors_import_error_does_not_stop_everything( ] ) self.assertEqual(distro_mock.load_instrumentor.call_count, 2) + mock_logger.exception.assert_any_call( + "Importing of %s failed, skipping it", + ep_mock1.name, + ) + + mock_logger.debug.assert_any_call("Instrumented %s", ep_mock2.name) @patch( "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" @@ -382,6 +389,46 @@ def test_load_instrumentors_raises_exception(self, iter_mock): ) self.assertEqual(distro_mock.load_instrumentor.call_count, 1) + @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + def test_load_instrumentors_module_not_found_error( + self, iter_mock, mock_logger + ): + ep_mock1 = Mock() + ep_mock1.name = "instr1" + + ep_mock2 = Mock() + ep_mock2.name = "instr2" + + distro_mock = Mock() + + distro_mock.load_instrumentor.side_effect = [ + ModuleNotFoundError("No module named 'fake_module'"), + None, + ] + + iter_mock.side_effect = [(), (ep_mock1, ep_mock2), ()] + + _load._load_instrumentors(distro_mock) + + distro_mock.load_instrumentor.assert_has_calls( + [ + call(ep_mock1, raise_exception_on_conflict=True), + call(ep_mock2, raise_exception_on_conflict=True), + ] + ) + self.assertEqual(distro_mock.load_instrumentor.call_count, 2) + + mock_logger.debug.assert_any_call( + "Skipping instrumentation %s: %s", + "instr1", + "No module named 'fake_module'", + ) + + mock_logger.debug.assert_any_call("Instrumented %s", ep_mock2.name) + def test_load_instrumentors_no_entry_point_mocks(self): distro_mock = Mock() _load._load_instrumentors(distro_mock)