Skip to content
1 change: 1 addition & 0 deletions .changelog/5660.fixed
Original file line number Diff line number Diff line change
@@ -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
19 changes: 9 additions & 10 deletions opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]:
Expand Down
67 changes: 67 additions & 0 deletions opentelemetry-sdk/tests/resources/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading