From c35c59ef41724196e7083b219d8a14b1fe33d7f4 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Tue, 18 Nov 2025 23:11:28 -0500 Subject: [PATCH 1/4] add OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS environment variable --- .../system_metrics/system_metrics.rst | 5 + .../system_metrics/__init__.py | 26 ++- .../system_metrics/environment_variables.py | 55 +++++ .../tests/test_system_metrics.py | 193 +++++++++++++++++- 4 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/environment_variables.py diff --git a/docs/instrumentation/system_metrics/system_metrics.rst b/docs/instrumentation/system_metrics/system_metrics.rst index 50807eab66..406c9bf527 100644 --- a/docs/instrumentation/system_metrics/system_metrics.rst +++ b/docs/instrumentation/system_metrics/system_metrics.rst @@ -5,3 +5,8 @@ OpenTelemetry system metrics Instrumentation :members: :undoc-members: :show-inheritance: + +.. automodule:: opentelemetry.instrumentation.system_metrics.environment_variables + :members: + :undoc-members: + :show-inheritance: 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..53605af818 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 @@ -94,6 +94,7 @@ from __future__ import annotations +import fnmatch import gc import logging import os @@ -105,6 +106,9 @@ import psutil from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.system_metrics.environment_variables import ( + OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, +) from opentelemetry.instrumentation.system_metrics.package import _instruments from opentelemetry.instrumentation.system_metrics.version import __version__ from opentelemetry.metrics import CallbackOptions, Observation, get_meter @@ -154,6 +158,23 @@ _DEFAULT_CONFIG.pop("system.network.connections") +def _build_default_config() -> dict[str, list[str] | None]: + excluded_metrics: list[str] = [ + pat.strip() + for pat in os.environ.get( + OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, "" + ).split(",") + if pat + ] + if excluded_metrics: + return { + key: value + for key, value in _DEFAULT_CONFIG.items() + if not any(fnmatch.fnmatch(key, pat) for pat in excluded_metrics) + } + return _DEFAULT_CONFIG + + class SystemMetricsInstrumentor(BaseInstrumentor): def __init__( self, @@ -161,10 +182,7 @@ 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 = config or _build_default_config() self._labels = {} if labels is None else labels self._meter = None self._python_implementation = python_implementation().lower() diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/environment_variables.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/environment_variables.py new file mode 100644 index 0000000000..5e6c8bc62c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/environment_variables.py @@ -0,0 +1,55 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS = ( + "OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS" +) +""" +.. envvar:: OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS + +Specifies which system and process metrics should be excluded from collection +when using the default configuration. The value should be provided as a +comma separated list of glob patterns that match metric names to exclude. + +**Example Usage:** + +To exclude all CPU related metrics and specific process metrics: + +.. code:: bash + + export OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS="system.cpu.*,process.memory.*" + +To exclude a specific metric: + +.. code:: bash + + export OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS="system.network.io" + +**Supported Glob Patterns:** + +The environment variable supports standard glob patterns for metric filtering: + +- ``*`` - Matches any sequence of characters within a metric name +- ``?`` - Matches any single character +- ``[seq]`` - Matches any character in the sequence +- ``[!seq]`` - Matches any character not in the sequence + +**Example Patterns:** + +- ``system.*`` - Exclude all system metrics +- ``process.cpu.*`` - Exclude all process CPU related metrics +- ``*.utilization`` - Exclude all utilization metrics +- ``system.memory.usage`` - Exclude the system memory usage metric + +""" 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..1b70e14347 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -11,17 +11,20 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import os # pylint: disable=protected-access,too-many-lines - import sys +import unittest from collections import namedtuple from platform import python_implementation from unittest import mock, skipIf from opentelemetry.instrumentation.system_metrics import ( _DEFAULT_CONFIG, + OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, SystemMetricsInstrumentor, + _build_default_config, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader @@ -1091,3 +1094,191 @@ def test_that_correct_config_is_read(self): instrumentor.instrument(meter_provider=meter_provider) meter_provider.force_flush() instrumentor.uninstrument() + + +class TestBuildDefaultConfig(unittest.TestCase): + def setUp(self): + # Store original environment to restore after each test + self.env_patcher = mock.patch.dict("os.environ", {}, clear=False) + self.env_patcher.start() + + def tearDown(self): + self.env_patcher.stop() + os.environ.pop(OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, None) + + def test_default_config_without_exclusions(self): + """Test that _DEFAULT_CONFIG is returned when no exclusions are specified.""" + test_cases = [ + { + "name": "no_env_var_set", + "env_value": None, + }, + { + "name": "empty_string", + "env_value": "", + }, + { + "name": "whitespace_only", + "env_value": " ", + }, + ] + + for test_case in test_cases: + with self.subTest(test_case["name"]): + if test_case["env_value"] is None: + # Don't set the environment variable + result = _build_default_config() + else: + with mock.patch.dict( + "os.environ", + { + OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS: test_case[ + "env_value" + ] + }, + ): + result = _build_default_config() + + self.assertEqual(result, _DEFAULT_CONFIG) + + def test_exact_metric_exclusions(self): + test_cases = [ + { + "name": "single_metric", + "pattern": "system.cpu.time", + "excluded": ["system.cpu.time"], + "included": ["system.cpu.utilization", "system.memory.usage"], + "expected_count": len(_DEFAULT_CONFIG) - 1, + }, + { + "name": "multiple_metrics", + "pattern": "system.cpu.time,system.memory.usage", + "excluded": ["system.cpu.time", "system.memory.usage"], + "included": ["system.cpu.utilization", "process.cpu.time"], + "expected_count": len(_DEFAULT_CONFIG) - 2, + }, + { + "name": "with_whitespace", + "pattern": "system.cpu.time , system.memory.usage , process.cpu.time", + "excluded": [ + "system.cpu.time", + "system.memory.usage", + "process.cpu.time", + ], + "included": ["system.cpu.utilization"], + "expected_count": len(_DEFAULT_CONFIG) - 3, + }, + { + "name": "non_existent_metric", + "pattern": "non.existent.metric", + "excluded": [], + "included": ["system.cpu.time", "process.cpu.time"], + "expected_count": len(_DEFAULT_CONFIG), + }, + ] + + for test_case in test_cases: + with self.subTest(test_case["name"]): + with mock.patch.dict( + "os.environ", + { + OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS: test_case[ + "pattern" + ] + }, + ): + result = _build_default_config() + + for metric in test_case["excluded"]: + self.assertNotIn( + metric, result, f"{metric} should be excluded" + ) + + for metric in test_case["included"]: + self.assertIn( + metric, result, f"{metric} should be included" + ) + + self.assertEqual(len(result), test_case["expected_count"]) + + def test_wildcard_patterns(self): + test_cases = [ + { + "name": "all_system_metrics", + "pattern": "system.*", + "excluded_prefixes": ["system."], + "included_prefixes": ["process.", "cpython."], + }, + { + "name": "system_cpu_prefix", + "pattern": "system.cpu.*", + "excluded": ["system.cpu.time", "system.cpu.utilization"], + "included": ["system.memory.usage", "system.disk.io"], + }, + { + "name": "utilization_suffix", + "pattern": "*.utilization", + "excluded_suffixes": [".utilization"], + "included": ["system.cpu.time", "system.memory.usage"], + }, + { + "name": "all_metrics", + "pattern": "*", + "expected_count": 0, + }, + ] + + for test_case in test_cases: + with self.subTest(test_case["name"]): + with mock.patch.dict( + "os.environ", + { + OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS: test_case[ + "pattern" + ] + }, + ): + result = _build_default_config() + + if "excluded" in test_case: + for metric in test_case["excluded"]: + self.assertNotIn(metric, result) + + if "included" in test_case: + for metric in test_case["included"]: + self.assertIn(metric, result) + + if "excluded_prefixes" in test_case: + for prefix in test_case["excluded_prefixes"]: + excluded_metrics = [ + k for k in result if k.startswith(prefix) + ] + self.assertEqual( + len(excluded_metrics), + 0, + ) + + if "included_prefixes" in test_case: + for prefix in test_case["included_prefixes"]: + included_metrics = [ + k for k in result if k.startswith(prefix) + ] + self.assertGreater( + len(included_metrics), + 0, + ) + + if "excluded_suffixes" in test_case: + for suffix in test_case["excluded_suffixes"]: + suffix_metrics = [ + k for k in result if k.endswith(suffix) + ] + self.assertEqual( + len(suffix_metrics), + 0, + ) + + if "expected_count" in test_case: + self.assertEqual( + len(result), test_case["expected_count"] + ) From 118f63a962bd3005cbe2ea53ad58763d77492dba Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 5 Dec 2025 10:00:15 -0500 Subject: [PATCH 2/4] fix CHANGELOG.md format --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d79a899ba..576751e39a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- `opentelemetry-instrumentation-system-metrics`: Add support for the `OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS` environment variable + ([#3959](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3959)) + ## Version 1.39.0/0.60b0 (2025-12-03) ### Added From c034dc8127719c80c1491b99402c370139901dad Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Tue, 18 Nov 2025 23:23:15 -0500 Subject: [PATCH 3/4] tweak how config is initialized --- .../opentelemetry/instrumentation/system_metrics/__init__.py | 2 +- .../tests/test_system_metrics.py | 2 -- 2 files changed, 1 insertion(+), 3 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 53605af818..6ffb50a9e3 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 @@ -182,7 +182,7 @@ def __init__( config: dict[str, list[str] | None] | None = None, ): super().__init__() - self._config = config or _build_default_config() + self._config = _build_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() 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 1b70e14347..ef110c3da1 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -1098,7 +1098,6 @@ def test_that_correct_config_is_read(self): class TestBuildDefaultConfig(unittest.TestCase): def setUp(self): - # Store original environment to restore after each test self.env_patcher = mock.patch.dict("os.environ", {}, clear=False) self.env_patcher.start() @@ -1107,7 +1106,6 @@ def tearDown(self): os.environ.pop(OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, None) def test_default_config_without_exclusions(self): - """Test that _DEFAULT_CONFIG is returned when no exclusions are specified.""" test_cases = [ { "name": "no_env_var_set", From cbf9d2b998afd8caafdd44af9daa6a0d84f5b7f9 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Mon, 24 Nov 2025 16:58:08 -0500 Subject: [PATCH 4/4] Limit documentation to basic glob patterns --- .../opentelemetry/instrumentation/system_metrics/__init__.py | 2 +- .../instrumentation/system_metrics/environment_variables.py | 3 --- 2 files changed, 1 insertion(+), 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 6ffb50a9e3..e0e425aada 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 @@ -164,7 +164,7 @@ def _build_default_config() -> dict[str, list[str] | None]: for pat in os.environ.get( OTEL_PYTHON_SYSTEM_METRICS_EXCLUDED_METRICS, "" ).split(",") - if pat + if pat.strip() ] if excluded_metrics: return { diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/environment_variables.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/environment_variables.py index 5e6c8bc62c..8a1379871e 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/environment_variables.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/environment_variables.py @@ -41,9 +41,6 @@ The environment variable supports standard glob patterns for metric filtering: - ``*`` - Matches any sequence of characters within a metric name -- ``?`` - Matches any single character -- ``[seq]`` - Matches any character in the sequence -- ``[!seq]`` - Matches any character not in the sequence **Example Patterns:**