From ae1983238f07d2ffc80c63a88e1cc19542a5e2c3 Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:39:42 -0700 Subject: [PATCH 1/6] update SDK to call version directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing the call to version in the resource to reduce the amount of libraries imported. ~17–27% memory reduction across all scenarios ~11–23% startup time reduction Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> --- .../opentelemetry/sdk/resources/__init__.py | 88 +++++++++---------- .../tests/resources/test_resources.py | 2 +- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index beb21098e1..3cf44c1951 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -80,10 +80,6 @@ OTEL_SERVICE_NAME, ) from opentelemetry.semconv.resource import ResourceAttributes -from opentelemetry.util._importlib_metadata import ( - entry_points, # type: ignore[reportUnknownVariableType] - version, -) from opentelemetry.util.types import AttributeValue psutil: Optional[ModuleType] = None @@ -158,9 +154,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,46 +188,46 @@ 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") + # "otel" is always included and resolves to OTELResourceDetector (defined in + # this module), so we instantiate it directly to avoid an entry_points scan + # in the common case where no extra detectors are configured. + resource_detectors: List[ResourceDetector] = [OTELResourceDetector()] - 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, + extra_detector_names: Set[str] = { + name.strip() + for name in environ.get(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "").split(",") + if name.strip() and name.strip() != "otel" + } + + if extra_detector_names: + from opentelemetry.util._importlib_metadata import entry_points # type: ignore[reportUnknownVariableType] + + if "*" in extra_detector_names: + # Expand wildcard to all registered detectors except "otel" (already added) + extra_detector_names = ( + set(entry_points(group="opentelemetry_resource_detector").names) # type: ignore[reportUnknownArgumentType] + - {"otel"} ) - continue + + for name in extra_detector_names: + try: + resource_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, + ) + continue + resource = get_aggregated_resources( resource_detectors, _DEFAULT_RESOURCE ).merge(Resource(attributes, schema_url)) @@ -323,6 +316,10 @@ def to_json(self, indent: Optional[int] = 4) -> str: _EMPTY_RESOURCE = Resource({}) + + +from opentelemetry.sdk.version import __version__ as _OPENTELEMETRY_SDK_VERSION + _DEFAULT_RESOURCE = Resource( { TELEMETRY_SDK_LANGUAGE: "python", @@ -332,6 +329,7 @@ def to_json(self, indent: Optional[int] = 4) -> str: ) + class ResourceDetector(abc.ABC): def __init__(self, raise_on_error: bool = False) -> None: self.raise_on_error = raise_on_error diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 19f066a8ac..5146832fe8 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( From 8d34c23c9d8bfa0795729d4d5842ecb852b48bf1 Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Mon, 27 Apr 2026 11:44:02 -0700 Subject: [PATCH 2/6] apply feedback Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> --- .../opentelemetry/sdk/resources/__init__.py | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 3cf44c1951..c34a7e1a1e 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 @@ -188,25 +188,28 @@ def create( if not attributes: attributes = {} - # "otel" is always included and resolves to OTELResourceDetector (defined in - # this module), so we instantiate it directly to avoid an entry_points scan - # in the common case where no extra detectors are configured. - resource_detectors: List[ResourceDetector] = [OTELResourceDetector()] + # Preserve env var ordering; deduplicate while keeping first occurrence. + # "otel" is excluded here and always appended last so OTEL_RESOURCE_ATTRIBUTES + # and OTEL_SERVICE_NAME take highest priority when merging. + extra_detector_names: list[str] = list( + dict.fromkeys( + name.strip() + for name in environ.get(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "").split(",") + if name.strip() and name.strip() != "otel" + ) + ) - extra_detector_names: Set[str] = { - name.strip() - for name in environ.get(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "").split(",") - if name.strip() and name.strip() != "otel" - } + resource_detectors: list[ResourceDetector] = [] if extra_detector_names: from opentelemetry.util._importlib_metadata import entry_points # type: ignore[reportUnknownVariableType] if "*" in extra_detector_names: - # Expand wildcard to all registered detectors except "otel" (already added) - extra_detector_names = ( - set(entry_points(group="opentelemetry_resource_detector").names) # type: ignore[reportUnknownArgumentType] - - {"otel"} + # Expand wildcard to all registered detectors except "otel" (appended last) + extra_detector_names = sorted( + name + for name in entry_points(group="opentelemetry_resource_detector").names # type: ignore[reportUnknownArgumentType] + if name != "otel" ) for name in extra_detector_names: @@ -228,6 +231,10 @@ def create( ) continue + # OTELResourceDetector is instantiated directly (no entry_points scan) and + # appended last so env var attributes override all other detectors. + resource_detectors.append(OTELResourceDetector()) + resource = get_aggregated_resources( resource_detectors, _DEFAULT_RESOURCE ).merge(Resource(attributes, schema_url)) From 2f7a25d0e08d52b065400174fe77e9ed4e4952ce Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:05:16 -0700 Subject: [PATCH 3/6] tidy precommit Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> --- .../opentelemetry/sdk/resources/__init__.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index c34a7e1a1e..dd28bbce7f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -79,6 +79,9 @@ OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME, ) +from opentelemetry.sdk.version import ( + __version__ as _OPENTELEMETRY_SDK_VERSION, +) from opentelemetry.semconv.resource import ResourceAttributes from opentelemetry.util.types import AttributeValue @@ -154,6 +157,7 @@ TELEMETRY_AUTO_VERSION = ResourceAttributes.TELEMETRY_AUTO_VERSION TELEMETRY_SDK_LANGUAGE = ResourceAttributes.TELEMETRY_SDK_LANGUAGE + class Resource: """A Resource is an immutable representation of the entity producing telemetry as Attributes.""" @@ -194,7 +198,9 @@ def create( extra_detector_names: list[str] = list( dict.fromkeys( name.strip() - for name in environ.get(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "").split(",") + for name in environ.get( + OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "" + ).split(",") if name.strip() and name.strip() != "otel" ) ) @@ -202,13 +208,18 @@ def create( resource_detectors: list[ResourceDetector] = [] if extra_detector_names: - from opentelemetry.util._importlib_metadata import entry_points # type: ignore[reportUnknownVariableType] + # pylint: disable=import-outside-toplevel + from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 + entry_points, # type: ignore[reportUnknownVariableType] + ) if "*" in extra_detector_names: # Expand wildcard to all registered detectors except "otel" (appended last) extra_detector_names = sorted( name - for name in entry_points(group="opentelemetry_resource_detector").names # type: ignore[reportUnknownArgumentType] + for name in entry_points( + group="opentelemetry_resource_detector" + ).names # type: ignore[reportUnknownArgumentType] if name != "otel" ) @@ -324,9 +335,6 @@ def to_json(self, indent: Optional[int] = 4) -> str: _EMPTY_RESOURCE = Resource({}) - -from opentelemetry.sdk.version import __version__ as _OPENTELEMETRY_SDK_VERSION - _DEFAULT_RESOURCE = Resource( { TELEMETRY_SDK_LANGUAGE: "python", @@ -336,7 +344,6 @@ def to_json(self, indent: Optional[int] = 4) -> str: ) - class ResourceDetector(abc.ABC): def __init__(self, raise_on_error: bool = False) -> None: self.raise_on_error = raise_on_error From 1f4af298c3134fe64d5cda6794c45003a94dccac Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:55:29 -0700 Subject: [PATCH 4/6] preserve ordering of resource detectors as before, refactored code into method Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> --- .../opentelemetry/sdk/resources/__init__.py | 124 ++++++++++-------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index dd28bbce7f..3e0658ceb2 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -192,62 +192,8 @@ def create( if not attributes: attributes = {} - # Preserve env var ordering; deduplicate while keeping first occurrence. - # "otel" is excluded here and always appended last so OTEL_RESOURCE_ATTRIBUTES - # and OTEL_SERVICE_NAME take highest priority when merging. - extra_detector_names: list[str] = list( - dict.fromkeys( - name.strip() - for name in environ.get( - OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "" - ).split(",") - if name.strip() and name.strip() != "otel" - ) - ) - - resource_detectors: list[ResourceDetector] = [] - - if extra_detector_names: - # pylint: disable=import-outside-toplevel - from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 - entry_points, # type: ignore[reportUnknownVariableType] - ) - - if "*" in extra_detector_names: - # Expand wildcard to all registered detectors except "otel" (appended last) - extra_detector_names = sorted( - name - for name in entry_points( - group="opentelemetry_resource_detector" - ).names # type: ignore[reportUnknownArgumentType] - if name != "otel" - ) - - for name in extra_detector_names: - try: - resource_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, - ) - continue - - # OTELResourceDetector is instantiated directly (no entry_points scan) and - # appended last so env var attributes override all other detectors. - resource_detectors.append(OTELResourceDetector()) - 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): @@ -528,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, From 74939e941414c9a7ae25200a507d592ab7d38abe Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:58:50 -0700 Subject: [PATCH 5/6] changelog Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 614f240d4e..7488a41980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#5120](https://github.com/open-telemetry/opentelemetry-python/pull/5120)) - Add WeaverLiveCheck test util ([#5088](https://github.com/open-telemetry/opentelemetry-python/pull/5088)) +- `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) From db9b6e427e0b4fc75faa0f96d07a7ad1f9934c86 Mon Sep 17 00:00:00 2001 From: Alex Boten <223565+codeboten@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:09:51 -0700 Subject: [PATCH 6/6] fixes Signed-off-by: Alex Boten <223565+codeboten@users.noreply.github.com> --- CHANGELOG.md | 1 - opentelemetry-sdk/tests/resources/test_resources.py | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7488a41980..c327c58cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#5088](https://github.com/open-telemetry/opentelemetry-python/pull/5088)) - `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/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 5146832fe8..d5f98b7927 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -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()