1414
1515from __future__ import annotations
1616
17+ import fnmatch
1718import logging
1819from typing import Callable , Optional
1920from urllib import parse
2021
2122from opentelemetry .sdk ._configuration .models import (
2223 AttributeNameValue ,
2324 AttributeType ,
25+ ExperimentalResourceDetector ,
26+ IncludeExclude ,
2427)
2528from opentelemetry .sdk ._configuration .models import Resource as ResourceConfig
2629from 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]:
8689def 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