diff --git a/.changelog/5660.fixed b/.changelog/5660.fixed new file mode 100644 index 00000000000..335362d6c13 --- /dev/null +++ b/.changelog/5660.fixed @@ -0,0 +1 @@ +`opentelemetry-sdk`: fix overriding of the service.instance.id which has been populated from the user provided values through the resource detectors diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index 8907d54191f..7c88f6145c5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -670,18 +670,17 @@ def _build_resource_detectors() -> list["ResourceDetector"]: Fast path: if no extra detectors are configured, returns only the two built-in detectors without scanning entry_points. - "service_instance" (ServiceInstanceIdResourceDetector) and "otel" - (OTELResourceDetector) are always appended as defaults. "otel" is last so + "service_instance" (ServiceInstanceIdResourceDetector) is prepended unless + it is explicitly configured. "otel" (OTELResourceDetector) is last so that OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME take highest merge - priority, but an explicit position in OTEL_EXPERIMENTAL_RESOURCE_DETECTORS - is respected for either name. + priority, but an explicit position in + OTEL_EXPERIMENTAL_RESOURCE_DETECTORS is respected for either name. """ - detector_names: list[str] = list( - dict.fromkeys( - [name.strip() for name in environ.get(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "").split(",") if name.strip()] - + ["service_instance", "otel"] - ) - ) + configured_detector_names = [ + name.strip() for name in environ.get(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "").split(",") if name.strip() + ] + default_detector_names = [] if "service_instance" in configured_detector_names else ["service_instance"] + detector_names: list[str] = list(dict.fromkeys(default_detector_names + configured_detector_names + ["otel"])) # Fast path: only the two built-in detectors — no entry_points scan needed. if detector_names == ["service_instance", "otel"]: diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 4fcf969bf76..063770854c5 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -11,6 +11,7 @@ import unittest import uuid from concurrent.futures import TimeoutError +from functools import partial from logging import ERROR, WARNING from os import environ from unittest.mock import MagicMock, Mock, call, mock_open, patch @@ -1247,6 +1248,72 @@ def tearDown(self) -> None: def test_is_process_dependent(self): self.assertTrue(ServiceInstanceIdResourceDetector().is_process_dependent()) + @patch.dict(environ, {}, clear=True) + def test_resource_attributes_override_service_instance_id(self): + resource = Resource.create({SERVICE_INSTANCE_ID: "resource-instance-id"}) + + self.assertEqual(resource.attributes[SERVICE_INSTANCE_ID], "resource-instance-id") + + @patch.dict( + environ, + {OTEL_RESOURCE_ATTRIBUTES: "service.instance.id=environment-instance-id"}, + clear=True, + ) + def test_environment_resource_attributes_override_service_instance_id(self): + resource = Resource.create() + + self.assertEqual(resource.attributes[SERVICE_INSTANCE_ID], "environment-instance-id") + + def test_service_instance_detector_ordering(self): + test_cases = ( + ("", False, True), + ("mock", True, False), + ("mock,service_instance", True, True), + ) + + def entry_points_side_effect(entry_point, *args, **kwargs): + if kwargs.get("name") == "mock": + return [entry_point] + return real_entry_points(*args, **kwargs) + + for detector_names, includes_custom_detector, expects_generated_id in test_cases: + with self.subTest(detector_names=detector_names): + custom_detector = Mock(spec=ResourceDetector) + custom_detector.detect.return_value = Resource( + { + SERVICE_INSTANCE_ID: "configured-instance-id", + "custom.detector": "value", + } + ) + entry_point = Mock(**{"load.return_value": Mock(return_value=custom_detector)}) + + with patch.dict( + environ, + {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: detector_names}, + clear=True, + ): + if includes_custom_detector: + with patch( + "opentelemetry.util._importlib_metadata.entry_points", + side_effect=partial(entry_points_side_effect, entry_point), + ): + resource = Resource.create() + custom_detector.detect.assert_called_once() + self.assertEqual(resource.attributes["custom.detector"], "value") + else: + resource = Resource.create() + + if expects_generated_id: + self.assertEqual( + uuid.UUID(resource.attributes[SERVICE_INSTANCE_ID]).version, + 4, + ) + else: + self.assertEqual( + resource.attributes[SERVICE_INSTANCE_ID], + "configured-instance-id", + ) + def test_detect_value_is_valid_uuid4(self): _resources_module._service_instance_id = None _resources_module._service_instance_id_pid = None