Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

### Other Changes

- Preserve the built-in Breeze SDKStats route when the OneSettings feature key is unavailable or invalid, while still applying OneSettings connection-string updates when present.

## 1.0.0b57 (2026-09-02)

### Features Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@

## ONE SETTINGS CONFIGS
_ONE_SETTINGS_DEFAULT_STATS_CONNECTION_STRING_KEY = "DEFAULT_STATS_CONNECTION_STRING"
_ONE_SETTINGS_DEFAULT_SDK_STATS_ENDPOINT_KEY = "DEFAULT_SDK_STATS_ENDPOINT"
_ONE_SETTINGS_SUPPORTED_DATA_BOUNDARIES_KEY = "SUPPORTED_DATA_BOUNDARIES"
_ONE_SETTINGS_FEATURE_LOCAL_STORAGE = "FEATURE_LOCAL_STORAGE"
_ONE_SETTINGS_FEATURE_LIVE_METRICS = "FEATURE_LIVE_METRICS"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,21 @@ def get_statsbeat_configuration_callback(settings: Dict[str, str]):

# Check if SDK stats should be enabled based on configuration
sdk_stats_enabled = evaluate_feature(_ONE_SETTINGS_FEATURE_SDK_STATS, settings)
if sdk_stats_enabled:
current_config = manager.get_current_config()
# Since config is preserved between shutdowns,
# It will only be None if never initialized
if not current_config:
return
# Get updated config from settings
updated_config = StatsbeatConfig.from_config(current_config, settings)
if updated_config:
manager.initialize(updated_config)
else:
# Disable statsbeat
if sdk_stats_enabled is False:
# Only an explicit OneSettings disable overrides the built-in enabled default.
manager.shutdown()
return

current_config = manager.get_current_config()
# Since config is preserved between shutdowns,
# It will only be None if never initialized
if not current_config:
return
# Get updated config from settings. Missing or invalid endpoint configuration falls back
# to the current built-in Breeze connection string in StatsbeatConfig.from_config.
updated_config = StatsbeatConfig.from_config(current_config, settings)
if updated_config:
manager.initialize(updated_config)


def shutdown_statsbeat_metrics() -> bool:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import os
import logging
import json
import re
from collections.abc import Iterable # pylint: disable=import-error
from typing import Optional, Dict, List
from urllib.parse import urlparse
from opentelemetry.metrics import CallbackOptions, Observation

from azure.monitor.opentelemetry.exporter._constants import (
Expand All @@ -19,8 +21,10 @@
_REQ_DURATION_NAME,
_REQ_SUCCESS_NAME,
_ONE_SETTINGS_DEFAULT_STATS_CONNECTION_STRING_KEY,
_ONE_SETTINGS_DEFAULT_SDK_STATS_ENDPOINT_KEY,
_ONE_SETTINGS_SUPPORTED_DATA_BOUNDARIES_KEY,
)
from azure.monitor.opentelemetry.exporter._connection_string_parser import ConnectionStringParser

