Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ 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)

Expand Down
122 changes: 74 additions & 48 deletions opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -323,6 +280,7 @@ def to_json(self, indent: Optional[int] = 4) -> str:


_EMPTY_RESOURCE = Resource({})

_DEFAULT_RESOURCE = Resource(
{
TELEMETRY_SDK_LANGUAGE: "python",
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 5 additions & 3 deletions opentelemetry-sdk/tests/resources/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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()

Expand All @@ -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()

Expand Down
Loading