diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a35f9d7639..1832b008349 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 `host` resource detector support to declarative file configuration via `detection_development.detectors[].host` + ([#5002](https://github.com/open-telemetry/opentelemetry-python/pull/5002)) - `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/pyproject.toml b/opentelemetry-sdk/pyproject.toml index 343e1f63974..1bf362e27a8 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -74,7 +74,7 @@ console = "opentelemetry.sdk.trace.export:ConsoleSpanExporter" otel = "opentelemetry.sdk.resources:OTELResourceDetector" process = "opentelemetry.sdk.resources:ProcessResourceDetector" os = "opentelemetry.sdk.resources:OsResourceDetector" -host = "opentelemetry.sdk.resources:_HostResourceDetector" +host = "opentelemetry.sdk.resources:HostResourceDetector" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py index d58bd4d31de..3eea145a813 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py @@ -29,6 +29,7 @@ from opentelemetry.sdk.resources import ( _DEFAULT_RESOURCE, SERVICE_NAME, + HostResourceDetector, Resource, ) @@ -149,6 +150,8 @@ def _run_detectors( is updated in-place; later detectors overwrite earlier ones for the same key. """ + if detector_config.host is not None: + detected_attrs.update(HostResourceDetector().detect().attributes) def _filter_attributes( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index a04d27e9ab1..fb6acdd5c6e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -490,10 +490,8 @@ def detect(self) -> "Resource": ) -class _HostResourceDetector(ResourceDetector): # type: ignore[reportUnusedClass] - """ - The HostResourceDetector detects the hostname and architecture attributes. - """ +class HostResourceDetector(ResourceDetector): + """Detects host.name (hostname) and host.arch (CPU architecture).""" def detect(self) -> "Resource": return Resource( diff --git a/opentelemetry-sdk/tests/_configuration/test_resource.py b/opentelemetry-sdk/tests/_configuration/test_resource.py index b50bc03fff4..d0d34109ae9 100644 --- a/opentelemetry-sdk/tests/_configuration/test_resource.py +++ b/opentelemetry-sdk/tests/_configuration/test_resource.py @@ -13,6 +13,7 @@ # limitations under the License. import os +import socket import unittest from unittest.mock import patch @@ -20,9 +21,14 @@ from opentelemetry.sdk._configuration.models import ( AttributeNameValue, AttributeType, + ExperimentalResourceDetection, + ExperimentalResourceDetector, + IncludeExclude, ) from opentelemetry.sdk._configuration.models import Resource as ResourceConfig from opentelemetry.sdk.resources import ( + HOST_ARCH, + HOST_NAME, SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, @@ -295,3 +301,74 @@ 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 TestHostResourceDetector(unittest.TestCase): + @staticmethod + def _config_with_host() -> ResourceConfig: + return ResourceConfig( + detection_development=ExperimentalResourceDetection( + detectors=[ExperimentalResourceDetector(host={})] + ) + ) + + def test_host_detector_adds_host_attributes(self): + resource = create_resource(self._config_with_host()) + self.assertIn(HOST_NAME, resource.attributes) + self.assertEqual(resource.attributes[HOST_NAME], socket.gethostname()) + self.assertIn(HOST_ARCH, resource.attributes) + + def test_host_detector_also_includes_sdk_defaults(self): + resource = create_resource(self._config_with_host()) + self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python") + self.assertIn(TELEMETRY_SDK_VERSION, resource.attributes) + + def test_host_detector_not_run_when_absent(self): + resource = create_resource(ResourceConfig()) + self.assertNotIn(HOST_NAME, resource.attributes) + self.assertNotIn(HOST_ARCH, resource.attributes) + + def test_host_detector_not_run_when_detection_development_is_none(self): + resource = create_resource(ResourceConfig(detection_development=None)) + self.assertNotIn(HOST_NAME, resource.attributes) + + def test_host_detector_not_run_when_detectors_list_empty(self): + config = ResourceConfig( + detection_development=ExperimentalResourceDetection(detectors=[]) + ) + resource = create_resource(config) + self.assertNotIn(HOST_NAME, resource.attributes) + + def test_explicit_attributes_override_host_detector(self): + config = ResourceConfig( + attributes=[ + AttributeNameValue(name="host.name", value="custom-host") + ], + detection_development=ExperimentalResourceDetection( + detectors=[ExperimentalResourceDetector(host={})] + ), + ) + resource = create_resource(config) + self.assertEqual(resource.attributes[HOST_NAME], "custom-host") + + def test_included_filter_limits_host_attributes(self): + config = ResourceConfig( + detection_development=ExperimentalResourceDetection( + detectors=[ExperimentalResourceDetector(host={})], + attributes=IncludeExclude(included=["host.name"]), + ) + ) + resource = create_resource(config) + self.assertIn(HOST_NAME, resource.attributes) + self.assertNotIn(HOST_ARCH, resource.attributes) + + def test_excluded_filter_removes_host_attributes(self): + config = ResourceConfig( + detection_development=ExperimentalResourceDetection( + detectors=[ExperimentalResourceDetector(host={})], + attributes=IncludeExclude(excluded=["host.name"]), + ) + ) + resource = create_resource(config) + self.assertNotIn(HOST_NAME, resource.attributes) + self.assertIn(HOST_ARCH, resource.attributes) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index c083eff1460..c70e5eebfcf 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -49,12 +49,12 @@ TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, TELEMETRY_SDK_VERSION, + HostResourceDetector, OsResourceDetector, OTELResourceDetector, ProcessResourceDetector, Resource, ResourceDetector, - _HostResourceDetector, get_aggregated_resources, ) @@ -813,7 +813,7 @@ class TestHostResourceDetector(unittest.TestCase): @patch("platform.machine", lambda: "AMD64") def test_host_resource_detector(self): resource = get_aggregated_resources( - [_HostResourceDetector()], + [HostResourceDetector()], Resource({}), ) self.assertEqual(resource.attributes[HOST_NAME], "foo")