diff --git a/.changelog/5259.added b/.changelog/5259.added new file mode 100644 index 00000000000..463de58085d --- /dev/null +++ b/.changelog/5259.added @@ -0,0 +1 @@ +`opentelemetry-sdk`: add `ServiceInstanceIdResourceDetector` for populating `service.instance.id` diff --git a/opentelemetry-sdk/pyproject.toml b/opentelemetry-sdk/pyproject.toml index e367cf45a66..4970dcfab6c 100644 --- a/opentelemetry-sdk/pyproject.toml +++ b/opentelemetry-sdk/pyproject.toml @@ -74,6 +74,7 @@ otel = "opentelemetry.sdk.resources:OTELResourceDetector" process = "opentelemetry.sdk.resources:ProcessResourceDetector" os = "opentelemetry.sdk.resources:OsResourceDetector" host = "opentelemetry.sdk.resources:_HostResourceDetector" +service_instance = "opentelemetry.sdk.resources:ServiceInstanceIdResourceDetector" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-sdk" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py index c5593909033..b1d244fa1a5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py @@ -56,6 +56,7 @@ import socket import sys import typing +import uuid from collections.abc import Sequence from json import dumps from os import environ @@ -462,6 +463,21 @@ def detect(self) -> "Resource": ) +class ServiceInstanceIdResourceDetector(ResourceDetector): + """Detects service.instance.id as a random UUID v4. + + Per the OpenTelemetry specification, SDKs SHOULD generate a random v1/v4 + UUID for service.instance.id to uniquely identify each service instance. + """ + + def __init__(self, raise_on_error: bool = False) -> None: + super().__init__(raise_on_error) + self._instance_id = str(uuid.uuid4()) + + def detect(self) -> "Resource": + return Resource({SERVICE_INSTANCE_ID: self._instance_id}) + + def _build_resource_detectors() -> list["ResourceDetector"]: """Returns the ordered list of resource detectors to use for Resource.create. diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py index 8c6ff460d7d..9ff1ace698d 100644 --- a/opentelemetry-sdk/tests/resources/test_resources.py +++ b/opentelemetry-sdk/tests/resources/test_resources.py @@ -34,6 +34,7 @@ PROCESS_RUNTIME_DESCRIPTION, PROCESS_RUNTIME_NAME, PROCESS_RUNTIME_VERSION, + SERVICE_INSTANCE_ID, SERVICE_NAME, TELEMETRY_SDK_LANGUAGE, TELEMETRY_SDK_NAME, @@ -43,6 +44,7 @@ ProcessResourceDetector, Resource, ResourceDetector, + ServiceInstanceIdResourceDetector, _HostResourceDetector, get_aggregated_resources, ) @@ -912,3 +914,40 @@ def test_resource_detector_entry_points_tolerate_missing_detector(self): resource.attributes["telemetry.sdk.language"], "python" ) self.assertIn(HOST_NAME, resource.attributes) + + +class TestServiceInstanceIdResourceDetector(unittest.TestCase): + def test_detect_value_is_valid_uuid4(self): + detector = ServiceInstanceIdResourceDetector() + value = detector.detect().attributes[SERVICE_INSTANCE_ID] + parsed = uuid.UUID(value) + self.assertEqual(parsed.version, 4) + + def test_detect_stable_within_instance(self): + detector = ServiceInstanceIdResourceDetector() + id1 = detector.detect().attributes[SERVICE_INSTANCE_ID] + id2 = detector.detect().attributes[SERVICE_INSTANCE_ID] + self.assertEqual(id1, id2) + + def test_detect_unique_across_instances(self): + id1 = ( + ServiceInstanceIdResourceDetector() + .detect() + .attributes[SERVICE_INSTANCE_ID] + ) + id2 = ( + ServiceInstanceIdResourceDetector() + .detect() + .attributes[SERVICE_INSTANCE_ID] + ) + self.assertNotEqual(id1, id2) + + @patch.dict( + environ, + {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "service_instance"}, + clear=True, + ) + def test_resource_detector_entry_points_service_instance(self): + resource = Resource.create() + self.assertIn(SERVICE_INSTANCE_ID, resource.attributes) + uuid.UUID(resource.attributes[SERVICE_INSTANCE_ID])