Skip to content

Commit c0cbfbd

Browse files
feat(config): wire container resource detector via entry point loading (#5004)
* config: add resource and propagator creation from declarative config Implements create_resource() and create_propagator()/configure_propagator() for the declarative file configuration. Resource creation does not read OTEL_RESOURCE_ATTRIBUTES or run any detectors (matches Java/JS SDK behavior). Propagator configuration always calls set_global_textmap to override Python's default tracecontext+baggage, setting a noop CompositePropagator when no propagator is configured. Assisted-by: Claude Sonnet 4.6 * update changelog with PR number Assisted-by: Claude Sonnet 4.6 * fix pylint, pyright and ruff errors in resource/propagator config - _resource.py: refactor _coerce_attribute_value to dispatch table to avoid too-many-return-statements; fix short variable names k/v -> attr_key/attr_val; fix return type of _sdk_default_attributes to dict[str, str] to satisfy pyright - _propagator.py: rename short variable names e -> exc, p -> propagator - test_resource.py: move imports to top level; split TestCreateResource (25 methods) into three focused classes to satisfy too-many-public-methods - test_propagator.py: add pylint disable for protected-access Assisted-by: Claude Sonnet 4.6 * address review feedback: use _DEFAULT_RESOURCE, fix bool_array coercion - replace _sdk_default_attributes() with _DEFAULT_RESOURCE from resources module - move _coerce_bool into dispatch tables for both scalar and array bool types, fixing a bug where bool_array with string values like "false" would coerce incorrectly via plain bool() (non-empty string -> True) - add test for bool_array with string values to cover the bug Assisted-by: Claude Sonnet 4.6 * fix linter * address review feedback: single coercion table, simplify attributes merge - collapse _SCALAR_COERCIONS and _ARRAY_COERCIONS into a single _COERCIONS dict using an _array() factory, reducing _coerce_attribute_value to two lines - process attributes_list before attributes so explicit attributes naturally overwrite list entries without needing an explicit guard Assisted-by: Claude Sonnet 4.6 * use Callable type annotation on _array helper Assisted-by: Claude Sonnet 4.6 * add detection infrastructure foundations for resource detectors Adds _run_detectors() stub and _filter_attributes() to create_resource(), providing the shared scaffolding for detector PRs to build on. Detectors are opt-in: nothing runs unless explicitly listed under detection_development.detectors in the config. The include/exclude attribute filter mirrors other SDK behaviour. Assisted-by: Claude Sonnet 4.6 * move service.name default into base resource Merges service.name=unknown_service into base before running detectors, so detectors (e.g. service) can override it. Previously it was added to config_attrs and merged last, which would have silently overridden any detector-provided service.name. Assisted-by: Claude Sonnet 4.6 * remove unused logging import from _propagator.py Assisted-by: Claude Sonnet 4.6 * wire container resource detector in declarative config When `resource.detection_development.detectors[].container` is set in the config file, attempt to use `ContainerResourceDetector` from the `opentelemetry-resource-detector-containerid` contrib package. Unlike the process, host, and service detectors which are implemented in the core SDK, container detection is not available in core. Rather than adding a hard dependency on the contrib package, we use a lazy import: if the package is installed it is used transparently; if absent a warning is logged with an actionable install instruction. This mirrors the approach taken by other SDKs: JS explicitly skips container detection when no implementation is available, and PHP also defers container detection to a contrib package. See: open-telemetry/opentelemetry-configuration#570 Assisted-by: Claude Sonnet 4.6 * add changelog entry for container resource detector (#5004) Assisted-by: Claude Sonnet 4.6 * add pylint disables for lazy container import in _resource.py Assisted-by: Claude Sonnet 4.6 * load container detector via entry point instead of lazy import Matches the env-var config counterpart (OTEL_EXPERIMENTAL_RESOURCE_DETECTORS) and avoids a hard import dependency on the contrib package. Tests updated to mock entry_points instead of sys.modules. Assisted-by: Claude Sonnet 4.6
1 parent f764e45 commit c0cbfbd

File tree

3 files changed

+114
-1
lines changed

3 files changed

+114
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
## Unreleased
1414

15+
- `opentelemetry-sdk`: Add `container` resource detector support to declarative file configuration via `detection_development.detectors[].container`, using entry point loading of the `opentelemetry-resource-detector-containerid` contrib package
16+
([#5004](https://github.com/open-telemetry/opentelemetry-python/pull/5004))
1517
- `opentelemetry-sdk`: Add `create_tracer_provider`/`configure_tracer_provider` to declarative file configuration, enabling TracerProvider instantiation from config files without reading env vars
1618
([#4985](https://github.com/open-telemetry/opentelemetry-python/pull/4985))
1719
- Enabled the flake8-tidy-import plugins rules for the ruff linter. These rules throw warnings for relative imports in the modules.

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
ProcessResourceDetector,
3333
Resource,
3434
)
35+
from opentelemetry.util._importlib_metadata import entry_points
3536

3637
_logger = logging.getLogger(__name__)
3738

@@ -150,6 +151,30 @@ def _run_detectors(
150151
is updated in-place; later detectors overwrite earlier ones for the
151152
same key.
152153
"""
154+
if detector_config.container is not None:
155+
# The container detector is not part of the core SDK. It is provided
156+
# by the opentelemetry-resource-detector-containerid contrib package,
157+
# which registers itself under the opentelemetry_resource_detector
158+
# entry point group as "container". Loading via entry point matches
159+
# the env-var config counterpart (OTEL_EXPERIMENTAL_RESOURCE_DETECTORS)
160+
# and avoids a hard import dependency on contrib. See also:
161+
# https://github.com/open-telemetry/opentelemetry-configuration/issues/570
162+
ep = next(
163+
iter(
164+
entry_points(
165+
group="opentelemetry_resource_detector", name="container"
166+
)
167+
),
168+
None,
169+
)
170+
if ep is None:
171+
_logger.warning(
172+
"container resource detector requested but "
173+
"'opentelemetry-resource-detector-containerid' is not "
174+
"installed; install it to enable container detection"
175+
)
176+
else:
177+
detected_attrs.update(ep.load()().detect().attributes)
153178
if detector_config.process is not None:
154179
detected_attrs.update(ProcessResourceDetector().detect().attributes)
155180

opentelemetry-sdk/tests/_configuration/test_resource.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import os
1616
import sys
1717
import unittest
18-
from unittest.mock import patch
18+
from unittest.mock import MagicMock, patch
1919

2020
from opentelemetry.sdk._configuration._resource import create_resource
2121
from opentelemetry.sdk._configuration.models import (
@@ -26,6 +26,7 @@
2626
)
2727
from opentelemetry.sdk._configuration.models import Resource as ResourceConfig
2828
from opentelemetry.sdk.resources import (
29+
CONTAINER_ID,
2930
PROCESS_PID,
3031
PROCESS_RUNTIME_NAME,
3132
SERVICE_NAME,
@@ -302,6 +303,91 @@ def test_attributes_list_invalid_pair_skipped(self):
302303
self.assertTrue(any("no-equals" in msg for msg in cm.output))
303304

304305

306+
class TestContainerResourceDetector(unittest.TestCase):
307+
@staticmethod
308+
def _config_with_container() -> ResourceConfig:
309+
return ResourceConfig(
310+
detection_development=ExperimentalResourceDetection(
311+
detectors=[ExperimentalResourceDetector(container={})]
312+
)
313+
)
314+
315+
def test_container_detector_not_run_when_absent(self):
316+
resource = create_resource(ResourceConfig())
317+
self.assertNotIn(CONTAINER_ID, resource.attributes)
318+
319+
def test_container_detector_not_run_when_detection_development_is_none(
320+
self,
321+
):
322+
resource = create_resource(ResourceConfig(detection_development=None))
323+
self.assertNotIn(CONTAINER_ID, resource.attributes)
324+
325+
def test_container_detector_not_run_when_detectors_list_empty(self):
326+
config = ResourceConfig(
327+
detection_development=ExperimentalResourceDetection(detectors=[])
328+
)
329+
resource = create_resource(config)
330+
self.assertNotIn(CONTAINER_ID, resource.attributes)
331+
332+
def test_container_detector_warns_when_package_missing(self):
333+
"""A warning is logged when the contrib entry point is not found."""
334+
with patch(
335+
"opentelemetry.sdk._configuration._resource.entry_points",
336+
return_value=[],
337+
):
338+
with self.assertLogs(
339+
"opentelemetry.sdk._configuration._resource", level="WARNING"
340+
) as cm:
341+
resource = create_resource(self._config_with_container())
342+
self.assertNotIn(CONTAINER_ID, resource.attributes)
343+
self.assertTrue(
344+
any(
345+
"opentelemetry-resource-detector-containerid" in msg
346+
for msg in cm.output
347+
)
348+
)
349+
350+
def test_container_detector_uses_contrib_when_available(self):
351+
"""When the contrib entry point is registered, container.id is detected."""
352+
mock_resource = Resource({CONTAINER_ID: "abc123"})
353+
mock_detector = MagicMock()
354+
mock_detector.return_value.detect.return_value = mock_resource
355+
mock_ep = MagicMock()
356+
mock_ep.load.return_value = mock_detector
357+
358+
with patch(
359+
"opentelemetry.sdk._configuration._resource.entry_points",
360+
return_value=[mock_ep],
361+
):
362+
resource = create_resource(self._config_with_container())
363+
364+
self.assertEqual(resource.attributes[CONTAINER_ID], "abc123")
365+
366+
def test_explicit_attributes_override_container_detector(self):
367+
"""Config attributes win over detector-provided values."""
368+
mock_resource = Resource({CONTAINER_ID: "detected-id"})
369+
mock_detector = MagicMock()
370+
mock_detector.return_value.detect.return_value = mock_resource
371+
mock_ep = MagicMock()
372+
mock_ep.load.return_value = mock_detector
373+
374+
config = ResourceConfig(
375+
attributes=[
376+
AttributeNameValue(name="container.id", value="explicit-id")
377+
],
378+
detection_development=ExperimentalResourceDetection(
379+
detectors=[ExperimentalResourceDetector(container={})]
380+
),
381+
)
382+
with patch(
383+
"opentelemetry.sdk._configuration._resource.entry_points",
384+
return_value=[mock_ep],
385+
):
386+
resource = create_resource(config)
387+
388+
self.assertEqual(resource.attributes[CONTAINER_ID], "explicit-id")
389+
390+
305391
class TestProcessResourceDetector(unittest.TestCase):
306392
@staticmethod
307393
def _config_with_process() -> ResourceConfig:

0 commit comments

Comments
 (0)