autoinstrumentation: catch `ModuleNotFoundError` when the library is not installed (#3423)
* catch ModuleNotFoundError when the library is not installed and prevent exception from bubbling up Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * cleanup Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * remove dup test Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com> * Update CHANGELOG.md --------- Signed-off-by: emdneto <9735060+emdneto@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									7562ff08f3
								
							
						
					
					
						commit
						9c969f363e
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -326,11 +326,12 @@ class TestLoad(TestCase): | |||
|             ] | ||||
|         ) | ||||
| 
 | ||||
|     @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 @@ class TestLoad(TestCase): | |||
|             ] | ||||
|         ) | ||||
|         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 @@ class TestLoad(TestCase): | |||
|         ) | ||||
|         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) | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue