Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3936](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3936))
- `opentelemetry-instrumentation-aiohttp-client`: Update instrumentor to respect suppressing http instrumentation
([#3957](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3957))
- `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.38.0/0.59b0 (2025-10-16)

Expand Down
5 changes: 5 additions & 0 deletions docs/instrumentation/system_metrics/system_metrics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@

from __future__ import annotations

import fnmatch
import gc
import logging
import os
Expand All @@ -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
Expand Down Expand Up @@ -154,17 +158,31 @@
_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.strip()
]
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,
labels: dict[str, str] | None = None,
config: dict[str, list[str] | None] | None = None,
):
super().__init__()
if config is None:
self._config = _DEFAULT_CONFIG
else:
self._config = 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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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

**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

"""
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1091,3 +1094,189 @@ 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):
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_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"]
)