Skip to content
Merged
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 @@ -3,6 +3,8 @@
## 1.0.0b53 (Unreleased)

### Features Added
- Read for global feature sdkstats bitmap and add a 15 second delay timer
([#47031](https://github.com/Azure/azure-sdk-for-python/pull/47031))

### Breaking Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,8 @@ def _check_and_set_folder_permissions(self) -> bool:
# Unix
else:
open_flags = (
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW # cspell:disable-line
) # pylint: disable=no-member
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW # pylint: disable=no-member # cspell:disable-line
Comment thread
rads-1996 marked this conversation as resolved.
)
dir_fd = os.open(self._path, open_flags)
try:
dir_stat = os.fstat(dir_fd)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

logger = logging.getLogger(__name__)

_STATSBEAT_INITIAL_EXPORT_WARMUP_SECONDS = 15 # 15 second warmup delay


class StatsbeatConfig:
"""Configuration class for Statsbeat metrics collection."""
Expand Down Expand Up @@ -154,6 +156,7 @@ def __init__(self) -> None:
self._initialized: bool = False # type: ignore
self._metrics: Optional[_StatsbeatMetrics] = None # type: ignore
self._meter_provider: Optional[MeterProvider] = None # type: ignore
self._warmup_timer: Optional[threading.Timer] = None

# Set during first initialization, preserved in shutdown for potential re-initialization
self._config: Optional[StatsbeatConfig] = None # type: ignore
Expand Down Expand Up @@ -241,8 +244,8 @@ def _do_initialize(self, config: StatsbeatConfig) -> bool:
config.distro_version,
)

# Force initial flush and initialize non-initial metrics
self._meter_provider.force_flush()
# Schedule initial statsbeat flush after warmup delay to allow feature bits to settle.
self._schedule_initial_export_flush()
self._metrics.init_non_initial_metrics()

self._config = config
Expand All @@ -258,8 +261,28 @@ def _do_initialize(self, config: StatsbeatConfig) -> bool:
self._cleanup()
return False

def _schedule_initial_export_flush(self) -> None:
def _flush() -> None:
meter_provider = self._meter_provider
if not self._initialized or meter_provider is None:
return
try:
meter_provider.force_flush()
except Exception as e: # pylint: disable=broad-except
Comment thread
rads-1996 marked this conversation as resolved.
logger.warning( # pylint: disable=do-not-log-exceptions-if-not-debug
"Failed to force flush statsbeat after warmup: %s", e
)

timer = threading.Timer(_STATSBEAT_INITIAL_EXPORT_WARMUP_SECONDS, _flush)
timer.daemon = True
self._warmup_timer = timer
timer.start()

def _cleanup(self, shutdown_meter_provider: bool = True) -> None:
# Clean up resources with optional meter provider shutdown
if hasattr(self, "_warmup_timer") and self._warmup_timer:
self._warmup_timer.cancel()
self._warmup_timer = None
if shutdown_meter_provider and self._meter_provider:
try:
self._meter_provider.shutdown()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"LIVE_METRICS_FEATURE_SET": False,
Comment thread
rads-1996 marked this conversation as resolved.
"CUSTOMER_SDKSTATS_FEATURE_SET": False,
"BROWSER_SDK_LOADER_FEATURE_SET": False,
"FEATURE_ATTRIBUTE_BITS": 0,
}
_STATSBEAT_STATE_LOCK = threading.Lock()
_STATSBEAT_FAILURE_COUNT_THRESHOLD = 3
Expand Down Expand Up @@ -115,3 +116,12 @@ def get_statsbeat_browser_sdk_loader_feature_set(): # pylint: disable=name-too-
def set_statsbeat_browser_sdk_loader_feature_set(): # pylint: disable=name-too-long
with _STATSBEAT_STATE_LOCK:
_STATSBEAT_STATE["BROWSER_SDK_LOADER_FEATURE_SET"] = True


def get_statsbeat_feature_attribute_bits() -> int:
return int(_STATSBEAT_STATE["FEATURE_ATTRIBUTE_BITS"])


