Skip to content

Commit 49762d1

Browse files
committed
Make auto instrumentation use the same dependency resolver as manual instrumentation does
1 parent 5478a0b commit 49762d1

File tree

6 files changed

+119
-140
lines changed

6 files changed

+119
-140
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
1212
## Unreleased
1313

14+
- Make auto instrumentation use the same dependency resolver as manual instrumentation does
15+
([#3202](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3202))
16+
1417
### Added
1518

1619
- `opentelemetry-instrumentation-confluent-kafka` Add support for confluent-kafka <=2.7.0

instrumentation/opentelemetry-instrumentation-fastapi/tests/test_fastapi_instrumentation.py

+54-39
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import unittest
1818
from timeit import default_timer
19-
from unittest.mock import Mock, patch
19+
from unittest.mock import Mock, call, patch
2020

2121
import fastapi
2222
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
@@ -37,6 +37,10 @@
3737
from opentelemetry.instrumentation.auto_instrumentation._load import (
3838
_load_instrumentors,
3939
)
40+
from opentelemetry.instrumentation.dependencies import (
41+
DependencyConflict,
42+
DependencyConflictError,
43+
)
4044
from opentelemetry.sdk.metrics.export import (
4145
HistogramDataPoint,
4246
NumberDataPoint,
@@ -54,10 +58,7 @@
5458
from opentelemetry.semconv.trace import SpanAttributes
5559
from opentelemetry.test.globals_test import reset_trace_globals
5660
from opentelemetry.test.test_base import TestBase
57-
from opentelemetry.util._importlib_metadata import (
58-
PackageNotFoundError,
59-
entry_points,
60-
)
61+
from opentelemetry.util._importlib_metadata import entry_points
6162
from opentelemetry.util.http import (
6263
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS,
6364
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST,
@@ -1031,26 +1032,6 @@ def client_response_hook(send_span, scope, message):
10311032
)
10321033

10331034

1034-
def mock_version_with_fastapi(*args, **kwargs):
1035-
req_name = args[0]
1036-
if req_name == "fastapi":
1037-
# TODO: Value now matters
1038-
return "0.58"
1039-
raise PackageNotFoundError()
1040-
1041-
1042-
def mock_version_with_old_fastapi(*args, **kwargs):
1043-
req_name = args[0]
1044-
if req_name == "fastapi":
1045-
# TODO: Value now matters
1046-
return "0.57"
1047-
raise PackageNotFoundError()
1048-
1049-
1050-
def mock_version_without_fastapi(*args, **kwargs):
1051-
raise PackageNotFoundError()
1052-
1053-
10541035
class TestAutoInstrumentation(TestBaseAutoFastAPI):
10551036
"""Test the auto-instrumented variant
10561037
@@ -1062,31 +1043,65 @@ def test_entry_point_exists(self):
10621043
(ep,) = entry_points(group="opentelemetry_instrumentor")
10631044
self.assertEqual(ep.name, "fastapi")
10641045

1065-
@patch("opentelemetry.instrumentation.dependencies.version")
1066-
def test_instruments_with_fastapi_installed(self, mock_version):
1067-
mock_version.side_effect = mock_version_with_fastapi
1046+
@staticmethod
1047+
def _instrumentation_loaded_successfully_call():
1048+
return call("Instrumented %s", "fastapi")
1049+
1050+
@staticmethod
1051+
def _instrumentation_failed_to_load_call(dependency_conflict):
1052+
return call(
1053+
"Skipping instrumentation %s: %s", "fastapi", dependency_conflict
1054+
)
1055+
1056+
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
1057+
def test_instruments_with_fastapi_installed(self, mock_logger):
10681058
mock_distro = Mock()
1059+
mock_distro.load_instrumentor.return_value = None
10691060
_load_instrumentors(mock_distro)
1070-
mock_version.assert_called_once_with("fastapi")
10711061
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
10721062
(ep,) = mock_distro.load_instrumentor.call_args.args
10731063
self.assertEqual(ep.name, "fastapi")
1064+
mock_logger.debug.assert_has_calls(
1065+
[self._instrumentation_loaded_successfully_call()]
1066+
)
10741067

1075-
@patch("opentelemetry.instrumentation.dependencies.version")
1076-
def test_instruments_with_old_fastapi_installed(self, mock_version): # pylint: disable=no-self-use
1077-
mock_version.side_effect = mock_version_with_old_fastapi
1068+
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
1069+
def test_instruments_with_old_fastapi_installed(self, mock_logger): # pylint: disable=no-self-use
1070+
dependency_conflict = DependencyConflict("0.58", "0.57")
10781071
mock_distro = Mock()
1072+
mock_distro.load_instrumentor.side_effect = DependencyConflictError(
1073+
dependency_conflict
1074+
)
10791075
_load_instrumentors(mock_distro)
1080-
mock_version.assert_called_once_with("fastapi")
1081-
mock_distro.load_instrumentor.assert_not_called()
1076+
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
1077+
(ep,) = mock_distro.load_instrumentor.call_args.args
1078+
self.assertEqual(ep.name, "fastapi")
1079+
assert (
1080+
self._instrumentation_loaded_successfully_call()
1081+
not in mock_logger.debug.call_args_list
1082+
)
1083+
mock_logger.debug.assert_has_calls(
1084+
[self._instrumentation_failed_to_load_call(dependency_conflict)]
1085+
)
10821086

1083-
@patch("opentelemetry.instrumentation.dependencies.version")
1084-
def test_instruments_without_fastapi_installed(self, mock_version): # pylint: disable=no-self-use
1085-
mock_version.side_effect = mock_version_without_fastapi
1087+
@patch("opentelemetry.instrumentation.auto_instrumentation._load._logger")
1088+
def test_instruments_without_fastapi_installed(self, mock_logger): # pylint: disable=no-self-use
1089+
dependency_conflict = DependencyConflict("0.58", None)
10861090
mock_distro = Mock()
1091+
mock_distro.load_instrumentor.side_effect = DependencyConflictError(
1092+
dependency_conflict
1093+
)
10871094
_load_instrumentors(mock_distro)
1088-
mock_version.assert_called_once_with("fastapi")
1089-
mock_distro.load_instrumentor.assert_not_called()
1095+
self.assertEqual(len(mock_distro.load_instrumentor.call_args_list), 1)
1096+
(ep,) = mock_distro.load_instrumentor.call_args.args
1097+
self.assertEqual(ep.name, "fastapi")
1098+
assert (
1099+
self._instrumentation_loaded_successfully_call()
1100+
not in mock_logger.debug.call_args_list
1101+
)
1102+
mock_logger.debug.assert_has_calls(
1103+
[self._instrumentation_failed_to_load_call(dependency_conflict)]
1104+
)
10901105

10911106
def _create_app(self):
10921107
# instrumentation is handled by the instrument call

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

+12-43
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,22 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from functools import cached_property
1615
from logging import getLogger
1716
from os import environ
1817

19-
from opentelemetry.instrumentation.dependencies import (
20-
get_dist_dependency_conflicts,
21-
)
18+
from opentelemetry.instrumentation.dependencies import DependencyConflictError
2219
from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro
2320
from opentelemetry.instrumentation.environment_variables import (
2421
OTEL_PYTHON_CONFIGURATOR,
2522
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
2623
OTEL_PYTHON_DISTRO,
2724
)
2825
from opentelemetry.instrumentation.version import __version__
29-
from opentelemetry.util._importlib_metadata import (
30-
EntryPoint,
31-
distributions,
32-
entry_points,
33-
)
26+
from opentelemetry.util._importlib_metadata import entry_points
3427

3528
_logger = getLogger(__name__)
3629

3730

38-
class _EntryPointDistFinder:
39-
@cached_property
40-
def _mapping(self):
41-
return {
42-
self._key_for(ep): dist
43-
for dist in distributions()
44-
for ep in dist.entry_points
45-
}
46-
47-
def dist_for(self, entry_point: EntryPoint):
48-
dist = getattr(entry_point, "dist", None)
49-
if dist:
50-
return dist
51-
52-
return self._mapping.get(self._key_for(entry_point))
53-
54-
@staticmethod
55-
def _key_for(entry_point: EntryPoint):
56-
return f"{entry_point.group}:{entry_point.name}:{entry_point.value}"
57-
58-
5931
def _load_distro() -> BaseDistro:
6032
distro_name = environ.get(OTEL_PYTHON_DISTRO, None)
6133
for entry_point in entry_points(group="opentelemetry_distro"):
@@ -83,7 +55,6 @@ def _load_distro() -> BaseDistro:
8355

8456
def _load_instrumentors(distro):
8557
package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, [])
86-
entry_point_finder = _EntryPointDistFinder()
8758
if isinstance(package_to_exclude, str):
8859
package_to_exclude = package_to_exclude.split(",")
8960
# to handle users entering "requests , flask" or "requests, flask" with spaces
@@ -100,19 +71,17 @@ def _load_instrumentors(distro):
10071
continue
10172

10273
try:
103-
entry_point_dist = entry_point_finder.dist_for(entry_point)
104-
conflict = get_dist_dependency_conflicts(entry_point_dist)
105-
if conflict:
106-
_logger.debug(
107-
"Skipping instrumentation %s: %s",
108-
entry_point.name,
109-
conflict,
110-
)
111-
continue
112-
113-
# tell instrumentation to not run dep checks again as we already did it above
114-
distro.load_instrumentor(entry_point, skip_dep_check=True)
74+
distro.load_instrumentor(
75+
entry_point, raise_exception_on_conflict=True
76+
)
11577
_logger.debug("Instrumented %s", entry_point.name)
78+
except DependencyConflictError as exc:
79+
_logger.debug(
80+
"Skipping instrumentation %s: %s",
81+
entry_point.name,
82+
exc.conflict,
83+
)
84+
continue
11685
except ImportError:
11786
# in scenarios using the kubernetes operator to do autoinstrumentation some
11887
# instrumentors (usually requiring binary extensions) may fail to load

opentelemetry-instrumentation/src/opentelemetry/instrumentation/dependencies.py

+8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ def __str__(self):
4040
return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"'
4141

4242

43+
class DependencyConflictError(Exception):
44+
def __init__(self, conflict: DependencyConflict):
45+
self.conflict = conflict
46+
47+
def __str__(self):
48+
return str(self.conflict)
49+
50+
4351
def get_dist_dependency_conflicts(
4452
dist: Distribution,
4553
) -> DependencyConflict | None:

opentelemetry-instrumentation/src/opentelemetry/instrumentation/instrumentor.py

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
)
2929
from opentelemetry.instrumentation.dependencies import (
3030
DependencyConflict,
31+
DependencyConflictError,
3132
get_dependency_conflicts,
3233
)
3334

@@ -104,10 +105,15 @@ def instrument(self, **kwargs: Any):
104105

105106
# check if instrumentor has any missing or conflicting dependencies
106107
skip_dep_check = kwargs.pop("skip_dep_check", False)
108+
raise_exception_on_conflict = kwargs.pop(
109+
"raise_exception_on_conflict", False
110+
)
107111
if not skip_dep_check:
108112
conflict = self._check_dependency_conflicts()
109113
if conflict:
110114
_LOG.error(conflict)
115+
if raise_exception_on_conflict:
116+
raise DependencyConflictError(conflict)
111117
return None
112118

113119
# initialize semantic conventions opt-in if needed

0 commit comments

Comments
 (0)