Skip to content

Commit ee2bc78

Browse files
Merge branch 'main' into reinit_metrics_on_fork
2 parents 719b959 + 9c270da commit ee2bc78

6 files changed

Lines changed: 97 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
- `opentelemetry-sdk`: Add fork-safety to metrics `SynchronousMeasurementConsumer` by registering a post-fork child hook, lazily reinitializing metric reader storages on first use in the child process, and clearing asynchronous instruments to avoid duplicated state after `fork()`
1616
([#4767](https://github.com/open-telemetry/opentelemetry-python/pull/4767))
17+
- `opentelemetry-sdk`: Add `service` resource detector support to declarative file configuration via `detection_development.detectors[].service`
18+
([#5003](https://github.com/open-telemetry/opentelemetry-python/pull/5003))
1719

1820
## Version 1.41.0/0.62b0 (2026-04-09)
1921

eachdist.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ packages=
3535
opentelemetry-exporter-opencensus
3636
opentelemetry-exporter-prometheus
3737
opentelemetry-distro
38+
opentelemetry-proto-json
3839
opentelemetry-semantic-conventions
3940
opentelemetry-test-utils
4041
tests

opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def tracer_configurator(tracer_scope):
8080
for i in range(num_tracer_configurator_rules)
8181
],
8282
default_config=_TracerConfig(is_enabled=True),
83-
)(tracer_scope=tracer_scope)
83+
)(tracer_scope)
8484

8585
tracer_provider._set_tracer_configurator(
8686
tracer_configurator=tracer_configurator

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

Lines changed: 13 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
ProcessResourceDetector,
3337
Resource,
@@ -152,6 +156,15 @@ def _run_detectors(
152156
is updated in-place; later detectors overwrite earlier ones for the
153157
same key.
154158
"""
159+
if detector_config.service is not None:
160+
attrs: dict[str, object] = {
161+
SERVICE_INSTANCE_ID: str(uuid.uuid4()),
162+
}
163+
service_name = os.environ.get(OTEL_SERVICE_NAME)
164+
if service_name:
165+
attrs[SERVICE_NAME] = service_name
166+
detected_attrs.update(attrs)
167+
155168
if detector_config.host is not None:
156169
detected_attrs.update(_HostResourceDetector().detect().attributes)
157170

opentelemetry-sdk/tests/_configuration/test_resource.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
HOST_NAME,
3434
PROCESS_PID,
3535
PROCESS_RUNTIME_NAME,
36+
SERVICE_INSTANCE_ID,
3637
SERVICE_NAME,
3738
TELEMETRY_SDK_LANGUAGE,
3839
TELEMETRY_SDK_NAME,
@@ -307,6 +308,79 @@ def test_attributes_list_invalid_pair_skipped(self):
307308
self.assertTrue(any("no-equals" in msg for msg in cm.output))
308309

309310

311+
class TestServiceResourceDetector(unittest.TestCase):
312+
@staticmethod
313+
def _config_with_service() -> ResourceConfig:
314+
return ResourceConfig(
315+
detection_development=ExperimentalResourceDetection(
316+
detectors=[ExperimentalResourceDetector(service={})]
317+
)
318+
)
319+
320+
def test_service_detector_adds_instance_id(self):
321+
resource = create_resource(self._config_with_service())
322+
self.assertIn(SERVICE_INSTANCE_ID, resource.attributes)
323+
324+
def test_service_instance_id_is_unique_per_call(self):
325+
r1 = create_resource(self._config_with_service())
326+
r2 = create_resource(self._config_with_service())
327+
self.assertNotEqual(
328+
r1.attributes[SERVICE_INSTANCE_ID],
329+
r2.attributes[SERVICE_INSTANCE_ID],
330+
)
331+
332+
def test_service_detector_reads_otel_service_name_env_var(self):
333+
with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "my-service"}):
334+
resource = create_resource(self._config_with_service())
335+
self.assertEqual(resource.attributes[SERVICE_NAME], "my-service")
336+
337+
def test_service_detector_no_env_var_leaves_default_service_name(self):
338+
with patch.dict(os.environ, {}, clear=True):
339+
resource = create_resource(self._config_with_service())
340+
self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service")
341+
342+
def test_explicit_service_name_overrides_env_var(self):
343+
"""Config attributes win over the service detector's env-var value."""
344+
config = ResourceConfig(
345+
attributes=[
346+
AttributeNameValue(name="service.name", value="explicit-svc")
347+
],
348+
detection_development=ExperimentalResourceDetection(
349+
detectors=[ExperimentalResourceDetector(service={})]
350+
),
351+
)
352+
with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "env-svc"}):
353+
resource = create_resource(config)
354+
self.assertEqual(resource.attributes[SERVICE_NAME], "explicit-svc")
355+
356+
def test_service_detector_not_run_when_absent(self):
357+
resource = create_resource(ResourceConfig())
358+
self.assertNotIn(SERVICE_INSTANCE_ID, resource.attributes)
359+
360+
def test_service_detector_not_run_when_detection_development_is_none(self):
361+
resource = create_resource(ResourceConfig(detection_development=None))
362+
self.assertNotIn(SERVICE_INSTANCE_ID, resource.attributes)
363+
364+
def test_service_detector_also_includes_sdk_defaults(self):
365+
resource = create_resource(self._config_with_service())
366+
self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python")
367+
self.assertIn(TELEMETRY_SDK_VERSION, resource.attributes)
368+
369+
def test_included_filter_limits_service_attributes(self):
370+
config = ResourceConfig(
371+
detection_development=ExperimentalResourceDetection(
372+
detectors=[ExperimentalResourceDetector(service={})],
373+
attributes=IncludeExclude(included=["service.instance.id"]),
374+
)
375+
)
376+
with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "my-service"}):
377+
resource = create_resource(config)
378+
self.assertIn(SERVICE_INSTANCE_ID, resource.attributes)
379+
# service.name comes from the filter-excluded detector output, but the
380+
# default "unknown_service" is still added by create_resource directly
381+
self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service")
382+
383+
310384
class TestHostResourceDetector(unittest.TestCase):
311385
@staticmethod
312386
def _config_with_host() -> ResourceConfig:

opentelemetry-sdk/tests/trace/test_trace.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,7 @@ def test_parent_child_span_exception(self):
22312231
exception_type = exception.__class__.__name__
22322232
exception_message = exception.args[0]
22332233

2234+
child_span = None
22342235
try:
22352236
with tracer.start_as_current_span(
22362237
"parent",
@@ -2245,6 +2246,8 @@ def test_parent_child_span_exception(self):
22452246
except Exception: # pylint: disable=broad-exception-caught
22462247
pass
22472248

2249+
self.assertIsNotNone(child_span)
2250+
22482251
self.assertTrue(child_span.status.is_ok)
22492252
self.assertIsNone(child_span.status.description)
22502253
self.assertTupleEqual(child_span.events, ())
@@ -2275,6 +2278,7 @@ def test_child_parent_span_exception(self):
22752278

22762279
exception = Exception("exception")
22772280

2281+
child_span = None
22782282
try:
22792283
with tracer.start_as_current_span(
22802284
"parent",
@@ -2290,6 +2294,8 @@ def test_child_parent_span_exception(self):
22902294
except Exception: # pylint: disable=broad-exception-caught
22912295
pass
22922296

2297+
self.assertIsNotNone(child_span)
2298+
22932299
self.assertTrue(child_span.status.is_ok)
22942300
self.assertIsNone(child_span.status.description)
22952301
self.assertTupleEqual(child_span.events, ())

0 commit comments

Comments
 (0)