Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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 shared `_parse_headers` helper for declarative config OTLP exporters
([#5021](https://github.com/open-telemetry/opentelemetry-python/pull/5021))
- `opentelemetry-api`: Replace a broad exception in attribute cleaning tests to satisfy pylint in the `lint-opentelemetry-api` CI job
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