diff --git a/CHANGELOG.md b/CHANGELOG.md index 941551d8a9..241a1c299d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#5088](https://github.com/open-telemetry/opentelemetry-python/pull/5088)) - ci: wait for tracecontext server readiness instead of a fixed sleep in `scripts/tracecontext-integration-test.sh` ([#5149](https://github.com/open-telemetry/opentelemetry-python/pull/5149)) +- `opentelemetry-sdk`: only load entrypoints for resource detectors if they are configured via `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` + ([#5145](https://github.com/open-telemetry/opentelemetry-python/pull/5145)) ## Version 1.41.0/0.62b0 (2026-04-09) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index beb21098e1..3e0658ceb2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -70,7 +70,7 @@ from json import dumps from os import environ from types import ModuleType -from typing import List, Optional, cast +from typing import Optional, cast from urllib import parse from opentelemetry.attributes import BoundedAttributes @@ -79,11 +79,10 @@ OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, ) -from opentelemetry.semconv.resource import ResourceAttributes -from opentelemetry.util._importlib_metadata import ( - entry_points, # type: ignore[reportUnknownVariableType] - version, +from opentelemetry.sdk.version import ( + __version__ as _OPENTELEMETRY_SDK_VERSION, ) +from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.util.types import AttributeValue psutil: Optional[ModuleType] = None @@ -158,8 +157,6 @@ TELEMETRY_AUTO_VERSION = ResourceAttributes.TELEMETRY_AUTO_VERSION TELEMETRY_SDK_LANGUAGE = ResourceAttributes.TELEMETRY_SDK_LANGUAGE -_OPENTELEMETRY_SDK_VERSION: str = version("opentelemetry-sdk") - class Resource: """A Resource is an immutable representation of the entity producing telemetry as Attributes.""" @@ -195,48 +192,8 @@ def create( if not attributes: attributes = {} - otel_experimental_resource_detectors: list[str] = [ - detector.strip() - for detector in environ.get( - OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "" - ).split(",") - if detector.strip() - ] - - resource_detectors: List[ResourceDetector] = [] - - if "*" in otel_experimental_resource_detectors: - otel_experimental_resource_detectors = [ - name - for name in sorted( - entry_points(group="opentelemetry_resource_detector").names - ) - if name != "otel" - ] - otel_experimental_resource_detectors.append("otel") - elif "otel" not in otel_experimental_resource_detectors: - otel_experimental_resource_detectors.append("otel") - - for resource_detector in otel_experimental_resource_detectors: - try: - resource_detectors.append( - next( - iter( - entry_points( - group="opentelemetry_resource_detector", - name=resource_detector.strip(), - ) # type: ignore[reportUnknownArgumentType] - ) - ).load()() - ) - except Exception: # pylint: disable=broad-exception-caught - logger.exception( - "Failed to load resource detector '%s', skipping", - resource_detector, - ) - continue resource = get_aggregated_resources( - resource_detectors, _DEFAULT_RESOURCE + _build_resource_detectors(), _DEFAULT_RESOURCE ).merge(Resource(attributes, schema_url)) if not resource.attributes.get(SERVICE_NAME, None): @@ -323,6 +280,7 @@ def to_json(self, indent: Optional[int] = 4) -> str: _EMPTY_RESOURCE = Resource({}) + _DEFAULT_RESOURCE = Resource( { TELEMETRY_SDK_LANGUAGE: "python", @@ -516,6 +474,74 @@ def detect(self) -> "Resource": ) +def _build_resource_detectors() -> list["ResourceDetector"]: + """Returns the ordered list of resource detectors to use for Resource.create. + + Fast path: if no extra detectors are configured, returns only + OTELResourceDetector without scanning entry_points. + + "otel" (OTELResourceDetector) defaults to last position so that + OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME take highest merge priority, + but an explicit position in OTEL_EXPERIMENTAL_RESOURCE_DETECTORS is respected. + """ + detector_names: list[str] = list( + dict.fromkeys( + name.strip() + for name in environ.get( + OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "" + ).split(",") + if name.strip() + ) + ) + + if "otel" not in detector_names: + detector_names.append("otel") + + # Fast path: only the built-in "otel" detector — no entry_points scan needed. + if detector_names == ["otel"]: + return [OTELResourceDetector()] + + # pylint: disable=import-outside-toplevel + from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 + entry_points, # type: ignore[reportUnknownVariableType] + ) + + if "*" in detector_names: + registered = sorted( + name + for name in entry_points( + group="opentelemetry_resource_detector" + ).names # type: ignore[reportUnknownArgumentType] + if name != "otel" + ) + existing = set(detector_names) - {"*"} + expansion = [n for n in registered if n not in existing] + idx = detector_names.index("*") + detector_names = ( + detector_names[:idx] + expansion + detector_names[idx + 1 :] + ) + + detectors: list[ResourceDetector] = [] + for name in detector_names: + try: + detectors.append( + next( + iter( + entry_points( + group="opentelemetry_resource_detector", + name=name, + ) # type: ignore[reportUnknownArgumentType] + ) + ).load()() + ) + except Exception: # pylint: disable=broad-exception-caught + logger.exception( + "Failed to load resource detector '%s', skipping", + name, + ) + return detectors + + def get_aggregated_resources( detectors: typing.List["ResourceDetector"], initial_resource: typing.Optional[Resource] = None, diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 19f066a8ac..d5f98b7927 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -688,7 +688,7 @@ def test_resource_detector_entry_points_default(self): environ, {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "mock"}, clear=True ) @patch( - "opentelemetry.sdk.resources.entry_points", + "opentelemetry.util._importlib_metadata.entry_points", Mock( return_value=[ Mock( @@ -827,7 +827,8 @@ def side_effect(*args, **kwargs): ) with patch( - "opentelemetry.sdk.resources.entry_points", side_effect=side_effect + "opentelemetry.util._importlib_metadata.entry_points", + side_effect=side_effect, ): resource = Resource({}).create() @@ -851,7 +852,8 @@ def side_effect(*args, **kwargs): return real_entry_points(*args, **kwargs) with patch( - "opentelemetry.sdk.resources.entry_points", side_effect=side_effect + "opentelemetry.util._importlib_metadata.entry_points", + side_effect=side_effect, ): resource = Resource({}).create()