From 238454750c27eb2659f1abbedf788dba30a02f27 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 5 Jun 2026 09:14:23 -0700 Subject: [PATCH 01/11] Pass metrics observations to upstream SDKStats Manager --- .../_sdkstats/_network_metrics.py | 103 +++--------------- 1 file changed, 18 insertions(+), 85 deletions(-) diff --git a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py index 5d26950a..3323c956 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py @@ -4,20 +4,19 @@ # license information. # -------------------------------------------------------------------------- -"""Network SDKStats gauges. +"""Distro-owned network SDKStats observations. The upstream statsbeat metrics only count requests sent to the Breeze -endpoint (the Azure Monitor exporter's destination). Microsoft OpenTelemetry also -ships OTLP and Agent365 exporters, whose per-export success counters -live in its own ``_REQUESTS_MAP``. This module registers an -observable gauge on the upstream ``StatsbeatManager``'s ``MeterProvider`` -so those counters are exported on the same statsbeat pipeline. +endpoint. This distro also ships OTLP and Agent365 exporters whose +per-export counters live in the distro's own ``_REQUESTS_MAP``. We +contribute extra rows to the upstream ``Request_Success_Count`` metric +via ``add_metric_callback`` so the backend treats them as part of the +same metric stream (same name + InstrumentationScope). """ from __future__ import annotations import logging -import threading from typing import Iterable, List from opentelemetry.metrics import CallbackOptions, Observation @@ -33,6 +32,7 @@ ) try: + from azure.monitor.opentelemetry.exporter._constants import _REQ_SUCCESS_NAME from azure.monitor.opentelemetry.exporter.statsbeat._statsbeat_metrics import ( _StatsbeatMetrics, ) @@ -43,9 +43,6 @@ logger = logging.getLogger(__name__) -_REGISTER_LOCK = threading.Lock() -_registered = False - def _get_common_attributes() -> dict: if _StatsbeatMetrics is None: @@ -54,9 +51,8 @@ def _get_common_attributes() -> dict: def _observe_request_success_count(options: CallbackOptions) -> Iterable[Observation]: - """Drain the per-endpoint success counts and emit one observation each.""" + """Drain per-endpoint success counts and emit one observation each.""" common = _get_common_attributes() - observations: List[Observation] = [] for key, value in drain(REQUEST_SUCCESS_NAME).items(): attributes = dict(common) @@ -148,76 +144,13 @@ def _observe_request_exception_count(options: CallbackOptions) -> Iterable[Obser return observations -def register_network_gauges() -> bool: - """Attach our network-stats callbacks to upstream's gauges. - - We emit per-endpoint (``Request_Success_Count``, ``Request_Duration``, - ``Request_Failure_Count``, ``Retry_Count``, ``Throttle_Count``, - ``Exception_Count``) observations via the upstream statsbeat pipeline. We cannot - create separate gauges with the same names because the stats backend identifies - metric streams by InstrumentationScope, and rows from an unknown scope are silently - dropped. Instead we append our callbacks to the already-registered - upstream observable gauges so our observations are emitted on the - exact same instrument/scope as upstream's breeze rows. - - Idempotent — subsequent calls are no-ops. Returns ``True`` on the - call that performs registration, ``False`` if registration was - skipped (already registered, upstream unavailable, or upstream - hasn't created the gauges yet). - """ - global _registered # pylint: disable=global-statement - - with _REGISTER_LOCK: - if _registered: - return False - - try: - from azure.monitor.opentelemetry.exporter.statsbeat._manager import ( - StatsbeatManager, - ) - except ImportError: - logger.debug("Upstream statsbeat unavailable; skipping network gauges.") - return False - - manager = StatsbeatManager() - meter_provider = manager._meter_provider # pylint: disable=protected-access - metrics = manager._metrics # pylint: disable=protected-access - if meter_provider is None or metrics is None: - logger.debug("StatsbeatManager not initialised; skipping network gauges.") - return False - - attached: List[str] = [] - for gauge_attr, callback in ( - ("_success_count", _observe_request_success_count), - ("_average_duration", _observe_request_duration), - ("_failure_count", _observe_request_failure_count), - ("_retry_count", _observe_request_retry_count), - ("_throttle_count", _observe_request_throttle_count), - ("_exception_count", _observe_request_exception_count), - ): - gauge = getattr(metrics, gauge_attr, None) - if gauge is None: - logger.debug("Upstream %s gauge not yet created; skipping.", gauge_attr) - continue - try: - gauge._callbacks.append(callback) # pylint: disable=protected-access - except AttributeError: - logger.debug( - "Upstream %s gauge has no _callbacks list; cannot attach.", - gauge_attr, - ) - continue - attached.append(gauge_attr) - - if not attached: - return False - - _registered = True - return True - - -def _reset_for_tests() -> None: - """Reset the module-level registration guard. Test-only.""" - global _registered # pylint: disable=global-statement - with _REGISTER_LOCK: - _registered = False +def register_network_gauges(): + try: + from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager # type: ignore[import-not-found] + except ImportError: + logger.debug("Upstream statsbeat unavailable; skipping network gauges.") + return + manager = StatsbeatManager() + return manager.add_metric_callback(_REQ_SUCCESS_NAME[0], _observe_request_success_count) + + From 346045952eb75a298db2371b6433cc9405bcba6b Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Fri, 5 Jun 2026 10:00:05 -0700 Subject: [PATCH 02/11] Fix tests --- tests/test_sdkstats.py | 53 +++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/tests/test_sdkstats.py b/tests/test_sdkstats.py index 7969f79c..f5fced19 100644 --- a/tests/test_sdkstats.py +++ b/tests/test_sdkstats.py @@ -86,9 +86,21 @@ def _reset_upstream_singleton(): def _reset_network_metrics_guard(): - from microsoft.opentelemetry._sdkstats import _network_metrics + """Clear upstream's additional-callbacks store so tests can re-register.""" + try: + from azure.monitor.opentelemetry.exporter.statsbeat import _manager as _upstream_manager + except ImportError: + return - _network_metrics._reset_for_tests() + callbacks = getattr(_upstream_manager, "_ADDITIONAL_CALLBACKS", None) + lock = getattr(_upstream_manager, "_ADDITIONAL_CALLBACKS_LOCK", None) + if callbacks is None: + return + if lock is not None: + with lock: + callbacks.clear() + else: + callbacks.clear() class TestSdkStatsEnabled(unittest.TestCase): @@ -320,29 +332,44 @@ def tearDown(self): _reset_upstream_singleton() _reset_network_metrics_guard() - def test_returns_false_when_manager_has_no_meter_provider(self): + def test_returns_true_then_false_on_repeat(self): from microsoft.opentelemetry._sdkstats._network_metrics import ( register_network_gauges, ) + self.assertTrue(register_network_gauges()) self.assertFalse(register_network_gauges()) - def test_returns_true_then_false_on_repeat(self): - from azure.monitor.opentelemetry.exporter.statsbeat._manager import ( - StatsbeatManager, - ) - from microsoft.opentelemetry._sdkstats._config import ( - _build_default_sdkstats_config, + def test_registers_callback_under_request_success_metric_name(self): + from azure.monitor.opentelemetry.exporter._constants import _REQ_SUCCESS_NAME + from azure.monitor.opentelemetry.exporter.statsbeat import _manager as _upstream_manager + from microsoft.opentelemetry._sdkstats._network_metrics import ( + _observe_request_success_count, + register_network_gauges, ) + + register_network_gauges() + callbacks = _upstream_manager._ADDITIONAL_CALLBACKS.get(_REQ_SUCCESS_NAME[0], []) + self.assertIn(_observe_request_success_count, callbacks) + + def test_returns_none_when_upstream_unavailable(self): + # Simulate upstream import failure by patching the import in + # ``register_network_gauges``'s try block. + import builtins + from microsoft.opentelemetry._sdkstats._network_metrics import ( register_network_gauges, ) - config = _build_default_sdkstats_config() - self.assertTrue(StatsbeatManager().initialize(config)) + real_import = builtins.__import__ - self.assertTrue(register_network_gauges()) - self.assertFalse(register_network_gauges()) + def fake_import(name, *args, **kwargs): + if name == "azure.monitor.opentelemetry.exporter.statsbeat._manager": + raise ImportError("simulated") + return real_import(name, *args, **kwargs) + + with patch("builtins.__import__", side_effect=fake_import): + self.assertIsNone(register_network_gauges()) class TestObserveRequestSuccessCount(unittest.TestCase): From 80b57428e5326329c66a61dbc1a6c63b09617e82 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Thu, 11 Jun 2026 10:07:04 -0700 Subject: [PATCH 03/11] Add the other metrics --- .../_sdkstats/_network_metrics.py | 21 ++++++- tests/test_sdkstats.py | 58 +++++++++++-------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py index 3323c956..e5a094f2 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py @@ -32,7 +32,14 @@ ) try: - from azure.monitor.opentelemetry.exporter._constants import _REQ_SUCCESS_NAME + from azure.monitor.opentelemetry.exporter._constants import ( + _REQ_DURATION_NAME, + _REQ_EXCEPTION_NAME, + _REQ_FAILURE_NAME, + _REQ_RETRY_NAME, + _REQ_SUCCESS_NAME, + _REQ_THROTTLE_NAME, + ) from azure.monitor.opentelemetry.exporter.statsbeat._statsbeat_metrics import ( _StatsbeatMetrics, ) @@ -151,6 +158,16 @@ def register_network_gauges(): logger.debug("Upstream statsbeat unavailable; skipping network gauges.") return manager = StatsbeatManager() - return manager.add_metric_callback(_REQ_SUCCESS_NAME[0], _observe_request_success_count) + callbacks = ( + (_REQ_SUCCESS_NAME[0], _observe_request_success_count), + (_REQ_DURATION_NAME[0], _observe_request_duration), + (_REQ_FAILURE_NAME[0], _observe_request_failure_count), + (_REQ_RETRY_NAME[0], _observe_request_retry_count), + (_REQ_THROTTLE_NAME[0], _observe_request_throttle_count), + (_REQ_EXCEPTION_NAME[0], _observe_request_exception_count), + ) + return all( + manager.add_metric_callback(name, callback) for name, callback in callbacks + ) diff --git a/tests/test_sdkstats.py b/tests/test_sdkstats.py index f5fced19..a8e5949e 100644 --- a/tests/test_sdkstats.py +++ b/tests/test_sdkstats.py @@ -369,7 +369,7 @@ def fake_import(name, *args, **kwargs): return real_import(name, *args, **kwargs) with patch("builtins.__import__", side_effect=fake_import): - self.assertIsNone(register_network_gauges()) + self.assertFalse(register_network_gauges()) class TestObserveRequestSuccessCount(unittest.TestCase): @@ -673,6 +673,17 @@ def tearDown(self): _reset_network_metrics_guard() def test_attaches_to_all_six_gauges(self): + from azure.monitor.opentelemetry.exporter._constants import ( + _REQ_DURATION_NAME, + _REQ_EXCEPTION_NAME, + _REQ_FAILURE_NAME, + _REQ_RETRY_NAME, + _REQ_SUCCESS_NAME, + _REQ_THROTTLE_NAME, + ) + from azure.monitor.opentelemetry.exporter.statsbeat import ( + _manager as _upstream_manager, + ) from azure.monitor.opentelemetry.exporter.statsbeat._manager import ( StatsbeatManager, ) @@ -680,38 +691,35 @@ def test_attaches_to_all_six_gauges(self): _build_default_sdkstats_config, ) from microsoft.opentelemetry._sdkstats._network_metrics import ( + _observe_request_duration, + _observe_request_exception_count, + _observe_request_failure_count, + _observe_request_retry_count, + _observe_request_success_count, + _observe_request_throttle_count, register_network_gauges, ) config = _build_default_sdkstats_config() self.assertTrue(StatsbeatManager().initialize(config)) - # The six upstream gauges the distro must attach to. - gauge_attrs = [ - "_success_count", - "_average_duration", - "_failure_count", - "_retry_count", - "_throttle_count", - "_exception_count", - ] - - # Snapshot pre-registration callback counts so we can assert exactly - # one new callback per gauge. - metrics = StatsbeatManager()._metrics # pylint: disable=protected-access - assert metrics is not None - before = { - name: len(getattr(metrics, name)._callbacks) for name in gauge_attrs # pylint: disable=protected-access - } - self.assertTrue(register_network_gauges()) - for name in gauge_attrs: - after = len(getattr(metrics, name)._callbacks) # pylint: disable=protected-access - self.assertEqual( - after, - before[name] + 1, - f"Expected one distro callback appended to {name}", + expected = { + _REQ_SUCCESS_NAME[0]: _observe_request_success_count, + _REQ_DURATION_NAME[0]: _observe_request_duration, + _REQ_FAILURE_NAME[0]: _observe_request_failure_count, + _REQ_RETRY_NAME[0]: _observe_request_retry_count, + _REQ_THROTTLE_NAME[0]: _observe_request_throttle_count, + _REQ_EXCEPTION_NAME[0]: _observe_request_exception_count, + } + # pylint: disable=protected-access + for metric_name, callback in expected.items(): + callbacks = _upstream_manager._ADDITIONAL_CALLBACKS.get(metric_name, []) + self.assertIn( + callback, + callbacks, + f"Expected distro callback registered under {metric_name}", ) From 2a5a025ba30009423f9ea6e56e674fb981968ef4 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Mon, 15 Jun 2026 16:15:47 -0700 Subject: [PATCH 04/11] Address feedback --- src/microsoft/opentelemetry/_sdkstats/_network_metrics.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py index e5a094f2..c61f7a4f 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py @@ -166,8 +166,7 @@ def register_network_gauges(): (_REQ_THROTTLE_NAME[0], _observe_request_throttle_count), (_REQ_EXCEPTION_NAME[0], _observe_request_exception_count), ) - return all( - manager.add_metric_callback(name, callback) for name, callback in callbacks - ) + for name, callback in callbacks: + manager.add_metric_callback(name, callback) From b793e213e73495472db61d56a00bf8a8a4ae2fda Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Tue, 16 Jun 2026 14:24:30 -0700 Subject: [PATCH 05/11] Address feedback --- .../opentelemetry/_sdkstats/_network_metrics.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py index c61f7a4f..32ce31dc 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py @@ -158,15 +158,16 @@ def register_network_gauges(): logger.debug("Upstream statsbeat unavailable; skipping network gauges.") return manager = StatsbeatManager() - callbacks = ( + for metric, callback in ( (_REQ_SUCCESS_NAME[0], _observe_request_success_count), (_REQ_DURATION_NAME[0], _observe_request_duration), (_REQ_FAILURE_NAME[0], _observe_request_failure_count), (_REQ_RETRY_NAME[0], _observe_request_retry_count), (_REQ_THROTTLE_NAME[0], _observe_request_throttle_count), (_REQ_EXCEPTION_NAME[0], _observe_request_exception_count), - ) - for name, callback in callbacks: - manager.add_metric_callback(name, callback) + ): + callbacks = manager._additional_callbacks.setdefault(metric, []) + if callback not in callbacks: + callbacks.append(callback) From a97eefb46dca161eac52ccb24c8d9b0faf4dd751 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 17 Jun 2026 07:57:28 -0700 Subject: [PATCH 06/11] Fix docstring --- src/microsoft/opentelemetry/_sdkstats/_network_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py index 32ce31dc..af3b939e 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py @@ -58,7 +58,7 @@ def _get_common_attributes() -> dict: def _observe_request_success_count(options: CallbackOptions) -> Iterable[Observation]: - """Drain per-endpoint success counts and emit one observation each.""" + """Drain the per-endpoint success counts and emit one observation each.""" common = _get_common_attributes() observations: List[Observation] = [] for key, value in drain(REQUEST_SUCCESS_NAME).items(): From 81ffcf9d195a39e7de2ed18cb84833424319dc35 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 17 Jun 2026 14:41:38 -0700 Subject: [PATCH 07/11] Match upstream api --- src/microsoft/opentelemetry/_sdkstats/_network_metrics.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py index af3b939e..9e30ea32 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py @@ -166,8 +166,6 @@ def register_network_gauges(): (_REQ_THROTTLE_NAME[0], _observe_request_throttle_count), (_REQ_EXCEPTION_NAME[0], _observe_request_exception_count), ): - callbacks = manager._additional_callbacks.setdefault(metric, []) - if callback not in callbacks: - callbacks.append(callback) + manager.add_additional_metric_callback(metric, callback) From 9bd21906d5cb356b024fa8146a1182cf2b9649c3 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Wed, 17 Jun 2026 14:59:38 -0700 Subject: [PATCH 08/11] Fix tests --- tests/test_sdkstats.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/tests/test_sdkstats.py b/tests/test_sdkstats.py index a8e5949e..c19752e9 100644 --- a/tests/test_sdkstats.py +++ b/tests/test_sdkstats.py @@ -86,21 +86,19 @@ def _reset_upstream_singleton(): def _reset_network_metrics_guard(): - """Clear upstream's additional-callbacks store so tests can re-register.""" + """Clear the StatsbeatManager instance's additional-callbacks store so tests can re-register.""" try: - from azure.monitor.opentelemetry.exporter.statsbeat import _manager as _upstream_manager + from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager except ImportError: return - callbacks = getattr(_upstream_manager, "_ADDITIONAL_CALLBACKS", None) - lock = getattr(_upstream_manager, "_ADDITIONAL_CALLBACKS_LOCK", None) - if callbacks is None: - return - if lock is not None: - with lock: + try: + manager = StatsbeatManager() + callbacks = getattr(manager, "_additional_callbacks", None) + if callbacks is not None: callbacks.clear() - else: - callbacks.clear() + except Exception: # pylint: disable=broad-except + pass class TestSdkStatsEnabled(unittest.TestCase): @@ -332,24 +330,25 @@ def tearDown(self): _reset_upstream_singleton() _reset_network_metrics_guard() - def test_returns_true_then_false_on_repeat(self): + def test_can_be_called_multiple_times_without_error(self): from microsoft.opentelemetry._sdkstats._network_metrics import ( register_network_gauges, ) - self.assertTrue(register_network_gauges()) - self.assertFalse(register_network_gauges()) + # register_network_gauges returns None; calling it twice must not raise + register_network_gauges() + register_network_gauges() def test_registers_callback_under_request_success_metric_name(self): from azure.monitor.opentelemetry.exporter._constants import _REQ_SUCCESS_NAME - from azure.monitor.opentelemetry.exporter.statsbeat import _manager as _upstream_manager + from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager from microsoft.opentelemetry._sdkstats._network_metrics import ( _observe_request_success_count, register_network_gauges, ) register_network_gauges() - callbacks = _upstream_manager._ADDITIONAL_CALLBACKS.get(_REQ_SUCCESS_NAME[0], []) + callbacks = StatsbeatManager()._additional_callbacks.get(_REQ_SUCCESS_NAME[0], []) self.assertIn(_observe_request_success_count, callbacks) def test_returns_none_when_upstream_unavailable(self): @@ -681,9 +680,6 @@ def test_attaches_to_all_six_gauges(self): _REQ_SUCCESS_NAME, _REQ_THROTTLE_NAME, ) - from azure.monitor.opentelemetry.exporter.statsbeat import ( - _manager as _upstream_manager, - ) from azure.monitor.opentelemetry.exporter.statsbeat._manager import ( StatsbeatManager, ) @@ -703,8 +699,9 @@ def test_attaches_to_all_six_gauges(self): config = _build_default_sdkstats_config() self.assertTrue(StatsbeatManager().initialize(config)) - self.assertTrue(register_network_gauges()) + register_network_gauges() + manager = StatsbeatManager() expected = { _REQ_SUCCESS_NAME[0]: _observe_request_success_count, _REQ_DURATION_NAME[0]: _observe_request_duration, @@ -715,7 +712,7 @@ def test_attaches_to_all_six_gauges(self): } # pylint: disable=protected-access for metric_name, callback in expected.items(): - callbacks = _upstream_manager._ADDITIONAL_CALLBACKS.get(metric_name, []) + callbacks = manager._additional_callbacks.get(metric_name, []) self.assertIn( callback, callbacks, From eb3325885163a0223079d00ff5eaa15aee62f323 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Thu, 18 Jun 2026 08:32:50 -0700 Subject: [PATCH 09/11] Align with upstream modifications --- .../_sdkstats/_network_metrics.py | 2 +- tests/test_sdkstats.py | 30 +++++++++---------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py index 9e30ea32..2beeab78 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py @@ -166,6 +166,6 @@ def register_network_gauges(): (_REQ_THROTTLE_NAME[0], _observe_request_throttle_count), (_REQ_EXCEPTION_NAME[0], _observe_request_exception_count), ): - manager.add_additional_metric_callback(metric, callback) + manager.add_additional_metric_callbacks(metric, callback) diff --git a/tests/test_sdkstats.py b/tests/test_sdkstats.py index c19752e9..f9aa2af5 100644 --- a/tests/test_sdkstats.py +++ b/tests/test_sdkstats.py @@ -86,19 +86,8 @@ def _reset_upstream_singleton(): def _reset_network_metrics_guard(): - """Clear the StatsbeatManager instance's additional-callbacks store so tests can re-register.""" - try: - from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager - except ImportError: - return - - try: - manager = StatsbeatManager() - callbacks = getattr(manager, "_additional_callbacks", None) - if callbacks is not None: - callbacks.clear() - except Exception: # pylint: disable=broad-except - pass + """Reset callback registration by recreating the upstream singleton.""" + _reset_upstream_singleton() class TestSdkStatsEnabled(unittest.TestCase): @@ -339,6 +328,16 @@ def test_can_be_called_multiple_times_without_error(self): register_network_gauges() register_network_gauges() + def test_register_calls_additional_callback_api_for_each_metric(self): + from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager + from microsoft.opentelemetry._sdkstats._network_metrics import ( + register_network_gauges, + ) + + with patch.object(StatsbeatManager, "add_additional_metric_callbacks") as mock_add: + register_network_gauges() + self.assertEqual(mock_add.call_count, 6) + def test_registers_callback_under_request_success_metric_name(self): from azure.monitor.opentelemetry.exporter._constants import _REQ_SUCCESS_NAME from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager @@ -348,7 +347,7 @@ def test_registers_callback_under_request_success_metric_name(self): ) register_network_gauges() - callbacks = StatsbeatManager()._additional_callbacks.get(_REQ_SUCCESS_NAME[0], []) + callbacks = StatsbeatManager().get_additional_metric_callbacks(_REQ_SUCCESS_NAME[0]) self.assertIn(_observe_request_success_count, callbacks) def test_returns_none_when_upstream_unavailable(self): @@ -710,9 +709,8 @@ def test_attaches_to_all_six_gauges(self): _REQ_THROTTLE_NAME[0]: _observe_request_throttle_count, _REQ_EXCEPTION_NAME[0]: _observe_request_exception_count, } - # pylint: disable=protected-access for metric_name, callback in expected.items(): - callbacks = manager._additional_callbacks.get(metric_name, []) + callbacks = manager.get_additional_metric_callbacks(metric_name) self.assertIn( callback, callbacks, From 5935d2a86b1364cdd3c98a9e830ecabe6de5bd44 Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Thu, 18 Jun 2026 10:04:50 -0700 Subject: [PATCH 10/11] Fix lint and format --- src/microsoft/opentelemetry/_sdkstats/_network_metrics.py | 4 +--- tests/test_sdkstats.py | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py index 2beeab78..b74eda49 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py @@ -153,7 +153,7 @@ def _observe_request_exception_count(options: CallbackOptions) -> Iterable[Obser def register_network_gauges(): try: - from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager # type: ignore[import-not-found] + from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager # type: ignore[import-not-found] # pylint: disable=line-too-long except ImportError: logger.debug("Upstream statsbeat unavailable; skipping network gauges.") return @@ -167,5 +167,3 @@ def register_network_gauges(): (_REQ_EXCEPTION_NAME[0], _observe_request_exception_count), ): manager.add_additional_metric_callbacks(metric, callback) - - diff --git a/tests/test_sdkstats.py b/tests/test_sdkstats.py index f9aa2af5..92d7ab74 100644 --- a/tests/test_sdkstats.py +++ b/tests/test_sdkstats.py @@ -48,6 +48,7 @@ record_throttle, reset_all, ) + # Network sdkstats wrappers are temporarily disabled. See # microsoft/opentelemetry/_sdkstats/_otlp_wrapper.py. # from microsoft.opentelemetry._sdkstats._otlp_wrapper import ( From 8583e4faf5992bba31dd94aab9e50c945307c11c Mon Sep 17 00:00:00 2001 From: Radhika Gupta Date: Thu, 18 Jun 2026 10:22:17 -0700 Subject: [PATCH 11/11] Fix mypy --- src/microsoft/opentelemetry/_sdkstats/_network_metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py index b74eda49..b7eb7b1a 100644 --- a/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py +++ b/src/microsoft/opentelemetry/_sdkstats/_network_metrics.py @@ -151,7 +151,7 @@ def _observe_request_exception_count(options: CallbackOptions) -> Iterable[Obser return observations -def register_network_gauges(): +def register_network_gauges() -> None: try: from azure.monitor.opentelemetry.exporter.statsbeat._manager import StatsbeatManager # type: ignore[import-not-found] # pylint: disable=line-too-long except ImportError: