Skip to content
Merged
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
1 change: 1 addition & 0 deletions .changelog/5129.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: add generic resource detector plugin loading to declarative file configuration via the `opentelemetry_resource_detector` entry point group, matching the spec's PluginComponentProvider mechanism
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@

from __future__ import annotations

import dataclasses
import fnmatch
import logging
import os
import uuid
from collections.abc import Callable
from typing import Any
from urllib import parse

from opentelemetry.sdk._configuration._common import load_entry_point
from opentelemetry.sdk._configuration.models import (
AttributeNameValue,
AttributeType,
Expand All @@ -26,7 +29,7 @@
Resource,
_HostResourceDetector,
)
from opentelemetry.util._importlib_metadata import entry_points
from opentelemetry.util.types import AttributeValue

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -135,55 +138,57 @@ def create_resource(config: ResourceConfig | None) -> Resource:
return result.merge(config_resource)


def _detect_service(_config: Any) -> dict[str, AttributeValue]:
"""Service detector: generates instance ID and reads OTEL_SERVICE_NAME."""
attrs: dict[str, AttributeValue] = {
SERVICE_INSTANCE_ID: str(uuid.uuid4()),
}
if service_name := os.environ.get(OTEL_SERVICE_NAME):
attrs[SERVICE_NAME] = service_name
return attrs


_RESOURCE_DETECTOR_REGISTRY: dict[
str, Callable[[Any], dict[str, AttributeValue]]
] = {
"service": _detect_service,
"host": lambda _: dict(_HostResourceDetector().detect().attributes),
"process": lambda _: dict(ProcessResourceDetector().detect().attributes),
}


def _run_detectors(
detector_config: ExperimentalResourceDetector,
detected_attrs: dict[str, object],
) -> None:
"""Run any detectors present in a single detector config entry.
"""Run detectors present in a single detector config entry.

Each detector PR adds its own branch here. The detected_attrs dict
is updated in-place; later detectors overwrite earlier ones for the
same key.
Known detectors (service, host, process) are handled directly via
_RESOURCE_DETECTOR_REGISTRY. All other detectors — including known
schema fields like container that require contrib packages, and
unknown plugin detectors captured in additional_properties — are
loaded via the ``opentelemetry_resource_detector`` entry point group.

The detected_attrs dict is updated in-place; later detectors overwrite
earlier ones for the same key.
"""
if detector_config.service is not None:
attrs: dict[str, object] = {
SERVICE_INSTANCE_ID: str(uuid.uuid4()),
}
service_name = os.environ.get(OTEL_SERVICE_NAME)
if service_name:
attrs[SERVICE_NAME] = service_name
detected_attrs.update(attrs)

if detector_config.host is not None:
detected_attrs.update(_HostResourceDetector().detect().attributes)

if detector_config.container is not None:
# The container detector is not part of the core SDK. It is provided
# by the opentelemetry-resource-detector-containerid contrib package,
# which registers itself under the opentelemetry_resource_detector
# entry point group as "container". Loading via entry point matches
# the env-var config counterpart (OTEL_EXPERIMENTAL_RESOURCE_DETECTORS)
# and avoids a hard import dependency on contrib. See also:
# https://github.com/open-telemetry/opentelemetry-configuration/issues/570
ep = next(
iter(
entry_points(
group="opentelemetry_resource_detector", name="container"
)
),
None,
)
if ep is None:
_logger.warning(
"container resource detector requested but "
"'opentelemetry-resource-detector-containerid' is not "
"installed; install it to enable container detection"
for name in dataclasses.fields(detector_config):
value = getattr(detector_config, name.name, None)
if value is None:
continue
if name.name in _RESOURCE_DETECTOR_REGISTRY:
detected_attrs.update(
_RESOURCE_DETECTOR_REGISTRY[name.name](value)
Comment thread
MikeGoldsmith marked this conversation as resolved.
)
else:
detected_attrs.update(ep.load()().detect().attributes)
cls = load_entry_point(
"opentelemetry_resource_detector", name.name
)
detected_attrs.update(cls(**(value or {})).detect().attributes)

if detector_config.process is not None:
detected_attrs.update(ProcessResourceDetector().detect().attributes)
for name, plugin_config in detector_config.additional_properties.items():
cls = load_entry_point("opentelemetry_resource_detector", name)
detected_attrs.update(cls(**(plugin_config or {})).detect().attributes)


def _filter_attributes(
Expand Down
61 changes: 45 additions & 16 deletions opentelemetry-sdk/tests/_configuration/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import unittest
from unittest.mock import MagicMock, patch

from opentelemetry.sdk._configuration._exceptions import ConfigurationError
from opentelemetry.sdk._configuration._resource import create_resource
from opentelemetry.sdk._configuration.models import (
AttributeNameValue,
Expand Down Expand Up @@ -467,23 +468,14 @@ def test_container_detector_not_run_when_detectors_list_empty(self):
resource = create_resource(config)
self.assertNotIn(CONTAINER_ID, resource.attributes)

def test_container_detector_warns_when_package_missing(self):
"""A warning is logged when the contrib entry point is not found."""
def test_container_detector_raises_when_package_missing(self):
"""ConfigurationError is raised when the contrib entry point is not found."""
with patch(
"opentelemetry.sdk._configuration._resource.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[],
):
with self.assertLogs(
"opentelemetry.sdk._configuration._resource", level="WARNING"
) as cm:
resource = create_resource(self._config_with_container())
self.assertNotIn(CONTAINER_ID, resource.attributes)
self.assertTrue(
any(
"opentelemetry-resource-detector-containerid" in msg
for msg in cm.output
)
)
with self.assertRaises(ConfigurationError):
create_resource(self._config_with_container())

def test_container_detector_uses_contrib_when_available(self):
"""When the contrib entry point is registered, container.id is detected."""
Expand All @@ -494,7 +486,7 @@ def test_container_detector_uses_contrib_when_available(self):
mock_ep.load.return_value = mock_detector

with patch(
"opentelemetry.sdk._configuration._resource.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
resource = create_resource(self._config_with_container())
Expand All @@ -518,7 +510,7 @@ def test_explicit_attributes_override_container_detector(self):
),
)
with patch(
"opentelemetry.sdk._configuration._resource.entry_points",
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
resource = create_resource(config)
Expand Down Expand Up @@ -591,3 +583,40 @@ def test_multiple_detector_entries_run_process_once(self):
)
resource = create_resource(config)
self.assertEqual(resource.attributes[PROCESS_PID], os.getpid())


class TestPluginResourceDetector(unittest.TestCase):
def test_plugin_detector_loaded_via_entry_point(self):
mock_resource = Resource({"custom.attr": "value"})
mock_detector = MagicMock()
mock_detector.return_value.detect.return_value = mock_resource
mock_ep = MagicMock()
mock_ep.load.return_value = mock_detector

config = ResourceConfig(
detection_development=ExperimentalResourceDetection(
# pylint: disable=unexpected-keyword-arg
detectors=[ExperimentalResourceDetector(my_custom_detector={})]
)
)
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[mock_ep],
):
resource = create_resource(config)

self.assertEqual(resource.attributes["custom.attr"], "value")

def test_unknown_detector_raises_configuration_error(self):
config = ResourceConfig(
detection_development=ExperimentalResourceDetection(
# pylint: disable=unexpected-keyword-arg
detectors=[ExperimentalResourceDetector(no_such_detector={})]
)
)
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[],
):
with self.assertRaises(ConfigurationError):
create_resource(config)
Loading