def set_statsbeat_feature_attribute_bits(feature_bits: int) -> None:
with _STATSBEAT_STATE_LOCK:
_STATSBEAT_STATE["FEATURE_ATTRIBUTE_BITS"] = int(feature_bits)
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from azure.monitor.opentelemetry.exporter.statsbeat._state import (
_REQUESTS_MAP_LOCK,
_REQUESTS_MAP,
get_statsbeat_feature_attribute_bits,
set_statsbeat_feature_attribute_bits,
get_statsbeat_live_metrics_feature_set,
get_statsbeat_custom_events_feature_set,
get_statsbeat_customer_sdkstats_feature_set,
Expand Down Expand Up @@ -133,7 +135,9 @@ def __init__(
_StatsbeatMetrics._COMMON_ATTRIBUTES["version"] = _get_version()

self._ikey = instrumentation_key
self._feature = _StatsbeatFeature.NONE
if _StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] is not None:
set_statsbeat_feature_attribute_bits(_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"])
self._feature = get_statsbeat_feature_attribute_bits()
if not disable_offline_storage:
self._feature |= _StatsbeatFeature.DISK_RETRY
if has_credential:
Expand Down Expand Up @@ -166,6 +170,7 @@ def __init__(

_StatsbeatMetrics._NETWORK_ATTRIBUTES["host"] = _shorten_host(endpoint)
_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] = self._feature
set_statsbeat_feature_attribute_bits(self._feature)
_StatsbeatMetrics._INSTRUMENTATION_ATTRIBUTES["feature"] = _utils.get_instrumentations()

self._vm_retry = True # True if we want to attempt to find if in VM
Expand Down Expand Up @@ -264,18 +269,29 @@ def _get_feature_metric(self, options: CallbackOptions) -> Iterable[Observation]
return observations
# Feature metric
# Check if any features were enabled during runtime
if _StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] is not None:
set_statsbeat_feature_attribute_bits(_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"])
feature_bits = get_statsbeat_feature_attribute_bits()
if feature_bits:
self._feature |= feature_bits
_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] = self._feature
set_statsbeat_feature_attribute_bits(self._feature)
if get_statsbeat_custom_events_feature_set():
self._feature |= _StatsbeatFeature.CUSTOM_EVENTS_EXTENSION
_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] = self._feature
set_statsbeat_feature_attribute_bits(self._feature)
if get_statsbeat_live_metrics_feature_set():
self._feature |= _StatsbeatFeature.LIVE_METRICS
_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] = self._feature
set_statsbeat_feature_attribute_bits(self._feature)
if get_statsbeat_customer_sdkstats_feature_set():
self._feature |= _StatsbeatFeature.CUSTOMER_SDKSTATS
_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] = self._feature
set_statsbeat_feature_attribute_bits(self._feature)
if get_statsbeat_browser_sdk_loader_feature_set():
self._feature |= _StatsbeatFeature.BROWSER_SDK_LOADER
_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] = self._feature
set_statsbeat_feature_attribute_bits(self._feature)

# Don't send observation if no features enabled
if self._feature is not _StatsbeatFeature.NONE:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ def setUp(self):
self.manager._metrics = None
if hasattr(self.manager, "_meter_provider"):
self.manager._meter_provider = None
if hasattr(self.manager, "_warmup_timer"):
self.manager._warmup_timer = None

