From 970d64fba20f2464b4e170ed3ab3f15c4a69f8b5 Mon Sep 17 00:00:00 2001 From: emdneto <9735060+emdneto@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:45:22 -0300 Subject: [PATCH 1/4] catch ModuleNotFoundError when the library is not installed and prevent exception from bubbling up Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> --- CHANGELOG.md | 8 ++ .../auto_instrumentation/_load.py | 11 ++- .../tests/auto_instrumentation/test_load.py | 73 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e5d5c74bd..0912f86bb9 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 prevent exception from bubbling up + ([#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..3c7588392d 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from logging import getLogger +from logging import DEBUG, basicConfig, getLogger from os import environ from opentelemetry.instrumentation.dependencies import DependencyConflictError @@ -25,6 +25,7 @@ from opentelemetry.instrumentation.version import __version__ from opentelemetry.util._importlib_metadata import entry_points +basicConfig(level=DEBUG) _logger = getLogger(__name__) @@ -82,6 +83,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..bcde5fe584 100644 --- a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py +++ b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py @@ -382,6 +382,79 @@ 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) + + @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") + @patch( + "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" + ) + def test_load_instrumentors_import_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 = [ImportError, 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.exception.assert_any_call( + "Importing of %s failed, skipping it", + ep_mock1.name, + ) + + 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) From 3ef078761f4a4df945bba6dc5f3f220381fb2fd0 Mon Sep 17 00:00:00 2001 From: emdneto <9735060+emdneto@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:47:26 -0300 Subject: [PATCH 2/4] cleanup Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> --- .../instrumentation/auto_instrumentation/_load.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py index 3c7588392d..4bbd95b41c 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from logging import DEBUG, basicConfig, getLogger +from logging import getLogger from os import environ from opentelemetry.instrumentation.dependencies import DependencyConflictError @@ -25,7 +25,6 @@ from opentelemetry.instrumentation.version import __version__ from opentelemetry.util._importlib_metadata import entry_points -basicConfig(level=DEBUG) _logger = getLogger(__name__) From 1bcb35429962aeca2624fe4b39b27ae7d680387d Mon Sep 17 00:00:00 2001 From: emdneto <9735060+emdneto@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:54:28 -0300 Subject: [PATCH 3/4] remove dup test Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> --- .../tests/auto_instrumentation/test_load.py | 42 ++++--------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py b/opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py index bcde5fe584..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" @@ -422,39 +429,6 @@ def test_load_instrumentors_module_not_found_error( mock_logger.debug.assert_any_call("Instrumented %s", ep_mock2.name) - @patch("opentelemetry.instrumentation.auto_instrumentation._load._logger") - @patch( - "opentelemetry.instrumentation.auto_instrumentation._load.entry_points" - ) - def test_load_instrumentors_import_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 = [ImportError, 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.exception.assert_any_call( - "Importing of %s failed, skipping it", - ep_mock1.name, - ) - - 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) From e56dbaa8aeb483f6158657d590c28c9cde20a9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Em=C3=ADdio=20Neto?= <9735060+emdneto@users.noreply.github.com> Date: Mon, 14 Apr 2025 21:50:07 -0300 Subject: [PATCH 4/4] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0912f86bb9..42ec9138cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - `opentelemetry-instrumentation` Catch `ModuleNotFoundError` when the library is not installed - and prevent exception from bubbling up + 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)