From d9944a91540c47a5ada4f5dedaf123ed87460f9f Mon Sep 17 00:00:00 2001 From: tbequinor Date: Fri, 20 Feb 2026 10:07:18 +0100 Subject: [PATCH 1/2] Use lazy loading to fix psutil.AccessDenied on import --- .../_performance_counters/_manager.py | 48 +++++++++++++++---- .../test_performance_counters.py | 34 +++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_performance_counters/_manager.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_performance_counters/_manager.py index ccbf5fe3c957..6d4acd745ead 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_performance_counters/_manager.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/azure/monitor/opentelemetry/exporter/_performance_counters/_manager.py @@ -48,10 +48,8 @@ # _PROCESS.io_counters() is not available on Mac OS and some Linux distros. _IO_AVAILABLE = hasattr(_PROCESS, "io_counters") _IO_LAST_COUNT = 0 -if _IO_AVAILABLE: - _io_counters_initial = _PROCESS.io_counters() - _IO_LAST_COUNT = _io_counters_initial.read_bytes + _io_counters_initial.write_bytes _IO_LAST_TIME = datetime.now() +_IO_INITIALIZED = False # Processor Time % _LAST_CPU_TIMES = psutil.cpu_times() # Request Rate @@ -168,25 +166,55 @@ def _get_process_io(options: CallbackOptions) -> Iterable[Observation]: :rtype: ~typing.Iterable[~opentelemetry.metrics.Observation] """ try: - if not _IO_AVAILABLE: - yield Observation(0, {}) - return # pylint: disable=global-statement global _IO_LAST_COUNT # pylint: disable=global-statement global _IO_LAST_TIME - # RSS is non-swapped physical memory a process has used - io_counters = _PROCESS.io_counters() - rw_count = io_counters.read_bytes + io_counters.write_bytes + # pylint: disable=global-statement + global _IO_INITIALIZED + # pylint: disable=global-statement + global _IO_AVAILABLE + + # Lazily initialize counters to avoid import-time failures in restricted environments. + if not _IO_INITIALIZED: + _IO_INITIALIZED = True + if _IO_AVAILABLE: + io_counters_func = getattr(_PROCESS, "io_counters", None) + if not callable(io_counters_func): + _IO_AVAILABLE = False + yield Observation(0, {}) + return + io_counters_initial = io_counters_func() + _IO_LAST_COUNT = int(getattr(io_counters_initial, "read_bytes", 0)) + int( + getattr(io_counters_initial, "write_bytes", 0) + ) + _IO_LAST_TIME = datetime.now() + + if not _IO_AVAILABLE: + yield Observation(0, {}) + return + + io_counters_func = getattr(_PROCESS, "io_counters", None) + if not callable(io_counters_func): + _IO_AVAILABLE = False + yield Observation(0, {}) + return + + io_counters = io_counters_func() + rw_count = int(getattr(io_counters, "read_bytes", 0)) + int(getattr(io_counters, "write_bytes", 0)) rw_diff = rw_count - _IO_LAST_COUNT _IO_LAST_COUNT = rw_count current_time = datetime.now() elapsed_time_s = (current_time - _IO_LAST_TIME).total_seconds() _IO_LAST_TIME = current_time + if elapsed_time_s <= 0: + yield Observation(0, {}) + return io_rate = rw_diff / elapsed_time_s yield Observation(io_rate, {}) except (psutil.NoSuchProcess, psutil.AccessDenied, Exception) as e: # pylint: disable=broad-except - _logger.exception("Error getting process I/O rate: %s", e) + _IO_AVAILABLE = False + _logger.debug("Disabling process I/O counter due to inaccessible io_counters: %s", e) yield Observation(0, {}) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/performance_counters/test_performance_counters.py b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/performance_counters/test_performance_counters.py index 3b7b979a23c5..0950044bd93e 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/performance_counters/test_performance_counters.py +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/tests/performance_counters/test_performance_counters.py @@ -57,6 +57,7 @@ def setUp(self): manager_module._IO_AVAILABLE = True manager_module._IO_LAST_COUNT = 0 manager_module._IO_LAST_TIME = datetime.now() + manager_module._IO_INITIALIZED = False manager_module._REQUESTS_COUNT = 0 manager_module._EXCEPTIONS_COUNT = 0 manager_module._LAST_REQUEST_RATE_TIME = datetime.now() @@ -167,6 +168,7 @@ def test_get_process_io_success(self, mock_process, mock_datetime): # Import and modify global variables import azure.monitor.opentelemetry.exporter._performance_counters._manager as manager_module + manager_module._IO_INITIALIZED = True manager_module._IO_LAST_COUNT = 3000 # Previous total manager_module._IO_LAST_TIME = start_time @@ -193,6 +195,7 @@ def test_get_process_io_unavailable(self, mock_process, mock_datetime): import azure.monitor.opentelemetry.exporter._performance_counters._manager as manager_module manager_module._IO_AVAILABLE = 0 # Previous total + manager_module._IO_INITIALIZED = True manager_module._IO_LAST_COUNT = 0 # Previous total manager_module._IO_LAST_TIME = start_time @@ -201,6 +204,37 @@ def test_get_process_io_unavailable(self, mock_process, mock_datetime): self.assertEqual(len(result), 1) self.assertAlmostEqual(result[0].value, 0.0) + @mock.patch("azure.monitor.opentelemetry.exporter._performance_counters._manager._PROCESS") + def test_get_process_io_access_denied_disables_io(self, mock_process): + """Test process I/O retrieval disables I/O counters when access is denied.""" + mock_process.io_counters.side_effect = psutil.AccessDenied(1) + + import azure.monitor.opentelemetry.exporter._performance_counters._manager as manager_module + + manager_module._IO_AVAILABLE = True + manager_module._IO_INITIALIZED = False + + result = list(_get_process_io(None)) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0].value, 0) + self.assertFalse(manager_module._IO_AVAILABLE) + mock_process.io_counters.assert_called_once_with() + + @mock.patch("azure.monitor.opentelemetry.exporter._performance_counters._manager._PROCESS") + def test_get_process_io_disabled_does_not_call_io_counters(self, mock_process): + """Test process I/O retrieval does not call io_counters when unavailable.""" + import azure.monitor.opentelemetry.exporter._performance_counters._manager as manager_module + + manager_module._IO_AVAILABLE = False + manager_module._IO_INITIALIZED = True + + result = list(_get_process_io(None)) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0].value, 0) + mock_process.io_counters.assert_not_called() + @mock.patch("psutil.cpu_times") def test_get_processor_time_success(self, mock_cpu_times): """Test successful processor time retrieval.""" From f1cf7addb66b84590490ca9c5f9e7785086bc199 Mon Sep 17 00:00:00 2001 From: tbequinor Date: Fri, 20 Feb 2026 10:43:10 +0100 Subject: [PATCH 2/2] Update changelog --- sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md index 2ab49efeb43e..1e1a00b64472 100644 --- a/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md +++ b/sdk/monitor/azure-monitor-opentelemetry-exporter/CHANGELOG.md @@ -7,7 +7,7 @@ ### Breaking Changes ### Bugs Fixed - +- Fix a bug with crashing on import if psutil gets accessDenied. ### Other Changes ## 1.0.0b48 (2026-02-05)