diff --git a/.changelog/5660.fixed b/.changelog/5660.fixed new file mode 100644 index 0000000000..335362d6c1 --- /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 36be319f71..1d9a8e62b8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -545,16 +545,20 @@ 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 - that OTEL_RESOURCE_ATTRIBUTES and OTEL_SERVICE_NAME take highest merge - priority, but an explicit position in OTEL_EXPERIMENTAL_RESOURCE_DETECTORS + "service_instance" (ServiceInstanceIdResourceDetector) is prepended. "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. """ 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"] + ["service_instance"] + + [ + name.strip() + for name in environ.get(OTEL_EXPERIMENTAL_RESOURCE_DETECTORS, "").split(",") + if name.strip() + ] + + ["otel"] ) ) diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index eec1cdcc93..2666351a50 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -1044,6 +1044,42 @@ def tearDown(self) -> None: def test_is_process_dependent(self): self.assertTrue(ServiceInstanceIdResourceDetector().is_process_dependent()) + @patch.dict(environ, {}, clear=True) + def test_service_instance_detector_sets_id_without_custom_detector(self): + resource = Resource.create() + + self.assertEqual(uuid.UUID(resource.attributes[SERVICE_INSTANCE_ID]).version, 4) + + @patch.dict( + environ, + {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "mock"}, + clear=True, + ) + def test_configured_detector_overrides_service_instance_id(self): + 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)}) + + def side_effect(*args, **kwargs): + if kwargs.get("name") == "mock": + return [entry_point] + return real_entry_points(*args, **kwargs) + + with patch( + "opentelemetry.util._importlib_metadata.entry_points", + side_effect=side_effect, + ): + resource = Resource.create() + + custom_detector.detect.assert_called_once() + self.assertEqual(resource.attributes[SERVICE_INSTANCE_ID], "configured-instance-id") + self.assertEqual(resource.attributes["custom.detector"], "value") + def test_detect_value_is_valid_uuid4(self): _resources_module._service_instance_id = None _resources_module._service_instance_id_pid = None