diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0aa36d4f..7843ed0a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `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 + ([#5004](https://github.com/open-telemetry/opentelemetry-python/pull/5004)) - `opentelemetry-sdk`: Add shared `_parse_headers` helper for declarative config OTLP exporters ([#5021](https://github.com/open-telemetry/opentelemetry-python/pull/5021)) - `opentelemetry-api`: Replace a broad exception in attribute cleaning tests to satisfy pylint in the `lint-opentelemetry-api` CI job diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py index d58bd4d31d..1be3ed89f1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py @@ -31,6 +31,7 @@ SERVICE_NAME, Resource, ) +from opentelemetry.util._importlib_metadata import entry_points _logger = logging.getLogger(__name__) @@ -149,6 +150,30 @@ def _run_detectors( is updated in-place; later detectors overwrite earlier ones for the same key. """ + 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" + ) + else: + detected_attrs.update(ep.load()().detect().attributes) def _filter_attributes( diff --git a/opentelemetry-sdk/tests/_configuration/test_resource.py b/opentelemetry-sdk/tests/_configuration/test_resource.py index b50bc03fff..37a793c1fa 100644 --- a/opentelemetry-sdk/tests/_configuration/test_resource.py +++ b/opentelemetry-sdk/tests/_configuration/test_resource.py @@ -14,15 +14,18 @@ import os import unittest -from unittest.mock import patch +from unittest.mock import MagicMock, patch from opentelemetry.sdk._configuration._resource import create_resource from opentelemetry.sdk._configuration.models import ( AttributeNameValue, AttributeType, + ExperimentalResourceDetection, + ExperimentalResourceDetector, ) from opentelemetry.sdk._configuration.models import Resource as ResourceConfig from opentelemetry.sdk.resources import ( + CONTAINER_ID, SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, @@ -295,3 +298,88 @@ def test_attributes_list_invalid_pair_skipped(self): self.assertEqual(resource.attributes["foo"], "bar") self.assertNotIn("no-equals", resource.attributes) self.assertTrue(any("no-equals" in msg for msg in cm.output)) + + +class TestContainerResourceDetector(unittest.TestCase): + @staticmethod + def _config_with_container() -> ResourceConfig: + return ResourceConfig( + detection_development=ExperimentalResourceDetection( + detectors=[ExperimentalResourceDetector(container={})] + ) + ) + + def test_container_detector_not_run_when_absent(self): + resource = create_resource(ResourceConfig()) + self.assertNotIn(CONTAINER_ID, resource.attributes) + + def test_container_detector_not_run_when_detection_development_is_none( + self, + ): + resource = create_resource(ResourceConfig(detection_development=None)) + self.assertNotIn(CONTAINER_ID, resource.attributes) + + def test_container_detector_not_run_when_detectors_list_empty(self): + config = ResourceConfig( + detection_development=ExperimentalResourceDetection(detectors=[]) + ) + 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.""" + with patch( + "opentelemetry.sdk._configuration._resource.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 + ) + ) + + def test_container_detector_uses_contrib_when_available(self): + """When the contrib entry point is registered, container.id is detected.""" + mock_resource = Resource({CONTAINER_ID: "abc123"}) + mock_detector = MagicMock() + mock_detector.return_value.detect.return_value = mock_resource + mock_ep = MagicMock() + mock_ep.load.return_value = mock_detector + + with patch( + "opentelemetry.sdk._configuration._resource.entry_points", + return_value=[mock_ep], + ): + resource = create_resource(self._config_with_container()) + + self.assertEqual(resource.attributes[CONTAINER_ID], "abc123") + + def test_explicit_attributes_override_container_detector(self): + """Config attributes win over detector-provided values.""" + mock_resource = Resource({CONTAINER_ID: "detected-id"}) + mock_detector = MagicMock() + mock_detector.return_value.detect.return_value = mock_resource + mock_ep = MagicMock() + mock_ep.load.return_value = mock_detector + + config = ResourceConfig( + attributes=[ + AttributeNameValue(name="container.id", value="explicit-id") + ], + detection_development=ExperimentalResourceDetection( + detectors=[ExperimentalResourceDetector(container={})] + ), + ) + with patch( + "opentelemetry.sdk._configuration._resource.entry_points", + return_value=[mock_ep], + ): + resource = create_resource(config) + + self.assertEqual(resource.attributes[CONTAINER_ID], "explicit-id")