Skip to content

Commit 970d64f

Browse files
committed
catch ModuleNotFoundError when the library is not installed and prevent exception from bubbling up
Signed-off-by: emdneto <[email protected]>
1 parent 4b83285 commit 970d64f

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
1212
## Unreleased
1313

14+
### Added
15+
16+
### Fixed
17+
18+
- `opentelemetry-instrumentation` Catch `ModuleNotFoundError` when the library is not installed
19+
and prevent exception from bubbling up
20+
([#3423](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3423))
21+
1422
## Version 1.32.0/0.53b0 (2025-04-10)
1523

1624
### Added

opentelemetry-instrumentation/src/opentelemetry/instrumentation/auto_instrumentation/_load.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from logging import getLogger
15+
from logging import DEBUG, basicConfig, getLogger
1616
from os import environ
1717

1818
from opentelemetry.instrumentation.dependencies import DependencyConflictError
@@ -25,6 +25,7 @@
2525
from opentelemetry.instrumentation.version import __version__
2626
from opentelemetry.util._importlib_metadata import entry_points
2727

28+
basicConfig(level=DEBUG)
2829
_logger = getLogger(__name__)
2930

3031

@@ -82,6 +83,14 @@ def _load_instrumentors(distro):
8283
exc.conflict,
8384
)
8485
continue
86+
except ModuleNotFoundError as exc:
87+
# ModuleNotFoundError is raised when the library is not installed
88+
# and the instrumentation is not required to be loaded.
89+
# See https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3421
90+
_logger.debug(
91+
"Skipping instrumentation %s: %s", entry_point.name, exc.msg
92+
)
93+
continue
8594
except ImportError:
8695
# in scenarios using the kubernetes operator to do autoinstrumentation some
8796
# instrumentors (usually requiring binary extensions) may fail to load

opentelemetry-instrumentation/tests/auto_instrumentation/test_load.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,79 @@ def test_load_instrumentors_raises_exception(self, iter_mock):
382382
)
383383
self.assertEqual(distro_mock.load_instrumentor.call_count, 1)
384384

385+
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
386+
@patch(
387+
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
388+
)
389+
def test_load_instrumentors_module_not_found_error(
390+
self, iter_mock, mock_logger
391+
):
392+
ep_mock1 = Mock()
393+
ep_mock1.name = "instr1"
394+
395+
ep_mock2 = Mock()
396+
ep_mock2.name = "instr2"
397+
398+
distro_mock = Mock()
399+
400+
distro_mock.load_instrumentor.side_effect = [
401+
ModuleNotFoundError("No module named 'fake_module'"),
402+
None,
403+
]
404+
405+
iter_mock.side_effect = [(), (ep_mock1, ep_mock2), ()]
406+
407+
_load._load_instrumentors(distro_mock)
408+
409+
distro_mock.load_instrumentor.assert_has_calls(
410+
[
411+
call(ep_mock1, raise_exception_on_conflict=True),
412+
call(ep_mock2, raise_exception_on_conflict=True),
413+
]
414+
)
415+
self.assertEqual(distro_mock.load_instrumentor.call_count, 2)
416+
417+
mock_logger.debug.assert_any_call(
418+
"Skipping instrumentation %s: %s",
419+
"instr1",
420+
"No module named 'fake_module'",
421+
)
422+
423+
mock_logger.debug.assert_any_call("Instrumented %s", ep_mock2.name)
424+
425+
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
426+
@patch(
427+
"opentelemetry.instrumentation.auto_instrumentation._load.entry_points"
428+
)
429+
def test_load_instrumentors_import_error(self, iter_mock, mock_logger):
430+
ep_mock1 = Mock()
431+
ep_mock1.name = "instr1"
432+
433+
ep_mock2 = Mock()
434+
ep_mock2.name = "instr2"
435+
436+
distro_mock = Mock()
437+
distro_mock.load_instrumentor.side_effect = [ImportError, None]
438+
439+
iter_mock.side_effect = [(), (ep_mock1, ep_mock2), ()]
440+
441+
_load._load_instrumentors(distro_mock)
442+
443+
distro_mock.load_instrumentor.assert_has_calls(
444+
[
445+
call(ep_mock1, raise_exception_on_conflict=True),
446+
call(ep_mock2, raise_exception_on_conflict=True),
447+
]
448+
)
449+
self.assertEqual(distro_mock.load_instrumentor.call_count, 2)
450+
451+
mock_logger.exception.assert_any_call(
452+
"Importing of %s failed, skipping it",
453+
ep_mock1.name,
454+
)
455+
456+
mock_logger.debug.assert_any_call("Instrumented %s", ep_mock2.name)
457+
385458
def test_load_instrumentors_no_entry_point_mocks(self):
386459
distro_mock = Mock()
387460
_load._load_instrumentors(distro_mock)

0 commit comments

Comments
 (0)