Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
87576cc
Added support for exclude list in attribute keys
saurabh-saraswat Jun 19, 2026
dc14a6d
corrected lint issues
saurabh-saraswat Jun 19, 2026
15256cc
Updated changelog fragment file
saurabh-saraswat Jun 19, 2026
65a652b
Update .changelog/5332.fixed
saurabh-saraswat Jul 1, 2026
498b25b
Renamed changelog fragment
saurabh-saraswat Jul 2, 2026
39ae4d8
Incorporated review comments
saurabh-saraswat Jul 6, 2026
3d15f96
Updated for comment related to assertions
saurabh-saraswat Jul 13, 2026
512dace
Merge branch 'main' into exclude_attribute
saurabh-saraswat Jul 13, 2026
e5e2f3c
Merge branch 'main' into exclude_attribute
saurabh-saraswat Jul 13, 2026
ac4e44a
Merge branch 'main' into exclude_attribute
xrmx Jul 14, 2026
e70b832
Updated measurement based on exclude attributes
saurabh-saraswat Jul 15, 2026
7b4046e
Merge branch 'main' into exclude_attribute
saurabh-saraswat Jul 15, 2026
87d042c
Merge branch 'main' into exclude_attribute
saurabh-saraswat Jul 16, 2026
aa4e89b
Merge branch 'main' into exclude_attribute
saurabh-saraswat Jul 17, 2026
7a36cb9
Apply suggestion from @herin049
saurabh-saraswat Jul 21, 2026
6ae5263
Apply suggestion from @herin049
saurabh-saraswat Jul 21, 2026
8a8d139
Apply suggestion from @herin049
saurabh-saraswat Jul 21, 2026
0172b13
Incorporated review comments related type of attribute_keys, exclude_…
saurabh-saraswat Jul 21, 2026
d1b0626
Merge branch 'main' into exclude_attribute
saurabh-saraswat Jul 27, 2026
70da89a
Based on review comment from Herin, refactored the code
saurabh-saraswat Jul 27, 2026
7251a05
Incorporated review comments related to testcase and cloning of measu…
saurabh-saraswat Jul 30, 2026
6e1d27b
Merge branch 'main' into exclude_attribute
saurabh-saraswat Jul 30, 2026
f5a367d
Merge branch 'main' into exclude_attribute
saurabh-saraswat Jul 31, 2026
494aa4f
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 3, 2026
de4ed2c
Incorporated review comment from lzchen by materializing the iterator…
saurabh-saraswat Aug 4, 2026
4771f97
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 5, 2026
bfacf31
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 6, 2026
fbf6f3e
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 12, 2026
39d17b4
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 13, 2026
b951d97
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 14, 2026
b38d113
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 21, 2026
0b4b678
Merge branch 'main' into exclude_attribute
lzchen Aug 21, 2026
f5765e8
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 24, 2026
f29dd82
Corrected header inclusion
saurabh-saraswat Aug 24, 2026
c799615
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 25, 2026
1c5de9b
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 26, 2026
4f360ac
Merge branch 'main' into exclude_attribute
saurabh-saraswat Aug 31, 2026
093da4e
Updated failing testcase to use hash
saurabh-saraswat Aug 31, 2026
3e0a9f9
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 1, 2026
d190888
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 1, 2026
71a1985
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 2, 2026
050795a
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 4, 2026
6483e65
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 7, 2026
517a8ac
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 8, 2026
32ebe70
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 9, 2026
71b1c3d
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 15, 2026
f4622df
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 17, 2026
12454d2
Update opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py
saurabh-saraswat Sep 21, 2026
fd56da6
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 21, 2026
21c7bd5
Incorporated review comments related to specfic exception, eliminatin…
saurabh-saraswat Sep 22, 2026
7596c15
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 22, 2026
0d4bb7d
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 24, 2026
55b392f
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 25, 2026
833951d
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 29, 2026
f0bd477
Merge branch 'main' into exclude_attribute
saurabh-saraswat Sep 30, 2026
915b5f3
Merge branch 'main' into exclude_attribute
saurabh-saraswat Oct 5, 2026
878213e
Merge branch 'main' into exclude_attribute
saurabh-saraswat Oct 6, 2026
bb588d3
Incorprated review comment related to wildcard handling
saurabh-saraswat Oct 7, 2026
3ffd0c7
Merge branch 'main' into exclude_attribute
saurabh-saraswat Oct 7, 2026
feb8082
Merge branch 'main' into exclude_attribute
saurabh-saraswat Oct 13, 2026
282c0ff
Merge branch 'main' into exclude_attribute
saurabh-saraswat Oct 15, 2026
f698935
Merge branch 'main' into exclude_attribute
saurabh-saraswat Oct 16, 2026
cbaec00
Merge branch 'main' into exclude_attribute
saurabh-saraswat Oct 21, 2026
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
1 change: 1 addition & 0 deletions .changelog/5332.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: add support for parameter `exclude_attribute_keys` in `View`
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,13 @@ def _create_view(config: ViewConfig) -> View:
if instrument_type is None:
raise ConfigurationError(f"Unknown instrument type: {selector.instrument_type!r}")

