Skip to content

Commit b62c43c

Browse files
committed
wire service resource detector in declarative config
Adds service detector support to _run_detectors(): sets a random UUID for service.instance.id and reads OTEL_SERVICE_NAME for service.name if set. Explicit config attributes still take priority (merged last). Assisted-by: Claude Sonnet 4.6
1 parent 367aa82 commit b62c43c

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import fnmatch
1818
import logging
19+
import os
20+
import uuid
1921
from typing import Callable, Optional
2022
from urllib import parse
2123

@@ -28,6 +30,8 @@
2830
from opentelemetry.sdk._configuration.models import Resource as ResourceConfig
2931
from opentelemetry.sdk.resources import (
3032
_DEFAULT_RESOURCE,
33+
OTEL_SERVICE_NAME,
34+
SERVICE_INSTANCE_ID,
3135
SERVICE_NAME,
3236
Resource,
3337
)
@@ -149,6 +153,14 @@ def _run_detectors(
149153
is updated in-place; later detectors overwrite earlier ones for the
150154
same key.
151155
"""
156+
if detector_config.service is not None:
157+
attrs: dict[str, object] = {
158+
SERVICE_INSTANCE_ID: str(uuid.uuid4()),
159+
}
160+
service_name = os.environ.get(OTEL_SERVICE_NAME)
161+
if service_name:
162+
attrs[SERVICE_NAME] = service_name
163+
detected_attrs.update(attrs)
152164

153165

154166
def _filter_attributes(

opentelemetry-sdk/tests/_configuration/test_resource.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@
2020
from opentelemetry.sdk._configuration.models import (
2121
AttributeNameValue,
2222
AttributeType,
23+
ExperimentalResourceDetection,
24+
ExperimentalResourceDetector,
25+
IncludeExclude,
2326
)
2427
from opentelemetry.sdk._configuration.models import Resource as ResourceConfig
2528
from opentelemetry.sdk.resources import (
29+
SERVICE_INSTANCE_ID,
2630
SERVICE_NAME,
2731
TELEMETRY_SDK_LANGUAGE,
2832
TELEMETRY_SDK_NAME,
@@ -295,3 +299,75 @@ def test_attributes_list_invalid_pair_skipped(self):
295299
self.assertEqual(resource.attributes["foo"], "bar")
296300
self.assertNotIn("no-equals", resource.attributes)
297301
self.assertTrue(any("no-equals" in msg for msg in cm.output))
302+
303+
304+
class TestServiceResourceDetector(unittest.TestCase):
305+
def _config_with_service(self) -> ResourceConfig:
306+
return ResourceConfig(
307+
detection_development=ExperimentalResourceDetection(
308+
detectors=[ExperimentalResourceDetector(service={})]
309+
)
310+
)
311+
312+
def test_service_detector_adds_instance_id(self):
313+
resource = create_resource(self._config_with_service())
314+
self.assertIn(SERVICE_INSTANCE_ID, resource.attributes)
315+
316+
def test_service_instance_id_is_unique_per_call(self):
317+
r1 = create_resource(self._config_with_service())
318+
r2 = create_resource(self._config_with_service())
319+
self.assertNotEqual(
320+
r1.attributes[SERVICE_INSTANCE_ID],
321+
r2.attributes[SERVICE_INSTANCE_ID],
322+
)
323+
324+
def test_service_detector_reads_otel_service_name_env_var(self):
325+
with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "my-service"}):
326+
resource = create_resource(self._config_with_service())
327+
self.assertEqual(resource.attributes[SERVICE_NAME], "my-service")
328+
329+
def test_service_detector_no_env_var_leaves_default_service_name(self):
330+
with patch.dict(os.environ, {}, clear=True):
331+
resource = create_resource(self._config_with_service())
332+
self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service")
333+
334+
def test_explicit_service_name_overrides_env_var(self):
335+
"""Config attributes win over the service detector's env-var value."""
336+
config = ResourceConfig(
337+
attributes=[
338+
AttributeNameValue(name="service.name", value="explicit-svc")
339+
],
340+
detection_development=ExperimentalResourceDetection(
341+
detectors=[ExperimentalResourceDetector(service={})]
342+
),
343+
)
344+
with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "env-svc"}):
345+
resource = create_resource(config)
346+
self.assertEqual(resource.attributes[SERVICE_NAME], "explicit-svc")
347+
348+
def test_service_detector_not_run_when_absent(self):
349+
resource = create_resource(ResourceConfig())
350+
self.assertNotIn(SERVICE_INSTANCE_ID, resource.attributes)
351+
352+
def test_service_detector_not_run_when_detection_development_is_none(self):
353+
resource = create_resource(ResourceConfig(detection_development=None))
354+
self.assertNotIn(SERVICE_INSTANCE_ID, resource.attributes)
355+
356+
def test_service_detector_also_includes_sdk_defaults(self):
357+
resource = create_resource(self._config_with_service())
358+
self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python")
359+
self.assertIn(TELEMETRY_SDK_VERSION, resource.attributes)
360+
361+
def test_included_filter_limits_service_attributes(self):
362+
config = ResourceConfig(
363+
detection_development=ExperimentalResourceDetection(
364+
detectors=[ExperimentalResourceDetector(service={})],
365+
attributes=IncludeExclude(included=["service.instance.id"]),
366+
)
367+
)
368+
with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "my-service"}):
369+
resource = create_resource(config)
370+
self.assertIn(SERVICE_INSTANCE_ID, resource.attributes)
371+
# service.name comes from the filter-excluded detector output, but the
372+
# default "unknown_service" is still added by create_resource directly
373+
self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service")

0 commit comments

Comments
 (0)