From d8facf6fd173a8c057e8ca624c2901944a9222cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozren=20Dabi=C4=87?= Date: Sun, 7 Sep 2025 17:30:37 +0200 Subject: [PATCH 1/6] Use dedicated constant for existing platform detection --- .../opentelemetry/instrumentation/system_metrics/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index b77ff12f64..fbc452f898 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -149,7 +149,7 @@ "process.runtime.context_switches": ["involuntary", "voluntary"], } -if sys.platform == "darwin": +if psutil.MACOS: # see https://github.com/giampaolo/psutil/issues/1219 _DEFAULT_CONFIG.pop("system.network.connections") From 0e8e68f05592960678e35a3278200b7b595ae02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozren=20Dabi=C4=87?= Date: Mon, 8 Sep 2025 19:43:44 +0200 Subject: [PATCH 2/6] Use ternary to assign config and create copies of argument/default --- .../instrumentation/system_metrics/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index fbc452f898..353116671c 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -99,6 +99,7 @@ import os import sys import threading +from copy import deepcopy from platform import python_implementation from typing import Any, Collection, Iterable @@ -161,10 +162,8 @@ def __init__( config: dict[str, list[str] | None] | None = None, ): super().__init__() - if config is None: - self._config = _DEFAULT_CONFIG - else: - self._config = config + + self._config = deepcopy(_DEFAULT_CONFIG if config is None else config) self._labels = {} if labels is None else labels self._meter = None self._python_implementation = python_implementation().lower() From fc878a69c619d83163b11afd18c77dc2822679f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozren=20Dabi=C4=87?= Date: Mon, 8 Sep 2025 19:54:52 +0200 Subject: [PATCH 3/6] Log warning on Mac if 'system.network.connections' was added and remove it --- .../instrumentation/system_metrics/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index 353116671c..9fad770ffd 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -168,6 +168,16 @@ def __init__( self._meter = None self._python_implementation = python_implementation().lower() + # If 'system.network.connections' is found in the config at this time, + # then the user chose to explicitly add it themselves. + # We therefore remove the metric and issue a warning. + if psutil.MACOS and "system.network.connections" in self._config: + _logger.warning( + "'psutil.net_connections' can not reliably be computed on macOS! " + "'system.network.connections' will be excluded from metrics." + ) + self._config.pop("system.network.connections") + self._proc = psutil.Process(os.getpid()) self._system_cpu_time_labels = self._labels.copy() From 94316923afa8944cfffd2f7535361213e2b3ebb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozren=20Dabi=C4=87?= Date: Mon, 8 Sep 2025 20:01:09 +0200 Subject: [PATCH 4/6] Log warning on Linux if `/proc/vmstat` is not available and remove metric states --- .../system_metrics/__init__.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index 9fad770ffd..f3a03a4ba6 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -178,6 +178,33 @@ def __init__( ) self._config.pop("system.network.connections") + # Filter 'sin' and 'sout' from 'system.swap' metrics + # if '/proc/vmstat' is not available and issue a warning. + # See: https://github.com/open-telemetry/opentelemetry-python-contrib/issues/3740. + if psutil.LINUX and ( + "system.swap.usage" in self._config + or "system.swap.utilization" in self._config + ): + vmstat = os.path.join(psutil.PROCFS_PATH, "vmstat") + if not os.path.exists(vmstat): + _logger.warning( + "Could not find '%s'! The 'sin' and 'sout' states" + "will not be included in 'system.swap' metrics.", + vmstat, + ) + if usage := self._config.get("system.swap.usage"): + self._config["system.swap.usage"] = [ + state + for state in usage + if state not in ("sin", "sout") + ] + if utilization := self._config.get("system.swap.utilization"): + self._config["system.swap.utilization"] = [ + state + for state in utilization + if state not in ("sin", "sout") + ] + self._proc = psutil.Process(os.getpid()) self._system_cpu_time_labels = self._labels.copy() From dbb81e0901e3f69fc47f091c1820125df498b055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozren=20Dabi=C4=87?= Date: Mon, 8 Sep 2025 20:29:10 +0200 Subject: [PATCH 5/6] Suppress warnings related to `psutil.swap_memory` when performing observations --- .../system_metrics/__init__.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index f3a03a4ba6..6cfc050d0f 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -99,6 +99,8 @@ import os import sys import threading +import warnings +from contextlib import contextmanager from copy import deepcopy from platform import python_implementation from typing import Any, Collection, Iterable @@ -664,7 +666,8 @@ def _get_system_swap_usage( self, options: CallbackOptions ) -> Iterable[Observation]: """Observer callback for swap usage""" - system_swap = psutil.swap_memory() + with self._suppress_psutil_swap_warnings(): + system_swap = psutil.swap_memory() for metric in self._config["system.swap.usage"]: self._system_swap_usage_labels["state"] = metric @@ -678,7 +681,8 @@ def _get_system_swap_utilization( self, options: CallbackOptions ) -> Iterable[Observation]: """Observer callback for swap utilization""" - system_swap = psutil.swap_memory() + with self._suppress_psutil_swap_warnings(): + system_swap = psutil.swap_memory() for metric in self._config["system.swap.utilization"]: if hasattr(system_swap, metric): @@ -1037,3 +1041,17 @@ def _get_runtime_context_switches( getattr(ctx_switches, metric), self._runtime_context_switches_labels.copy(), ) + + @staticmethod + @contextmanager + def _suppress_psutil_swap_warnings(): + with warnings.catch_warnings(): + warnings.filterwarnings( + action="ignore", + category=RuntimeWarning, + # language=regexp + message=r"^'sin' and 'sout' swap memory stats couldn't be determined and were set to 0", + # language=regexp + module=r"^psutil$", + ) + yield From 4753b385683c3756b29eb4a5119fc8188c5ddf63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ozren=20Dabi=C4=87?= Date: Mon, 8 Sep 2025 22:30:48 +0200 Subject: [PATCH 6/6] Add a test case --- .../tests/test_system_metrics.py | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py index b71c307758..b2d4f050b7 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -17,7 +17,8 @@ import sys from collections import namedtuple from platform import python_implementation -from unittest import mock, skipIf +from tempfile import TemporaryDirectory +from unittest import mock, skipIf, skipUnless from opentelemetry.instrumentation.system_metrics import ( _DEFAULT_CONFIG, @@ -415,6 +416,38 @@ def test_system_swap_utilization(self, mock_swap_memory): ] self._test_metrics("system.swap.utilization", expected) + @skipUnless(sys.platform == "linux", "Linux only") + def test_system_swap_states_removed_when_vmstat_missing(self): + with ( + self.assertLogs(level="WARNING") as logwatcher, + TemporaryDirectory() as tmpdir, + mock.patch( + target="psutil.PROCFS_PATH", + new=tmpdir, + create=True, + ), + ): + runtime_config = { + "system.swap.usage": ["free", "sin", "sout", "used"], + "system.swap.utilization": ["free", "sin", "sout", "used"], + } + + runtime_metrics = SystemMetricsInstrumentor(config=runtime_config) + runtime_metrics.instrument() + + self.assertEqual( + first=len(logwatcher.records), + second=1, + ) + self.assertListEqual( + list1=runtime_metrics._config["system.swap.usage"], + list2=["free", "used"], + ) + self.assertListEqual( + list1=runtime_metrics._config["system.swap.utilization"], + list2=["free", "used"], + ) + @mock.patch("psutil.disk_io_counters") def test_system_disk_io(self, mock_disk_io_counters): DiskIO = namedtuple(