Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- `opentelemetry-sdk`: Add `service` resource detector support to declarative file configuration via `detection_development.detectors[].service`
([#5003](https://github.com/open-telemetry/opentelemetry-python/pull/5003))
- `opentelemetry-sdk`: Add `create_resource` and `create_propagator`/`configure_propagator` to declarative file configuration, enabling Resource and propagator instantiation from config files without reading env vars
([#4979](https://github.com/open-telemetry/opentelemetry-python/pull/4979))
- `opentelemetry-sdk`: Map Python `CRITICAL` log level to OTel `FATAL` severity text per the specification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import fnmatch
import logging
import os
import uuid
from typing import Callable, Optional
from urllib import parse

Expand All @@ -28,6 +30,8 @@
from opentelemetry.sdk._configuration.models import Resource as ResourceConfig
from opentelemetry.sdk.resources import (
_DEFAULT_RESOURCE,
OTEL_SERVICE_NAME,
SERVICE_INSTANCE_ID,
SERVICE_NAME,
Resource,
)
Expand Down Expand Up @@ -149,6 +153,14 @@ def _run_detectors(
is updated in-place; later detectors overwrite earlier ones for the
same key.
"""
if detector_config.service is not None:
attrs: dict[str, object] = {
SERVICE_INSTANCE_ID: str(uuid.uuid4()),
}
service_name = os.environ.get(OTEL_SERVICE_NAME)
if service_name:
attrs[SERVICE_NAME] = service_name
detected_attrs.update(attrs)


def _filter_attributes(
Expand Down
77 changes: 77 additions & 0 deletions opentelemetry-sdk/tests/_configuration/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@
from opentelemetry.sdk._configuration.models import (
AttributeNameValue,
AttributeType,
ExperimentalResourceDetection,
ExperimentalResourceDetector,
IncludeExclude,
)
from opentelemetry.sdk._configuration.models import Resource as ResourceConfig
from opentelemetry.sdk.resources import (
SERVICE_INSTANCE_ID,
SERVICE_NAME,
TELEMETRY_SDK_LANGUAGE,
TELEMETRY_SDK_NAME,
Expand Down Expand Up @@ -295,3 +299,76 @@ def test_attributes_list_invalid_pair_skipped(self):
self.assertEqual(resource.attributes["foo"], "bar")
self.assertNotIn("no-equals", resource.attributes)
self.assertTrue(any("no-equals" in msg for msg in cm.output))


class TestServiceResourceDetector(unittest.TestCase):
@staticmethod
def _config_with_service() -> ResourceConfig:
return ResourceConfig(
detection_development=ExperimentalResourceDetection(
detectors=[ExperimentalResourceDetector(service={})]
)
)

def test_service_detector_adds_instance_id(self):
resource = create_resource(self._config_with_service())
self.assertIn(SERVICE_INSTANCE_ID, resource.attributes)

def test_service_instance_id_is_unique_per_call(self):
r1 = create_resource(self._config_with_service())
r2 = create_resource(self._config_with_service())
self.assertNotEqual(
r1.attributes[SERVICE_INSTANCE_ID],
r2.attributes[SERVICE_INSTANCE_ID],
)

def test_service_detector_reads_otel_service_name_env_var(self):
with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "my-service"}):
resource = create_resource(self._config_with_service())
self.assertEqual(resource.attributes[SERVICE_NAME], "my-service")

def test_service_detector_no_env_var_leaves_default_service_name(self):
with patch.dict(os.environ, {}, clear=True):
resource = create_resource(self._config_with_service())
self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service")

def test_explicit_service_name_overrides_env_var(self):
"""Config attributes win over the service detector's env-var value."""
config = ResourceConfig(
attributes=[
AttributeNameValue(name="service.name", value="explicit-svc")
],
detection_development=ExperimentalResourceDetection(
detectors=[ExperimentalResourceDetector(service={})]
),
)
with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "env-svc"}):
resource = create_resource(config)
self.assertEqual(resource.attributes[SERVICE_NAME], "explicit-svc")

def test_service_detector_not_run_when_absent(self):
resource = create_resource(ResourceConfig())
self.assertNotIn(SERVICE_INSTANCE_ID, resource.attributes)

def test_service_detector_not_run_when_detection_development_is_none(self):
resource = create_resource(ResourceConfig(detection_development=None))
self.assertNotIn(SERVICE_INSTANCE_ID, resource.attributes)

def test_service_detector_also_includes_sdk_defaults(self):
resource = create_resource(self._config_with_service())
self.assertEqual(resource.attributes[TELEMETRY_SDK_LANGUAGE], "python")
self.assertIn(TELEMETRY_SDK_VERSION, resource.attributes)

def test_included_filter_limits_service_attributes(self):
config = ResourceConfig(
detection_development=ExperimentalResourceDetection(
detectors=[ExperimentalResourceDetector(service={})],
attributes=IncludeExclude(included=["service.instance.id"]),
)
)
with patch.dict(os.environ, {"OTEL_SERVICE_NAME": "my-service"}):
resource = create_resource(config)
self.assertIn(SERVICE_INSTANCE_ID, resource.attributes)
# service.name comes from the filter-excluded detector output, but the
# default "unknown_service" is still added by create_resource directly
self.assertEqual(resource.attributes[SERVICE_NAME], "unknown_service")
Loading