Skip to content

Commit 9cfdcce

Browse files
committed
add detection infrastructure foundations for resource detectors
Adds _run_detectors() stub and _filter_attributes() to create_resource(), providing the shared scaffolding for detector PRs to build on. Detectors are opt-in: nothing runs unless explicitly listed under detection_development.detectors in the config. The include/exclude attribute filter mirrors other SDK behaviour. Assisted-by: Claude Sonnet 4.6
1 parent 516aecc commit 9cfdcce

File tree

1 file changed

+74
-11
lines changed
  • opentelemetry-sdk/src/opentelemetry/sdk/_configuration

1 file changed

+74
-11
lines changed

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

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@
1414

1515
from __future__ import annotations
1616

17+
import fnmatch
1718
import logging
1819
from typing import Callable, Optional
1920
from urllib import parse
2021

2122
from opentelemetry.sdk._configuration.models import (
2223
AttributeNameValue,
2324
AttributeType,
25+
ExperimentalResourceDetector,
26+
IncludeExclude,
2427
)
2528
from opentelemetry.sdk._configuration.models import Resource as ResourceConfig
2629
from opentelemetry.sdk.resources import (
@@ -42,7 +45,7 @@ def _array(coerce: Callable) -> Callable:
4245
return lambda value: [coerce(item) for item in value]
4346

4447

45-
# Unified dispatch table for all attribute type coercions
48+
# Dispatch table mapping AttributeType to its coercion callable
4649
_COERCIONS = {
4750
AttributeType.string: str,
4851
AttributeType.int: int,
@@ -86,24 +89,25 @@ def _parse_attributes_list(attributes_list: str) -> dict[str, str]:
8689
def create_resource(config: Optional[ResourceConfig]) -> Resource:
8790
"""Create an SDK Resource from declarative config.
8891
89-
Does NOT read OTEL_RESOURCE_ATTRIBUTES or run any resource detectors.
90-
Starts from SDK telemetry defaults (telemetry.sdk.*) and merges config
91-
attributes on top, matching Java SDK behavior.
92+
Does NOT read OTEL_RESOURCE_ATTRIBUTES. Resource detectors are only run
93+
when explicitly listed under detection_development.detectors in the config.
94+
Starts from SDK telemetry defaults (telemetry.sdk.*), merges any detected
95+
attributes, then merges explicit config attributes on top (highest priority).
9296
9397
Args:
9498
config: Resource config from the parsed config file, or None.
9599
96100
Returns:
97-
A Resource with SDK defaults merged with any config-specified attributes.
101+
A Resource with SDK defaults, optional detector attributes, and any
102+
config-specified attributes merged in priority order.
98103
"""
99104
base = _DEFAULT_RESOURCE
100105

101106
if config is None:
102107
service_resource = Resource({SERVICE_NAME: "unknown_service"})
103108
return base.merge(service_resource)
104109

105-
# attributes_list is lower priority; process it first so that explicit
106-
# attributes can simply overwrite any conflicting keys.
110+
# attributes_list is lower priority; explicit attributes overwrite conflicts.
107111
config_attrs: dict[str, object] = {}
108112
if config.attributes_list:
109113
config_attrs.update(_parse_attributes_list(config.attributes_list))
@@ -112,13 +116,72 @@ def create_resource(config: Optional[ResourceConfig]) -> Resource:
112116
for attr in config.attributes:
113117
config_attrs[attr.name] = _coerce_attribute_value(attr)
114118

119+
# Spec requires service.name to always be present.
120+
if SERVICE_NAME not in config_attrs:
121+
config_attrs[SERVICE_NAME] = "unknown_service"
122+
115123
schema_url = config.schema_url
116124

125+
# Run detectors only if detection_development is configured. Collect all
126+
# detected attributes, apply the include/exclude filter, then merge before
127+
# config attributes so explicit values always win.
128+
result = base
129+
if config.detection_development:
130+
detected_attrs: dict[str, object] = {}
131+
if config.detection_development.detectors:
132+
for detector_config in config.detection_development.detectors:
133+
_run_detectors(detector_config, detected_attrs)
134+
135+
filtered = _filter_attributes(
136+
detected_attrs, config.detection_development.attributes
137+
)
138+
if filtered:
139+
result = result.merge(Resource(filtered)) # type: ignore[arg-type]
140+
117141
config_resource = Resource(config_attrs, schema_url) # type: ignore[arg-type]
118-
result = base.merge(config_resource)
142+
return result.merge(config_resource)
143+
144+
145+
def _run_detectors(
146+
detector_config: ExperimentalResourceDetector,
147+
detected_attrs: dict[str, object],
148+
) -> None:
149+
"""Run any detectors present in a single detector config entry.
150+
151+
Each detector PR adds its own branch here. The detected_attrs dict
152+
is updated in-place; later detectors overwrite earlier ones for the
153+
same key.
154+
"""
155+
119156

120-
# Add default service.name if not specified (matches Java SDK behavior)
121-
if not result.attributes.get(SERVICE_NAME):
122-
result = result.merge(Resource({SERVICE_NAME: "unknown_service"}))
157+
def _filter_attributes(
158+
attrs: dict[str, object], filter_config: Optional[IncludeExclude]
159+
) -> dict[str, object]:
160+
"""Filter detected attribute keys using include/exclude glob patterns.
123161
162+
Mirrors other SDK IncludeExcludePredicate.createPatternMatching behaviour:
163+
- No filter config (attributes absent) → include all detected attributes.
164+
- included patterns are checked first; excluded patterns are applied after.
165+
- An empty included list is treated as "include everything".
166+
"""
167+
if filter_config is None:
168+
return attrs
169+
170+
included = filter_config.included
171+
excluded = filter_config.excluded
172+
173+
if not included and not excluded:
174+
return attrs
175+
176+
effective_included = included if included else None # [] → include all
177+
178+
result: dict[str, object] = {}
179+
for key, value in attrs.items():
180+
if effective_included is not None and not any(
181+
fnmatch.fnmatch(key, pat) for pat in effective_included
182+
):
183+
continue
184+
if excluded and any(fnmatch.fnmatch(key, pat) for pat in excluded):
185+
continue
186+
result[key] = value
124187
return result

0 commit comments

Comments
 (0)