attribute_keys: set[str] | None = None
attribute_keys: list[str] | None = None
exclude_attribute_keys: list[str] | None = None
if stream.attribute_keys is not None:
if stream.attribute_keys.excluded:
_logger.warning(
"attribute_keys.excluded is not supported by the Python SDK View; the exclusion list will be ignored."
)
if stream.attribute_keys.excluded is not None:
exclude_attribute_keys = stream.attribute_keys.excluded
if stream.attribute_keys.included is not None:
attribute_keys = set(stream.attribute_keys.included)
attribute_keys = stream.attribute_keys.included

aggregation = None
if stream.aggregation is not None:
Expand All @@ -255,6 +254,7 @@ def _create_view(config: ViewConfig) -> View:
description=stream.description,
attribute_keys=attribute_keys,
aggregation=aggregation,
exclude_attribute_keys=exclude_attribute_keys,
)


Expand Down
11 changes: 7 additions & 4 deletions opentelemetry-configuration/tests/test_meter_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,11 +868,14 @@ def test_stream_attribute_keys_included(self):
)
self.assertEqual(view._attribute_keys, {"key1", "key2"})

def test_stream_attribute_keys_excluded_logs_warning(self):
def test_stream_attribute_keys_excluded_is_applied(self):
config = self._make_view_config(stream_kwargs={"attribute_keys": IncludeExclude(excluded=["key1"])})
with self.assertLogs("opentelemetry.configuration._meter_provider", level="WARNING") as log:
create_meter_provider(config)
self.assertTrue(any("excluded" in msg for msg in log.output))
meter_provider = create_meter_provider(config)
views = meter_provider._sdk_config.views
self.assertEqual(len(views), 1)
view = views[0]
self.assertEqual(view._exclude_attribute_keys, frozenset({"key1"}))
self.assertIsNone(view._attribute_keys)

def test_stream_aggregation_drop(self):
view = self._get_view(self._make_view_config(stream_kwargs={"aggregation": AggregationConfig(drop={})}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# SPDX-License-Identifier: Apache-2.0

from collections.abc import Mapping, Sequence
from fnmatch import fnmatchcase
from logging import getLogger
from threading import Lock
from time import time_ns
Expand Down Expand Up @@ -130,7 +131,19 @@ def consume_measurement(self, measurement: Measurement, should_sample_exemplar:
# leading to unexpected behavior, but deep copying is expensive.
attributes = dict(measurement.attributes)
if self._view._attribute_keys is not None:
attributes = {k: v for k, v in attributes.items() if k in self._view._attribute_keys}
attributes = {
key: value
for key, value in attributes.items()
if any(fnmatchcase(key, pattern) for pattern in self._view._attribute_keys)
}
elif self._view._exclude_attribute_keys:
attributes = {
key: value
for key, value in attributes.items()
if not any(fnmatchcase(key, pattern) for pattern in self._view._exclude_attribute_keys)
}
else:
attributes = {}

aggr_key = _hash_attributes(attributes)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# SPDX-License-Identifier: Apache-2.0


from collections.abc import Callable
from collections.abc import Callable, Iterable
from fnmatch import fnmatchcase
from logging import getLogger

Expand Down Expand Up @@ -90,6 +90,16 @@ class View:
instrument_unit: This is an instrument matching attribute: the unit the
instrument must have to match the view.

exclude_attribute_keys: This is a metric stream customizing attribute: this is
a set of attribute keys. If not `None` then measurement attributes whose
keys are in ``exclude_attribute_keys`` will be removed before identifying
the metric stream.
Matching is case-sensitive. Values are evaluated to match as follows:
- If the value exactly matches.
- If the value matches the wildcard pattern, where '?' matches any single
character and '*' matches any number of characters including none.


This class is not intended to be subclassed by the user.
"""

Expand All @@ -104,10 +114,11 @@ def __init__(
meter_schema_url: str | None = None,
name: str | None = None,
description: str | None = None,
attribute_keys: set[str] | None = None,
attribute_keys: Iterable[str] | None = None,
aggregation: Aggregation | None = None,
exemplar_reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder] | None = None,
instrument_unit: str | None = None,
exclude_attribute_keys: Iterable[str] | None = None,
):
if (
instrument_type
Expand All @@ -130,7 +141,28 @@ def __init__(
if any(c in instrument_name for c in "*?["):
# pylint: disable=broad-exception-raised
raise Exception(f"View {name} declared with wildcard characters in instrument_name")

attribute_keys = frozenset(attribute_keys) if attribute_keys is not None else None

exclude_attribute_keys = frozenset(exclude_attribute_keys) if exclude_attribute_keys is not None else None

if attribute_keys is not None and exclude_attribute_keys is not None:
overlap = [
(attribute_key, exclude_attribute_key)
for attribute_key in attribute_keys
for exclude_attribute_key in exclude_attribute_keys
if (
fnmatchcase(attribute_key, exclude_attribute_key)
or fnmatchcase(
exclude_attribute_key,
attribute_key,
)
)
]

if overlap:
# pylint: disable=broad-exception-raised
raise ValueError(f"attribute_keys and exclude_attribute_keys contain overlapping patterns: {overlap}")
# _name, _description, _aggregation, _exemplar_reservoir_factory and
# _attribute_keys will be accessed when instantiating a _ViewInstrumentMatch.
self._name = name
Expand All @@ -145,6 +177,7 @@ def __init__(
self._attribute_keys = attribute_keys
self._aggregation = aggregation or self._default_aggregation
self._exemplar_reservoir_factory = exemplar_reservoir_factory or _default_reservoir_factory
self._exclude_attribute_keys = exclude_attribute_keys

# pylint: disable=too-many-return-statements
# pylint: disable=too-many-branches
Expand Down
41 changes: 41 additions & 0 deletions opentelemetry-sdk/tests/metrics/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,47 @@ def test_view_name(self):
self.assertEqual(view._name, "name")
self.assertEqual(view._instrument_name, "instrument_name_1")

def test_attribute_keys_and_exclude_attribute_keys_overlap(self):
test_cases = [
{
"name": "exact overlap",
"attribute_keys": ("method", "status_code"),
"exclude_attribute_keys": ("method", "user_id"),
},
{
"name": "wildcard include overlaps exact exclude",
"attribute_keys": ("k8s.*",),
"exclude_attribute_keys": ("k8s.node",),
},
{
"name": "exact include overlaps wildcard exclude",
"attribute_keys": ("k8s.node",),
"exclude_attribute_keys": ("k8s.*",),
},
{
"name": "wildcard overlaps wildcard",
"attribute_keys": ("k8s.*",),
"exclude_attribute_keys": ("k8s.n*",),
},
]

for case in test_cases:
with self.subTest(case=case["name"]):
with self.assertRaises(ValueError):
View(
instrument_name="instrument_name",
attribute_keys=case["attribute_keys"],
exclude_attribute_keys=case["exclude_attribute_keys"],
)

def test_attribute_keys_and_exclude_attribute_keys_disjoint(self):
view = View(
instrument_name="instrument_name",
attribute_keys=("method", "status_code"),
exclude_attribute_keys=("user_id",),
)

self.assertIsNotNone(view)
def test_view_name_without_instrument_name(self):
cases = [
("custom_name", {"instrument_type": Mock}),
Expand Down
122 changes: 122 additions & 0 deletions opentelemetry-sdk/tests/metrics/test_view_instrument_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,90 @@ def setUpClass(cls):
views=[],
Comment thread
saurabh-saraswat marked this conversation as resolved.
)

def test_consume_measurement_with_exclude_attribute_keys(self):
Comment thread
saurabh-saraswat marked this conversation as resolved.
test_cases = [
{
"name": "excluded attribute removed",
"exclude_attribute_keys": {"f"},
"measurement_attributes": {"c": "d", "f": "g"},
"expected_attributes": {"c": "d"},
},
{
"name": "wildcard exclusion",
"exclude_attribute_keys": {"k8s.*"},
"measurement_attributes": {
"k8s.node": "node1",
"k8s.cluster": "cluster1",
"service.name": "svc",
},
"expected_attributes": {
"service.name": "svc",
},
},
{
"name": "question mark exclusion",
"exclude_attribute_keys": {"node?"},
"measurement_attributes": {
"node1": "a",
"node2": "b",
"node12": "c",
},
"expected_attributes": {
"node12": "c",
},
},
{
"name": "wildcard exact exclusion",
"exclude_attribute_keys": {"*.node"},
"measurement_attributes": {
"k8s.node": "node1",
"k8s.cluster": "cluster1",
},
"expected_attributes": {
"k8s.cluster": "cluster1",
},
},
{
"name": "none attributes become empty attributes",
"exclude_attribute_keys": {"f"},
"measurement_attributes": None,
"expected_attributes": {},
},
]

for case in test_cases:
with self.subTest(case=case["name"]):
instrument1 = Mock(name="instrument1")
instrument1.instrumentation_scope = self.mock_instrumentation_scope

view_instrument_match = _ViewInstrumentMatch(
view=View(
instrument_name="instrument1",
name="name",
aggregation=self.mock_aggregation_factory,
exclude_attribute_keys=case["exclude_attribute_keys"],
),
instrument=instrument1,
instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}),
)

view_instrument_match.consume_measurement(
Measurement(
value=0,
time_unix_nano=time_ns(),
instrument=instrument1,
context=Context(),
attributes=case["measurement_attributes"],
)
)

self.assertEqual(
view_instrument_match._attributes_aggregation,
{
_hash_attributes(case["expected_attributes"]): (self.mock_created_aggregation),
},
)

def test_consume_measurement(self):
instrument1 = Mock(name="instrument1")
instrument1.instrumentation_scope = self.mock_instrumentation_scope
Expand Down Expand Up @@ -119,6 +203,44 @@ def test_consume_measurement(self):
},
)

# wildcard attribute_keys should match multiple attributes
view_instrument_match = _ViewInstrumentMatch(
view=View(
instrument_name="instrument1",
name="name",
aggregation=self.mock_aggregation_factory,
attribute_keys={"k8s.*"},
),
instrument=instrument1,
instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}),
)

view_instrument_match.consume_measurement(
Measurement(
value=0,
time_unix_nano=time_ns(),
instrument=instrument1,
context=Context(),
attributes={
"k8s.node": "node1",
"k8s.cluster": "cluster1",
"service.name": "svc",
},
)
)

self.assertEqual(
view_instrument_match._attributes_aggregation,
{
_hash_attributes(
{
"k8s.node": "node1",
"k8s.cluster": "cluster1",
}
): self.mock_created_aggregation,
},
)

# setup new instrument match without `attribute_keys` set, defaults to keeping
# all attributes.
view_instrument_match = _ViewInstrumentMatch(
Expand Down