diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a35f9d763..da8d68d312 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 a lazy import of the `opentelemetry-resource-detector-containerid` contrib package + ([#5004](https://github.com/open-telemetry/opentelemetry-python/pull/5004)) - `opentelemetry-sdk`: Add `create_resource` and `create_propagator`/`configure_propagator` to declarative file configuration, enabling Resource and propagator instantiation from config files without reading env vars ([#4979](https://github.com/open-telemetry/opentelemetry-python/pull/4979)) - `opentelemetry-sdk`: Map Python `CRITICAL` log level to OTel `FATAL` severity text per the specification diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py index d58bd4d31d..c7213fe483 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py @@ -149,6 +149,28 @@ 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. + # We attempt a lazy import so the core SDK has no hard dependency on + # contrib; if the package is absent we log an actionable warning rather + # than raising an error. Other SDKs (e.g. JS) similarly skip container + # detection when no implementation is available. See also: + # https://github.com/open-telemetry/opentelemetry-configuration/issues/570 + try: + from opentelemetry.resource.detector.containerid import ( # type: ignore[import-not-found] # noqa: PLC0415 # pylint: disable=import-outside-toplevel,no-name-in-module + ContainerResourceDetector, + ) + + detected_attrs.update( + ContainerResourceDetector().detect().attributes + ) + except ImportError: + _logger.warning( + "container resource detector requested but " + "'opentelemetry-resource-detector-containerid' is not " + "installed; install it to enable container detection" + ) def _filter_attributes( diff --git a/opentelemetry-sdk/tests/_configuration/test_resource.py b/opentelemetry-sdk/tests/_configuration/test_resource.py index b50bc03fff..f2f821a7fa 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 package is not installed.""" + with patch.dict( + "sys.modules", + {"opentelemetry.resource.detector.containerid": None}, + ): + 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 package is installed, container.id is detected.""" + mock_resource = Resource({CONTAINER_ID: "abc123"}) + mock_detector = MagicMock() + mock_detector.return_value.detect.return_value = mock_resource + mock_module = MagicMock() + mock_module.ContainerResourceDetector = mock_detector + + with patch.dict( + "sys.modules", + {"opentelemetry.resource.detector.containerid": mock_module}, + ): + 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_module = MagicMock() + mock_module.ContainerResourceDetector = mock_detector + + config = ResourceConfig( + attributes=[ + AttributeNameValue(name="container.id", value="explicit-id") + ], + detection_development=ExperimentalResourceDetection( + detectors=[ExperimentalResourceDetector(container={})] + ), + ) + with patch.dict( + "sys.modules", + {"opentelemetry.resource.detector.containerid": mock_module}, + ): + resource = create_resource(config) + + self.assertEqual(resource.attributes[CONTAINER_ID], "explicit-id")