Skip to content

Commit c91747d

Browse files
authored
Fix OneSettings validation logic to match schema (#48059)
1 parent 3eeccbb commit c91747d

6 files changed

Lines changed: 245 additions & 273 deletions

File tree

sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
([#47949](https://github.com/Azure/azure-sdk-for-python/pull/47949))
2020
- Harden OneSettings configuration manager and worker: handle non-retryable HTTP errors by slow-polling instead of retrying, fix worker holding its lock across network I/O, make shutdown a soft reset that leaves the singleton reusable, and make callback registration thread-safe and initialization-independent
2121
([#48027](https://github.com/Azure/azure-sdk-for-python/pull/48027))
22+
- Align OneSettings feature-flag evaluation with the control-plane schema: use full-name `os`/`rp`/`attach` values, add `ikey` and `region` conditions, require exact single-value matches (removing list and version-range support), and only honor a `ver` condition when a matching `component` is also present <!-- cspell:ignore ikey -->
23+
([#48059](https://github.com/Azure/azure-sdk-for-python/pull/48059))
2224

2325
## 1.0.0b55 (2026-07-01)
2426

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_configuration/_utils.py

Lines changed: 39 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class _ConfigurationProfile:
2525
version: str = ""
2626
component: str = ""
2727
region: str = ""
28+
ikey: str = ""
2829

2930
@classmethod
3031
def fill(cls, **kwargs) -> None:
@@ -41,6 +42,8 @@ def fill(cls, **kwargs) -> None:
4142
cls.attach = kwargs["attach"]
4243
if "region" in kwargs and cls.region == "":
4344
cls.region = kwargs["region"]
45+
if "ikey" in kwargs and cls.ikey == "":
46+
cls.ikey = kwargs["ikey"]
4447

4548

4649
class OneSettingsResponse:
@@ -218,48 +221,34 @@ def evaluate_feature(feature_key: str, settings: Dict[str, Any]) -> Optional[boo
218221
219222
Example settings structure:
220223
{
221-
"live_metrics": {
224+
"FEATURE_LIVE_METRICS": {
222225
"default": "disabled", # Feature is disabled by default
223226
"override": [
224-
{"os": "w"}, # Enable on Windows (any version)
225-
{"os": "l", "ver": {"min": "1.0.0b20"}}, # Enable on Linux with version >= 1.0.0b20
226-
{"component": "ext", "rp": "f"} # Enable if component is exporter AND rp is functions
227+
{"os": "windows"}, # Enable on Windows (any version)
228+
# Enable on Linux at exact version 1.0.0b21 with the distro component
229+
{"os": "linux", "ver": "1.0.0b21", "component": "dst"},
230+
{"ikey": "12345678-1234-1234-1234-123456789abc"}, # Enable for a specific instrumentation key
231+
{"component": "dst"} # Enable if component is distro
227232
]
228233
},
229-
"sampling": {
234+
"FEATURE_SDK_STATS": {
230235
"default": "enabled", # Feature is enabled by default
231236
"override": [
232-
{"os": ["w", "l"]}, # Disable on Windows OR Linux
233-
{"ver": {"max": "1.0.0"}}, # Disable on versions <= 1.0.0
234-
# Disable if attach is integratedauto/manual AND region is eastus
235-
{"attach": ["i", "m"], "region": "eastus"}
236-
]
237-
},
238-
"profiling": {
239-
"default": "disabled",
240-
"override": [
241-
{"os": "w", "ver": {"min": "2.0.0", "max": "3.0.0"}}, # Enable on Windows with version 2.0.0-3.0.0
242-
# Enable if component is exporter AND rp is functions/appsvc AND region is westus/eastus
243-
{"component": "ext", "rp": ["f", "a"], "region": ["westus", "eastus"]}
244-
]
245-
},
246-
"debug_logging": {
247-
"default": "enabled",
248-
"override": [
249-
{"ver": "1.0.0b1"}, # Disable on exact version 1.0.0b1
250-
# Disable on Linux with distro component, manual attach, and AKS runtime
251-
{"os": "l", "component": "dst", "attach": "m", "rp": "k"}
237+
{"os": "linux"}, # Disable on Linux
238+
# Disable on Linux at exact version 1.0.0b20 with the exporter component
239+
{"os": "linux", "ver": "1.0.0b20", "component": "ext"}
252240
]
253241
}
254242
}
255243
256-
Available condition fields:
257-
- os: Operating system ("w"=windows, "l"=linux, "d"=darwin, "u"=unknown, etc.) - supports single value or list
258-
- ver: Version constraints - supports exact string match or dict with "min"/"max" keys
259-
- component: Component type ("ext"=exporter, "dst"=distro) - exact string match
260-
- rp: Runtime platform ("u"=unknown, "f"=functions, "a"=appsvc, "k"=aks) - supports single value or list
261-
- region: Host region ("westus", "eastus", etc.) - supports single value or list
262-
- attach: Attachment type ("m"=manual, "i"=integratedauto) - supports single value or list
244+
Available condition fields (each override rule is a single-value exact match per field):
245+
- os: Operating system ("windows", "linux", "darwin", "unknown")
246+
- ver: Exact version string match (e.g. "1.0.0b21"); when present, "component" is also required
247+
- rp: Resource provider ("appsvc", "fn", "aks", "unknown")
248+
- ikey: Instrumentation key (GUID format, case-insensitive)
249+
- component: Component type ("dst"=distro, "ext"=exporter, "mot"=msft distro)
250+
- attach: Attach type ("manual", "integratedauto")
251+
- region: Azure region (e.g. "eastus", "westeurope")
263252
264253
Override logic:
265254
- Each item in the override list is an independent rule
@@ -301,6 +290,11 @@ def _matches_override_rule(override_rule: Dict[str, Any]) -> bool:
301290
302291
All conditions within a single override rule must match for the rule to apply.
303292
293+
A version ("ver") condition is only honored when the rule also carries a "component"
294+
condition (per the OneSettings schema, "ver" requires "component"). A rule that specifies
295+
"ver" without "component" is treated as non-matching. Because "component" is a regular
296+
condition, it is still matched against the current profile like any other field.
297+
304298
:param override_rule: Dictionary of conditions that must all be true
305299
:type override_rule: Dict[str, Any]
306300
:return: True if all conditions in the rule match, False otherwise
@@ -310,6 +304,10 @@ def _matches_override_rule(override_rule: Dict[str, Any]) -> bool:
310304
if not override_rule:
311305
return False
312306

307+
# A "ver" condition requires a "component" condition to be present in the same rule.
308+
if "ver" in override_rule and "component" not in override_rule:
309+
return False
310+
313311
# All conditions in this rule must match
314312
for condition_key, condition_value in override_rule.items():
315313
if not _matches_condition(condition_key, condition_value):
@@ -338,129 +336,35 @@ def _matches_condition(condition_key: str, condition_value: Any) -> bool:
338336
return False
339337

340338
if condition_key == "os":
341-
# OS condition - check if current OS is in the list
342-
if isinstance(condition_value, list):
343-
return profile.os.lower() in [str(os).lower() for os in condition_value]
339+
# OS condition - exact match (case-insensitive)
344340
return profile.os.lower() == str(condition_value).lower()
345341

346342
if condition_key == "ver":
347-
# Version condition - support min/max version checks
348-
if isinstance(condition_value, dict):
349-
current_version = profile.version
350-
if not current_version:
351-
return False
352-
353-
# Check minimum version
354-
if "min" in condition_value:
355-
min_version = condition_value["min"]
356-
if not _compare_versions(current_version, str(min_version), ">="):
357-
return False
358-
359-
# Check maximum version
360-
if "max" in condition_value:
361-
max_version = condition_value["max"]
362-
if not _compare_versions(current_version, str(max_version), "<="):
363-
return False
364-
365-
return True
366-
# Exact version match
343+
# Version condition - exact match
367344
return profile.version == str(condition_value)
368345

369346
if condition_key == "component":
370347
# Component condition - exact match
371348
return profile.component == str(condition_value)
372349

373350
if condition_key == "rp":
374-
# Runtime platform condition - check if current RP is in the list
375-
if isinstance(condition_value, list):
376-
return profile.rp in [str(rp) for rp in condition_value]
351+
# Resource provider condition - exact match
377352
return profile.rp == str(condition_value)
378353

379354
if condition_key == "region":
380-
# Region condition - check if current region is in the list
381-
if isinstance(condition_value, list):
382-
return profile.region in [str(region) for region in condition_value]
355+
# Region condition - exact match
383356
return profile.region == str(condition_value)
384357

385358
if condition_key == "attach":
386-
# Attach type condition - check if current attach type is in the list
387-
if isinstance(condition_value, list):
388-
return profile.attach in [str(attach) for attach in condition_value]
359+
# Attach type condition - exact match
389360
return profile.attach == str(condition_value)
390361

362+
if condition_key == "ikey":
363+
# Instrumentation key condition - exact match, case-insensitive (GUIDs are hex)
364+
return profile.ikey.lower() == str(condition_value).lower()
365+
391366
# Unknown condition key
392367
return False
393368

394369

395-
def _compare_versions(version1: str, version2: str, operator: str) -> bool:
396-
"""Compare two version strings using the specified operator.
397-
398-
Handles standard semantic versioning with beta versions (e.g., "1.0.0b28").
399-
400-
:param version1: First version string (e.g., "2.9.1", "1.0.0b28")
401-
:type version1: str
402-
:param version2: Second version string (e.g., "2.9.0", "1.0.0b20")
403-
:type version2: str
404-
:param operator: Comparison operator (">=", "<=", "==", ">", "<")
405-
:type operator: str
406-
:return: True if the comparison is satisfied, False otherwise
407-
:rtype: bool
408-
"""
409-
try:
410-
# Parse version strings into comparable tuples
411-
v1_parts = _parse_version_with_beta(version1)
412-
v2_parts = _parse_version_with_beta(version2)
413-
414-
# Compare tuples
415-
if operator == ">=":
416-
return v1_parts >= v2_parts
417-
if operator == "<=":
418-
return v1_parts <= v2_parts
419-
if operator == "==":
420-
return v1_parts == v2_parts
421-
if operator == ">":
422-
return v1_parts > v2_parts
423-
if operator == "<":
424-
return v1_parts < v2_parts
425-
return False
426-
except (ValueError, AttributeError):
427-
# If version parsing fails, fall back to string comparison
428-
if operator == ">=":
429-
return version1 >= version2
430-
if operator == "<=":
431-
return version1 <= version2
432-
if operator == "==":
433-
return version1 == version2
434-
if operator == ">":
435-
return version1 > version2
436-
if operator == "<":
437-
return version1 < version2
438-
return False
439-
440-
441-
def _parse_version_with_beta(version: str) -> tuple:
442-
"""Parse a version string that may contain beta suffix into a comparable tuple.
443-
444-
Examples:
445-
- "1.0.0" -> (1, 0, 0, float('inf')) # Release version sorts after beta
446-
- "1.0.0b28" -> (1, 0, 0, 28) # Beta version with number
447-
- "2.1.5b1" -> (2, 1, 5, 1) # Beta version with number
448-
449-
:param version: Version string to parse
450-
:type version: str
451-
:return: Tuple representing version for comparison
452-
:rtype: tuple
453-
"""
454-
# Check if version contains beta suffix
455-
if "b" in version:
456-
# Split on 'b' to separate base version and beta number
457-
base_version, beta_part = version.split("b", 1)
458-
base_parts = [int(x) for x in base_version.split(".")]
459-
beta_number = int(beta_part) if beta_part.isdigit() else 0
460-
return tuple(base_parts + [beta_number])
461-
# Release version - use infinity for beta part so it sorts after beta versions
462-
base_parts = [int(x) for x in version.split(".")]
463-
return tuple(base_parts + [float("inf")])
464-
465-
466370
# cSpell:enable

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_utils.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,41 @@ def _get_attach_type():
117117
return attach_type
118118

119119

120+
# OneSettings
121+
122+
# cspell:ignore appsvc
123+
# The OneSettings feature-flag schema expects full names for os/rp/attach (windows/linux/darwin,
124+
# appsvc/fn/aks, manual/integratedauto), unlike the short single-letter codes used for the statsbeat
125+
# SDK version prefix. These helpers are used only for the OneSettings _ConfigurationProfile.
126+
127+
128+
def _get_os_name():
129+
system = platform.system()
130+
if system == "Linux":
131+
return "linux"
132+
if system == "Windows":
133+
return "windows"
134+
if system == "Darwin":
135+
return "darwin"
136+
return "unknown"
137+
138+
139+
def _get_rp_name():
140+
if _is_on_functions():
141+
return "fn"
142+
if _is_on_app_service():
143+
return "appsvc"
144+
if _is_on_aks():
145+
return "aks"
146+
return "unknown"
147+
148+
149+
def _get_attach_type_name():
150+
if _is_attach_enabled():
151+
return "integratedauto"
152+
return "manual"
153+
154+
120155
def _get_sdk_version_prefix():
121156
sdk_version_prefix = ""
122157
rp = _get_rp()

sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/export/_base.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,17 +196,22 @@ def __init__(self, **kwargs: Any) -> None:
196196
**kwargs,
197197
)
198198
# TODO: Uncomment configuration changes once testing is completed
199-
# from azure.monitor.opentelemetry.exporter._utils import _get_os, _get_rp, _get_attach_type
199+
# from azure.monitor.opentelemetry.exporter._utils import (
200+
# _get_os_name,
201+
# _get_rp_name,
202+
# _get_attach_type_name,
203+
# )
200204
# from azure.monitor.opentelemetry.exporter._version import VERSION as ext_version
201205

202206
# if self._configuration_manager:
203207
# self._configuration_manager.initialize(
204-
# os=_get_os(),
205-
# rp=_get_rp(),
206-
# attach=_get_attach_type(),
208+
# os=_get_os_name(),
209+
# rp=_get_rp_name(),
210+
# attach=_get_attach_type_name(),
207211
# component="ext",
208212
# version=ext_version,
209213
# region=self._region,
214+
# ikey=self._instrumentation_key,
210215
# )
211216
self.storage: Optional[LocalFileStorage] = None
212217
if not self._disable_offline_storage:

0 commit comments

Comments
 (0)