def tearDown(self):
"""Clean up after tests."""
Expand Down Expand Up @@ -366,9 +368,16 @@ def test_initialize_invalid_config(self, mock_is_enabled):
@patch("azure.monitor.opentelemetry.exporter.statsbeat._manager.PeriodicExportingMetricReader")
@patch("azure.monitor.opentelemetry.exporter.export.metrics._exporter.AzureMonitorMetricExporter")
@patch("azure.monitor.opentelemetry.exporter.statsbeat._manager._StatsbeatMetrics")
@patch("azure.monitor.opentelemetry.exporter.statsbeat._manager.threading.Timer")
@patch("azure.monitor.opentelemetry.exporter.statsbeat._state.is_statsbeat_enabled")
def test_initialize_success(
self, mock_is_enabled, mock_statsbeat_metrics, mock_exporter_class, mock_reader_class, mock_meter_provider_class
self,
mock_is_enabled,
mock_timer_class,
mock_statsbeat_metrics,
mock_exporter_class,
mock_reader_class,
mock_meter_provider_class,
):
"""Test successful initialization."""
mock_is_enabled.return_value = True
Expand All @@ -388,6 +397,7 @@ def test_initialize_success(
# Mock the statsbeat metrics
mock_metrics = Mock()
mock_statsbeat_metrics.return_value = mock_metrics
mock_timer_class.return_value = Mock()

config = self._create_valid_config()

Expand All @@ -404,9 +414,47 @@ def test_initialize_success(
mock_reader_class.assert_called_once()
mock_meter_provider_class.assert_called_once()
mock_statsbeat_metrics.assert_called_once()
mock_meter_provider.force_flush.assert_called_once()
mock_timer_class.assert_called_once()
mock_timer_class.return_value.start.assert_called_once()
mock_meter_provider.force_flush.assert_not_called()
mock_metrics.init_non_initial_metrics.assert_called_once()

@patch("azure.monitor.opentelemetry.exporter.statsbeat._manager.threading.Timer")
@patch("azure.monitor.opentelemetry.exporter.statsbeat._manager.MeterProvider")
@patch("azure.monitor.opentelemetry.exporter.statsbeat._manager.PeriodicExportingMetricReader")
@patch("azure.monitor.opentelemetry.exporter.export.metrics._exporter.AzureMonitorMetricExporter")
@patch("azure.monitor.opentelemetry.exporter.statsbeat._manager._StatsbeatMetrics")
@patch("azure.monitor.opentelemetry.exporter.statsbeat._state.is_statsbeat_enabled")
def test_initialize_with_delay_schedules_non_blocking_flush(
self,
mock_is_enabled,
mock_statsbeat_metrics,
mock_exporter_class,
mock_reader_class,
mock_meter_provider_class,
mock_timer_class,
):
"""Test delayed initial statsbeat flush is scheduled asynchronously."""
mock_is_enabled.return_value = True

mock_exporter_class.return_value = Mock()
mock_reader_class.return_value = Mock()
mock_meter_provider = Mock()
mock_meter_provider_class.return_value = mock_meter_provider
mock_metrics = Mock()
mock_statsbeat_metrics.return_value = mock_metrics

mock_timer = Mock()
mock_timer_class.return_value = mock_timer

result = self.manager.initialize(self._create_valid_config())

self.assertTrue(result)
mock_timer_class.assert_called_once()
self.assertEqual(mock_timer_class.call_args[0][0], 15)
mock_timer.start.assert_called_once()
mock_meter_provider.force_flush.assert_not_called()

@patch("azure.monitor.opentelemetry.exporter.statsbeat._manager.MeterProvider")
@patch("azure.monitor.opentelemetry.exporter.statsbeat._manager.PeriodicExportingMetricReader")
@patch("azure.monitor.opentelemetry.exporter.export.metrics._exporter.AzureMonitorMetricExporter")
Expand Down Expand Up @@ -627,6 +675,8 @@ def test_cleanup_with_shutdown(self):
self.manager._initialized = True
mock_meter_provider = Mock()
self.manager._meter_provider = mock_meter_provider
mock_timer = Mock()
self.manager._warmup_timer = mock_timer
self.manager._metrics = Mock()
config_mock = Mock()
self.manager._config = config_mock
Expand All @@ -636,8 +686,10 @@ def test_cleanup_with_shutdown(self):
self.assertFalse(self.manager._initialized)
self.assertIsNone(self.manager._meter_provider)
self.assertIsNone(self.manager._metrics)
self.assertIsNone(self.manager._warmup_timer)
# Config is intact for potential re-initialization
self.assertEqual(self.manager._config, config_mock)
mock_timer.cancel.assert_called_once()
mock_meter_provider.shutdown.assert_called_once()

def test_cleanup_without_shutdown(self):
Expand All @@ -646,6 +698,8 @@ def test_cleanup_without_shutdown(self):
self.manager._initialized = True
mock_meter_provider = Mock()
self.manager._meter_provider = mock_meter_provider
mock_timer = Mock()
self.manager._warmup_timer = mock_timer
self.manager._metrics = Mock()
config_mock = Mock()
self.manager._config = config_mock
Expand All @@ -655,8 +709,10 @@ def test_cleanup_without_shutdown(self):
self.assertFalse(self.manager._initialized)
self.assertIsNone(self.manager._meter_provider)
self.assertIsNone(self.manager._metrics)
self.assertIsNone(self.manager._warmup_timer)
# Config is intact for potential re-initialization
self.assertEqual(self.manager._config, config_mock)
mock_timer.cancel.assert_called_once()
mock_meter_provider.shutdown.assert_not_called()

def test_cleanup_meter_provider_exception(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
_REQUESTS_MAP,
_STATSBEAT_STATE,
_STATSBEAT_STATE_LOCK,
get_statsbeat_feature_attribute_bits,
set_statsbeat_feature_attribute_bits,
)
from azure.monitor.opentelemetry.exporter.statsbeat._statsbeat_metrics import (
_shorten_host,
Expand Down Expand Up @@ -87,6 +89,7 @@ def setUp(self):
_STATSBEAT_STATE["CUSTOM_EVENTS_FEATURE_SET"] = False
_STATSBEAT_STATE["LIVE_METRICS_FEATURE_SET"] = False
_STATSBEAT_STATE["CUSTOMER_SDKSTATS_FEATURE_SET"] = False
_STATSBEAT_STATE["FEATURE_ATTRIBUTE_BITS"] = 0

_StatsbeatMetrics._COMMON_ATTRIBUTES = dict(_StatsbeatMetrics_COMMON_ATTRS)
_StatsbeatMetrics._NETWORK_ATTRIBUTES = dict(_StatsbeatMetrics_NETWORK_ATTRS)
Expand Down Expand Up @@ -126,6 +129,14 @@ def test_statsbeat_metric_init(self):
self.assertEqual(metric._attach_metric.name, _ATTACH_METRIC_NAME[0])
self.assertEqual(metric._feature_metric.name, _FEATURE_METRIC_NAME[0])

def test_statsbeat_feature_attribute_bits_getter_default(self):
self.assertEqual(get_statsbeat_feature_attribute_bits(), 0)

def test_statsbeat_feature_attribute_bits_setter_and_getter(self):
feature_bits = _StatsbeatFeature.DISK_RETRY | _StatsbeatFeature.LIVE_METRICS
set_statsbeat_feature_attribute_bits(feature_bits)
self.assertEqual(get_statsbeat_feature_attribute_bits(), feature_bits)

@mock.patch("azure.monitor.opentelemetry.exporter._utils._is_attach_enabled")
def test_statsbeat_metric_init_attach_enabled(self, attach_mock):
mp = MeterProvider()
Expand Down Expand Up @@ -716,6 +727,8 @@ def test_get_feature_metric_instrumentation(self):
False,
)
metric._feature = _StatsbeatFeature.NONE
_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] = _StatsbeatFeature.NONE
set_statsbeat_feature_attribute_bits(_StatsbeatFeature.NONE)
attributes = dict(_StatsbeatMetrics._COMMON_ATTRIBUTES)
attributes.update(_StatsbeatMetrics._INSTRUMENTATION_ATTRIBUTES)
self.assertEqual(attributes["type"], _FEATURE_TYPES.INSTRUMENTATION)
Expand Down Expand Up @@ -747,6 +760,8 @@ def test_get_feature_metric_instrumentation_none(self):
False,
)
metric._feature = _StatsbeatFeature.NONE
_StatsbeatMetrics._FEATURE_ATTRIBUTES["feature"] = _StatsbeatFeature.NONE
set_statsbeat_feature_attribute_bits(_StatsbeatFeature.NONE)
self.assertEqual(_StatsbeatMetrics._INSTRUMENTATION_ATTRIBUTES["feature"], 0)
with mock.patch("azure.monitor.opentelemetry.exporter._utils.get_instrumentations") as instrumentations:
instrumentations.return_value = 0
Expand Down
Loading