from azure.monitor.opentelemetry.exporter.statsbeat._state import (
_REQUESTS_MAP,
Expand Down Expand Up @@ -108,13 +112,14 @@ def _get_connection_string_for_region_from_config(target_region: str, settings:
logger = logging.getLogger(__name__)

default_connection_string = settings.get(_ONE_SETTINGS_DEFAULT_STATS_CONNECTION_STRING_KEY)
default_endpoint = settings.get(_ONE_SETTINGS_DEFAULT_SDK_STATS_ENDPOINT_KEY)

try:
# Get supported data boundaries
supported_boundaries = settings.get(_ONE_SETTINGS_SUPPORTED_DATA_BOUNDARIES_KEY)
if not supported_boundaries:
logger.warning("Supported data boundaries key not found in configuration")
return default_connection_string
return _apply_sdk_stats_endpoint(default_connection_string, default_endpoint)

# Parse if it's a JSON string
if isinstance(supported_boundaries, str):
Expand All @@ -123,7 +128,7 @@ def _get_connection_string_for_region_from_config(target_region: str, settings:
# supported_boundaries should be a list
if not isinstance(supported_boundaries, Iterable):
logger.warning("Supported data boundaries is not iterable")
return default_connection_string
return _apply_sdk_stats_endpoint(default_connection_string, default_endpoint)

# Check each supported boundary to find the region
for boundary in supported_boundaries:
Expand All @@ -147,15 +152,16 @@ def _get_connection_string_for_region_from_config(target_region: str, settings:
connection_string = settings.get(connection_string_key)

if connection_string:
return connection_string
endpoint = settings.get(f"{boundary}_SDK_STATS_ENDPOINT") or default_endpoint
return _apply_sdk_stats_endpoint(connection_string, endpoint)

logger.warning("Connection string key '%s' not found in configuration", connection_string_key)

# Region not found in any specific boundary, try DEFAULT
if not default_connection_string:
logger.warning("Default stats connection string not found in configuration")
return None
return default_connection_string
return _apply_sdk_stats_endpoint(default_connection_string, default_endpoint)
except (ValueError, TypeError, KeyError) as ex:
logger.warning( # pylint: disable=do-not-log-exceptions-if-not-debug
"Error parsing configuration for region '%s': %s", target_region, str(ex)
Expand All @@ -168,6 +174,29 @@ def _get_connection_string_for_region_from_config(target_region: str, settings:
return None


def _apply_sdk_stats_endpoint(connection_string: Optional[str], endpoint: Optional[str]) -> Optional[str]:
if not connection_string or not endpoint:
return connection_string

try:
parsed_endpoint = urlparse(endpoint)
if parsed_endpoint.scheme.lower() != "https" or not parsed_endpoint.netloc:
return connection_string
ConnectionStringParser(connection_string)
except ValueError:
return connection_string

normalized_endpoint = endpoint.rstrip("/") + "/"
if re.search(r"(?:^|;)IngestionEndpoint=", connection_string, re.IGNORECASE):
return re.sub(
r"(?i)(IngestionEndpoint=)[^;]*",
rf"\g<1>{normalized_endpoint}",
connection_string,
count=1,
)
return f"{connection_string.rstrip(';')};IngestionEndpoint={normalized_endpoint}"


def _get_additional_observations(metric_name: str, options: CallbackOptions) -> List[Observation]:
"""Return observations contributed by extra callbacks registered on :class:`StatsbeatManager`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from azure.monitor.opentelemetry.exporter.statsbeat import StatsbeatConfig, _statsbeat
from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager
from azure.monitor.opentelemetry.exporter.statsbeat._utils import _get_connection_string_for_region_from_config
from azure.monitor.opentelemetry.exporter.statsbeat._statsbeat_metrics import _StatsbeatFeature, _StatsbeatMetrics
from azure.monitor.opentelemetry.exporter.statsbeat._state import (
_STATSBEAT_STATE,
Expand Down Expand Up @@ -56,6 +57,39 @@ def tearDown(self):
if StatsbeatManager in StatsbeatManager._instances:
del StatsbeatManager._instances[StatsbeatManager]

def test_one_settings_endpoint_overrides_connection_string_endpoint(self):
settings = {
"SUPPORTED_DATA_BOUNDARIES": '["EU"]',
"EU_REGIONS": '["westeurope"]',
"EU_STATS_CONNECTION_STRING": (
"InstrumentationKey=11111111-1111-1111-1111-111111111111;"
"IngestionEndpoint=https://eu.stats.example.com/"
),
"EU_SDK_STATS_ENDPOINT": "https://eu.collector.example.com/",
}

result = _get_connection_string_for_region_from_config("westeurope", settings)

self.assertEqual(
result,
"InstrumentationKey=11111111-1111-1111-1111-111111111111;"
"IngestionEndpoint=https://eu.collector.example.com/",
)

def test_invalid_one_settings_endpoint_preserves_connection_string_endpoint(self):
connection_string = (
"InstrumentationKey=11111111-1111-1111-1111-111111111111;"
"IngestionEndpoint=https://default.stats.example.com/"
)
settings = {
"DEFAULT_STATS_CONNECTION_STRING": connection_string,
"DEFAULT_SDK_STATS_ENDPOINT": "not-a-url",
}

result = _get_connection_string_for_region_from_config("westus", settings)

self.assertEqual(result, connection_string)

@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat._manager._StatsbeatMetrics")
@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat._manager.MeterProvider")
@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat._manager.PeriodicExportingMetricReader")
Expand Down Expand Up @@ -250,6 +284,33 @@ def test_get_statsbeat_configuration_callback_disable_sdkstats(
mock_manager_instance.initialize.assert_not_called()
mock_manager_instance.shutdown.assert_called_once()

@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat._statsbeat.evaluate_feature")
@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat._statsbeat.get_statsbeat_manager")
@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat._statsbeat.StatsbeatConfig")
def test_get_statsbeat_configuration_callback_missing_feature_uses_default(
self, mock_statsbeat_config_cls, mock_get_manager, mock_evaluate_feature
):
"""Test that missing feature configuration preserves SDKStats and applies endpoint settings."""
mock_manager_instance = mock.Mock()
mock_get_manager.return_value = mock_manager_instance
current_config = mock.Mock()
mock_manager_instance.get_current_config.return_value = current_config
updated_config = mock.Mock()
mock_statsbeat_config_cls.from_config.return_value = updated_config
mock_evaluate_feature.return_value = None
settings = {
"DEFAULT_STATS_CONNECTION_STRING": (
"InstrumentationKey=4321abcd-5678-4efa-8abc-1234567890ab;"
"IngestionEndpoint=https://stats.example.com/"
)
}

_statsbeat.get_statsbeat_configuration_callback(settings)

mock_statsbeat_config_cls.from_config.assert_called_once_with(current_config, settings)
mock_manager_instance.initialize.assert_called_once_with(updated_config)
mock_manager_instance.shutdown.assert_not_called()

@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat._statsbeat.evaluate_feature")
@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat._statsbeat.get_statsbeat_manager")
@mock.patch("azure.monitor.opentelemetry.exporter.statsbeat._statsbeat.StatsbeatConfig")
Expand Down