From 87576ccc1c2f077753746814fe01e26ff7813df0 Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Fri, 19 Jun 2026 08:51:00 +0000 Subject: [PATCH 01/16] Added support for exclude list in attribute keys --- .../configuration/_meter_provider.py | 8 +- .../tests/test_meter_provider.py | 14 +-- .../_internal/_view_instrument_match.py | 8 ++ .../sdk/metrics/_internal/view.py | 8 ++ .../metrics/test_view_instrument_match.py | 98 +++++++++++++++++++ 5 files changed, 124 insertions(+), 12 deletions(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py index 23db41f2078..8943eb974cd 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py @@ -240,11 +240,8 @@ def _create_view(config: ViewConfig) -> View: attribute_keys: set[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 = set(stream.attribute_keys.excluded) if stream.attribute_keys.included is not None: attribute_keys = set(stream.attribute_keys.included) @@ -263,6 +260,7 @@ def _create_view(config: ViewConfig) -> View: description=stream.description, attribute_keys=attribute_keys, aggregation=aggregation, + exclude_attribute_keys=exclude_attribute_keys, ) diff --git a/opentelemetry-configuration/tests/test_meter_provider.py b/opentelemetry-configuration/tests/test_meter_provider.py index 5e1d7e686a6..872e539d6d8 100644 --- a/opentelemetry-configuration/tests/test_meter_provider.py +++ b/opentelemetry-configuration/tests/test_meter_provider.py @@ -875,16 +875,16 @@ 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( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index d9ba05363b1..209caac367b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -98,6 +98,14 @@ def consume_measurement( else: attributes = {} + if self._view._exclude_attribute_keys is not None: + if attributes is not None: + attributes = { + key: value + for key, value in attributes.items() + if key not in self._view._exclude_attribute_keys + } + aggr_key = frozenset(attributes.items()) if aggr_key not in self._attributes_aggregation: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index 7eb1fcc728e..3e332009fa6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -88,6 +88,12 @@ 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. Applied after ``attribute_keys`` if both are provided. + + This class is not intended to be subclassed by the user. """ @@ -109,6 +115,7 @@ def __init__( ] | None = None, instrument_unit: str | None = None, + exclude_attribute_keys: set[str] | None = None, ): if ( instrument_type @@ -152,6 +159,7 @@ def __init__( 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 diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 73b0e6e24a5..175e2836ce0 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -73,6 +73,104 @@ def setUpClass(cls): resource=cls.mock_resource, views=[], ) + def test_view_instrument_match_exclude_attribute_keys_affects_aggregation(self): + instrument1 = Mock(name="instrument1") + instrument1.instrumentation_scope = self.mock_instrumentation_scope + + mock_aggregation = MagicMock() + mock_aggregation._create_aggregation.return_value = MagicMock() + + instrument_class_aggregation = MagicMock() + instrument_class_aggregation.__getitem__.return_value = mock_aggregation + + view = View( + instrument_name="instrument1", + exclude_attribute_keys={"user_id"}, + ) + match = _ViewInstrumentMatch( + view, + instrument=instrument1, + instrument_class_aggregation= instrument_class_aggregation + ) + measurement1 = Measurement( + value=1, + time_unix_nano=time_ns(), + instrument=instrument1, + context=Context(), + attributes={"method": "GET", "user_id": "u1"}, + ) + measurement2 = Measurement( + value=2, + time_unix_nano=time_ns(), + instrument=instrument1, + context=Context(), + attributes={"method": "GET", "user_id": "u2"}, + ) + + match.consume_measurement(measurement1) + match.consume_measurement(measurement2) + assert len(match._attributes_aggregation) == 1 + aggr_key = list(match._attributes_aggregation.keys())[0] + assert dict(aggr_key) == {"method": "GET"} + + + def test_view_instrument_match_exclude_removes_attributes(self): + instrument1 = Mock(name="instrument1") + instrument1.instrumentation_scope = self.mock_instrumentation_scope + mock_aggregation = MagicMock() + mock_aggregation._create_aggregation.return_value = MagicMock() + + instrument_class_aggregation = MagicMock() + instrument_class_aggregation.__getitem__.return_value = mock_aggregation + view = View( + instrument_name="instrument1", + exclude_attribute_keys={"user_id"}, + ) + match = _ViewInstrumentMatch( + view, + instrument=instrument1, + instrument_class_aggregation=instrument_class_aggregation + ) + measurement = Measurement( + value=1, + time_unix_nano=time_ns(), + instrument=instrument1, + context=Context(), + attributes={"method": "GET", "user_id": "u1"}, + ) + match.consume_measurement(measurement) + aggr_key = list(match._attributes_aggregation.keys())[0] + assert "user_id" not in dict(aggr_key) + + def test_view_instrument_match_include_then_exclude(self): + instrument1 = Mock(name="instrument1") + instrument1.instrumentation_scope = self.mock_instrumentation_scope + mock_aggregation = MagicMock() + mock_aggregation._create_aggregation.return_value = MagicMock() + + instrument_class_aggregation = MagicMock() + instrument_class_aggregation.__getitem__.return_value = mock_aggregation + view = View( + instrument_name="instrument1", + attribute_keys={"method", "user_id"}, + exclude_attribute_keys={"user_id"}, + ) + match = _ViewInstrumentMatch( + view, + instrument=instrument1, + instrument_class_aggregation=instrument_class_aggregation + ) + measurement = Measurement( + value=1, + time_unix_nano=time_ns(), + instrument=instrument1, + context=Context(), + attributes={"method": "GET", "user_id": "u1", "x": "y"}, + ) + match.consume_measurement(measurement) + aggr_key = list(match._attributes_aggregation.keys())[0] + assert dict(aggr_key) == {"method": "GET"} + def test_consume_measurement(self): instrument1 = Mock(name="instrument1") From dc14a6d8c6727fa9d79f5738420982d9c9aea3a1 Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:34:00 +0000 Subject: [PATCH 02/16] corrected lint issues --- .../tests/test_meter_provider.py | 1 + .../metrics/test_view_instrument_match.py | 25 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/opentelemetry-configuration/tests/test_meter_provider.py b/opentelemetry-configuration/tests/test_meter_provider.py index 872e539d6d8..9ac04cb72fa 100644 --- a/opentelemetry-configuration/tests/test_meter_provider.py +++ b/opentelemetry-configuration/tests/test_meter_provider.py @@ -875,6 +875,7 @@ def test_stream_attribute_keys_included(self): ) ) self.assertEqual(view._attribute_keys, {"key1", "key2"}) + def test_stream_attribute_keys_excluded_is_applied(self): config = self._make_view_config( stream_kwargs={"attribute_keys": IncludeExclude(excluded=["key1"])} diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 175e2836ce0..e0481090e60 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -73,7 +73,10 @@ def setUpClass(cls): resource=cls.mock_resource, views=[], ) - def test_view_instrument_match_exclude_attribute_keys_affects_aggregation(self): + + def test_view_instrument_match_exclude_attribute_keys_affects_aggregation( + self, + ): instrument1 = Mock(name="instrument1") instrument1.instrumentation_scope = self.mock_instrumentation_scope @@ -81,7 +84,9 @@ def test_view_instrument_match_exclude_attribute_keys_affects_aggregation(self): mock_aggregation._create_aggregation.return_value = MagicMock() instrument_class_aggregation = MagicMock() - instrument_class_aggregation.__getitem__.return_value = mock_aggregation + instrument_class_aggregation.__getitem__.return_value = ( + mock_aggregation + ) view = View( instrument_name="instrument1", @@ -90,7 +95,7 @@ def test_view_instrument_match_exclude_attribute_keys_affects_aggregation(self): match = _ViewInstrumentMatch( view, instrument=instrument1, - instrument_class_aggregation= instrument_class_aggregation + instrument_class_aggregation=instrument_class_aggregation, ) measurement1 = Measurement( value=1, @@ -113,7 +118,6 @@ def test_view_instrument_match_exclude_attribute_keys_affects_aggregation(self): aggr_key = list(match._attributes_aggregation.keys())[0] assert dict(aggr_key) == {"method": "GET"} - def test_view_instrument_match_exclude_removes_attributes(self): instrument1 = Mock(name="instrument1") instrument1.instrumentation_scope = self.mock_instrumentation_scope @@ -121,7 +125,9 @@ def test_view_instrument_match_exclude_removes_attributes(self): mock_aggregation._create_aggregation.return_value = MagicMock() instrument_class_aggregation = MagicMock() - instrument_class_aggregation.__getitem__.return_value = mock_aggregation + instrument_class_aggregation.__getitem__.return_value = ( + mock_aggregation + ) view = View( instrument_name="instrument1", exclude_attribute_keys={"user_id"}, @@ -129,7 +135,7 @@ def test_view_instrument_match_exclude_removes_attributes(self): match = _ViewInstrumentMatch( view, instrument=instrument1, - instrument_class_aggregation=instrument_class_aggregation + instrument_class_aggregation=instrument_class_aggregation, ) measurement = Measurement( value=1, @@ -149,7 +155,9 @@ def test_view_instrument_match_include_then_exclude(self): mock_aggregation._create_aggregation.return_value = MagicMock() instrument_class_aggregation = MagicMock() - instrument_class_aggregation.__getitem__.return_value = mock_aggregation + instrument_class_aggregation.__getitem__.return_value = ( + mock_aggregation + ) view = View( instrument_name="instrument1", attribute_keys={"method", "user_id"}, @@ -158,7 +166,7 @@ def test_view_instrument_match_include_then_exclude(self): match = _ViewInstrumentMatch( view, instrument=instrument1, - instrument_class_aggregation=instrument_class_aggregation + instrument_class_aggregation=instrument_class_aggregation, ) measurement = Measurement( value=1, @@ -171,7 +179,6 @@ def test_view_instrument_match_include_then_exclude(self): aggr_key = list(match._attributes_aggregation.keys())[0] assert dict(aggr_key) == {"method": "GET"} - def test_consume_measurement(self): instrument1 = Mock(name="instrument1") instrument1.instrumentation_scope = self.mock_instrumentation_scope From 15256cc6d09c146ed84e831bcc68a402fadff5a3 Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:01:33 +0000 Subject: [PATCH 03/16] Updated changelog fragment file --- .changelog/5332.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/5332.fixed diff --git a/.changelog/5332.fixed b/.changelog/5332.fixed new file mode 100644 index 00000000000..52ddb1af26f --- /dev/null +++ b/.changelog/5332.fixed @@ -0,0 +1 @@ +Added support for parameter exclude_attribute_keys in View From 65a652b29a92a023e45f0a90f3970310b10867de Mon Sep 17 00:00:00 2001 From: saurabh-saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:40:25 +0530 Subject: [PATCH 04/16] Update .changelog/5332.fixed Co-authored-by: Riccardo Magliocchetti --- .changelog/5332.fixed | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changelog/5332.fixed b/.changelog/5332.fixed index 52ddb1af26f..d8e9a2dd664 100644 --- a/.changelog/5332.fixed +++ b/.changelog/5332.fixed @@ -1 +1 @@ -Added support for parameter exclude_attribute_keys in View +`opentelemetry-sdk`: add support for parameter `exclude_attribute_keys` in `View` From 498b25b1c77b12a8a556c792483e1d4f791af0ef Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Thu, 2 Jul 2026 10:52:26 +0000 Subject: [PATCH 05/16] Renamed changelog fragment --- .changelog/{5332.fixed => 5332.added} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{5332.fixed => 5332.added} (100%) diff --git a/.changelog/5332.fixed b/.changelog/5332.added similarity index 100% rename from .changelog/5332.fixed rename to .changelog/5332.added From 39ae4d8910074c95f852f4607a6d339fd5e774d6 Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:59:11 +0000 Subject: [PATCH 06/16] Incorporated review comments --- .../opentelemetry/configuration/_meter_provider.py | 1 + .../sdk/metrics/_internal/_view_instrument_match.py | 11 +++++------ .../src/opentelemetry/sdk/metrics/_internal/view.py | 10 ++++++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py index 8943eb974cd..4a907e7ce87 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py @@ -239,6 +239,7 @@ def _create_view(config: ViewConfig) -> View: ) attribute_keys: set[str] | None = None + exclude_attribute_keys: set[str] | None = None if stream.attribute_keys is not None: if stream.attribute_keys.excluded is not None: exclude_attribute_keys = set(stream.attribute_keys.excluded) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 209caac367b..253c97252f9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -99,12 +99,11 @@ def consume_measurement( attributes = {} if self._view._exclude_attribute_keys is not None: - if attributes is not None: - attributes = { - key: value - for key, value in attributes.items() - if key not in self._view._exclude_attribute_keys - } + attributes = { + key: value + for key, value in attributes.items() + if key not in self._view._exclude_attribute_keys + } aggr_key = frozenset(attributes.items()) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index 3e332009fa6..7d8d33f51c3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -154,12 +154,18 @@ def __init__( self._meter_schema_url = meter_schema_url self._description = description - self._attribute_keys = attribute_keys + self._attribute_keys = ( + frozenset(attribute_keys) if attribute_keys is not None else None + ) 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 + self._exclude_attribute_keys = ( + frozenset(exclude_attribute_keys) + if exclude_attribute_keys is not None + else None + ) # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches From 3d15f96e48c799d64b32c38eee410af522571e8e Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:13:51 +0000 Subject: [PATCH 07/16] Updated for comment related to assertions --- .../metrics/test_view_instrument_match.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index e0481090e60..dc2e033c4f8 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -114,9 +114,17 @@ def test_view_instrument_match_exclude_attribute_keys_affects_aggregation( match.consume_measurement(measurement1) match.consume_measurement(measurement2) - assert len(match._attributes_aggregation) == 1 + + self.assertEqual( + len(match._attributes_aggregation), + 1, + ) + aggr_key = list(match._attributes_aggregation.keys())[0] - assert dict(aggr_key) == {"method": "GET"} + self.assertDictEqual( + dict(aggr_key), + {"method": "GET"}, + ) def test_view_instrument_match_exclude_removes_attributes(self): instrument1 = Mock(name="instrument1") @@ -146,7 +154,10 @@ def test_view_instrument_match_exclude_removes_attributes(self): ) match.consume_measurement(measurement) aggr_key = list(match._attributes_aggregation.keys())[0] - assert "user_id" not in dict(aggr_key) + self.assertNotIn( + "user_id", + dict(aggr_key), + ) def test_view_instrument_match_include_then_exclude(self): instrument1 = Mock(name="instrument1") @@ -177,7 +188,10 @@ def test_view_instrument_match_include_then_exclude(self): ) match.consume_measurement(measurement) aggr_key = list(match._attributes_aggregation.keys())[0] - assert dict(aggr_key) == {"method": "GET"} + self.assertDictEqual( + dict(aggr_key), + {"method": "GET"}, + ) def test_consume_measurement(self): instrument1 = Mock(name="instrument1") From e70b8327ee034c0460278820b420d6b71899e141 Mon Sep 17 00:00:00 2001 From: saurabh-saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Wed, 15 Jul 2026 11:05:59 +0530 Subject: [PATCH 08/16] Updated measurement based on exclude attributes Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../sdk/metrics/_internal/_view_instrument_match.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 253c97252f9..4bc91745dd8 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -98,13 +98,21 @@ def consume_measurement( else: attributes = {} - if self._view._exclude_attribute_keys is not None: + if self._view._exclude_attribute_keys: attributes = { key: value for key, value in attributes.items() if key not in self._view._exclude_attribute_keys } + if self._view._attribute_keys is not None or self._view._exclude_attribute_keys: + measurement = Measurement( + value=measurement.value, + time_unix_nano=measurement.time_unix_nano, + instrument=measurement.instrument, + context=measurement.context, + attributes=attributes, + ) aggr_key = frozenset(attributes.items()) if aggr_key not in self._attributes_aggregation: From 7a36cb9a9459a0dbb831438c1e76aab1a533d4dd Mon Sep 17 00:00:00 2001 From: saurabh-saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:11:19 +0530 Subject: [PATCH 09/16] Apply suggestion from @herin049 Apply suggestion from @herin049 for making attribute_keys and exclude_attribute_keys as list Co-authored-by: Lukas Hering <40302054+herin049@users.noreply.github.com> --- .../src/opentelemetry/configuration/_meter_provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py index 4a907e7ce87..2af28382cba 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py @@ -238,8 +238,8 @@ def _create_view(config: ViewConfig) -> View: f"Unknown instrument type: {selector.instrument_type!r}" ) - attribute_keys: set[str] | None = None - exclude_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 is not None: exclude_attribute_keys = set(stream.attribute_keys.excluded) From 6ae52638c5223e6d75a9d42915efd0e7ac054d97 Mon Sep 17 00:00:00 2001 From: saurabh-saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:12:39 +0530 Subject: [PATCH 10/16] Apply suggestion from @herin049 For considering for not considering exclude_attribute_keys as set Co-authored-by: Lukas Hering <40302054+herin049@users.noreply.github.com> --- .../src/opentelemetry/configuration/_meter_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py index 2af28382cba..0f1e9fdf774 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py @@ -242,7 +242,7 @@ def _create_view(config: ViewConfig) -> View: exclude_attribute_keys: list[str] | None = None if stream.attribute_keys is not None: if stream.attribute_keys.excluded is not None: - exclude_attribute_keys = set(stream.attribute_keys.excluded) + exclude_attribute_keys = stream.attribute_keys.excluded if stream.attribute_keys.included is not None: attribute_keys = set(stream.attribute_keys.included) From 8a8d139758d038c066861f17a2c2302a7696b820 Mon Sep 17 00:00:00 2001 From: saurabh-saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:13:46 +0530 Subject: [PATCH 11/16] Apply suggestion from @herin049 Not considering attribute_keys as set Co-authored-by: Lukas Hering <40302054+herin049@users.noreply.github.com> --- .../src/opentelemetry/configuration/_meter_provider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py index 0f1e9fdf774..cb5b793c29f 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py @@ -244,7 +244,7 @@ def _create_view(config: ViewConfig) -> View: 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: From 0172b1377609268c9f5ee902efc01d015f2249bc Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:50:24 +0000 Subject: [PATCH 12/16] Incorporated review comments related type of attribute_keys, exclude_attribute_keys, raising of Exception if there is overlap between include attribute and exclude attribute --- .../configuration/_meter_provider.py | 4 ++-- .../metrics/_internal/_view_instrument_match.py | 5 ++++- .../opentelemetry/sdk/metrics/_internal/view.py | 15 ++++++++++++--- opentelemetry-sdk/tests/metrics/test_view.py | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py index cb5b793c29f..a25365ce68d 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_meter_provider.py @@ -238,8 +238,8 @@ def _create_view(config: ViewConfig) -> View: f"Unknown instrument type: {selector.instrument_type!r}" ) -attribute_keys: list[str] | None = None -exclude_attribute_keys: list[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 is not None: exclude_attribute_keys = stream.attribute_keys.excluded diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index 4bc91745dd8..d98462b593a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -105,7 +105,10 @@ def consume_measurement( if key not in self._view._exclude_attribute_keys } - if self._view._attribute_keys is not None or self._view._exclude_attribute_keys: + if ( + self._view._attribute_keys is not None + or self._view._exclude_attribute_keys + ): measurement = Measurement( value=measurement.value, time_unix_nano=measurement.time_unix_nano, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index 7d8d33f51c3..c3ec2fc4c12 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 -from collections.abc import Callable +from collections.abc import Callable, Iterable from fnmatch import fnmatch from logging import getLogger @@ -108,14 +108,14 @@ 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: set[str] | None = None, + exclude_attribute_keys: Iterable[str] | None = None, ): if ( instrument_type @@ -143,6 +143,15 @@ def __init__( "characters in instrument_name" ) + if attribute_keys is not None and exclude_attribute_keys is not None: + overlap = set(attribute_keys).intersection(exclude_attribute_keys) + + if overlap: + raise Exception( + "attribute_keys and exclude_attribute_keys " + f"must be disjoint. Overlapping keys: " + f"{sorted(overlap)}" + ) # _name, _description, _aggregation, _exemplar_reservoir_factory and # _attribute_keys will be accessed when instantiating a _ViewInstrumentMatch. self._name = name diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index 03914c99c6f..671f186cc56 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -105,3 +105,20 @@ def test_additive_criteria(self): def test_view_name(self): with self.assertRaises(Exception): View(name="name", instrument_name="instrument_name*") + + def test_attribute_keys_and_exclude_attribute_keys_overlap(self): + with self.assertRaises(Exception): + View( + instrument_name="instrument_name", + attribute_keys=("method", "status_code"), + exclude_attribute_keys=("method", "user_id"), + ) + + 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) From 70da89a2eeced6a74b2b08af4a109928a335a733 Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:08:25 +0000 Subject: [PATCH 13/16] Based on review comment from Herin, refactored the code --- .../sdk/metrics/_internal/_view_instrument_match.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index d98462b593a..d6b6d68084b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -93,17 +93,16 @@ def consume_measurement( for key, value in (measurement.attributes or {}).items(): if key in self._view._attribute_keys: attributes[key] = value - elif measurement.attributes is not None: - attributes = dict(measurement.attributes) - else: - attributes = {} - - if self._view._exclude_attribute_keys: + elif self._view._exclude_attribute_keys: attributes = { key: value - for key, value in attributes.items() + for key, value in (measurement.attributes or {}).items() if key not in self._view._exclude_attribute_keys } + elif measurement.attributes is not None: + attributes = dict(measurement.attributes) + else: + attributes = {} if ( self._view._attribute_keys is not None From 7251a05117a8fc1362a227be00c368a1a83dd6b6 Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Thu, 30 Jul 2026 09:57:25 +0000 Subject: [PATCH 14/16] Incorporated review comments related to testcase and cloning of measurment --- .../_internal/_view_instrument_match.py | 11 -- .../metrics/test_view_instrument_match.py | 153 ++++++------------ 2 files changed, 50 insertions(+), 114 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py index d6b6d68084b..acb79f20d9b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/_view_instrument_match.py @@ -104,17 +104,6 @@ def consume_measurement( else: attributes = {} - if ( - self._view._attribute_keys is not None - or self._view._exclude_attribute_keys - ): - measurement = Measurement( - value=measurement.value, - time_unix_nano=measurement.time_unix_nano, - instrument=measurement.instrument, - context=measurement.context, - attributes=attributes, - ) aggr_key = frozenset(attributes.items()) if aggr_key not in self._attributes_aggregation: diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index dc2e033c4f8..8897c2cafa5 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -74,123 +74,70 @@ def setUpClass(cls): views=[], ) - def test_view_instrument_match_exclude_attribute_keys_affects_aggregation( - self, - ): + def test_consume_measurement_with_exclude_attribute_keys(self): instrument1 = Mock(name="instrument1") instrument1.instrumentation_scope = self.mock_instrumentation_scope - mock_aggregation = MagicMock() - mock_aggregation._create_aggregation.return_value = MagicMock() - - instrument_class_aggregation = MagicMock() - instrument_class_aggregation.__getitem__.return_value = ( - mock_aggregation - ) - - view = View( - instrument_name="instrument1", - exclude_attribute_keys={"user_id"}, - ) - match = _ViewInstrumentMatch( - view, - instrument=instrument1, - instrument_class_aggregation=instrument_class_aggregation, - ) - measurement1 = Measurement( - value=1, - time_unix_nano=time_ns(), - instrument=instrument1, - context=Context(), - attributes={"method": "GET", "user_id": "u1"}, - ) - measurement2 = Measurement( - value=2, - time_unix_nano=time_ns(), + # exclude_attribute_keys should remove excluded attributes + view_instrument_match = _ViewInstrumentMatch( + view=View( + instrument_name="instrument1", + name="name", + aggregation=self.mock_aggregation_factory, + exclude_attribute_keys={"f"}, + ), instrument=instrument1, - context=Context(), - attributes={"method": "GET", "user_id": "u2"}, + instrument_class_aggregation=MagicMock( + **{"__getitem__.return_value": DefaultAggregation()} + ), ) - match.consume_measurement(measurement1) - match.consume_measurement(measurement2) - - self.assertEqual( - len(match._attributes_aggregation), - 1, + view_instrument_match.consume_measurement( + Measurement( + value=0, + time_unix_nano=time_ns(), + instrument=instrument1, + context=Context(), + attributes={"c": "d", "f": "g"}, + ) ) - aggr_key = list(match._attributes_aggregation.keys())[0] - self.assertDictEqual( - dict(aggr_key), - {"method": "GET"}, + self.assertEqual( + view_instrument_match._attributes_aggregation, + { + frozenset([("c", "d")]): self.mock_created_aggregation, + }, ) - def test_view_instrument_match_exclude_removes_attributes(self): - instrument1 = Mock(name="instrument1") - instrument1.instrumentation_scope = self.mock_instrumentation_scope - mock_aggregation = MagicMock() - mock_aggregation._create_aggregation.return_value = MagicMock() - - instrument_class_aggregation = MagicMock() - instrument_class_aggregation.__getitem__.return_value = ( - mock_aggregation - ) - view = View( - instrument_name="instrument1", - exclude_attribute_keys={"user_id"}, - ) - match = _ViewInstrumentMatch( - view, - instrument=instrument1, - instrument_class_aggregation=instrument_class_aggregation, - ) - measurement = Measurement( - value=1, - time_unix_nano=time_ns(), + # None measurement attributes should result in empty attributes + view_instrument_match = _ViewInstrumentMatch( + view=View( + instrument_name="instrument1", + name="name", + aggregation=self.mock_aggregation_factory, + exclude_attribute_keys={"f"}, + ), instrument=instrument1, - context=Context(), - attributes={"method": "GET", "user_id": "u1"}, - ) - match.consume_measurement(measurement) - aggr_key = list(match._attributes_aggregation.keys())[0] - self.assertNotIn( - "user_id", - dict(aggr_key), + instrument_class_aggregation=MagicMock( + **{"__getitem__.return_value": DefaultAggregation()} + ), ) - def test_view_instrument_match_include_then_exclude(self): - instrument1 = Mock(name="instrument1") - instrument1.instrumentation_scope = self.mock_instrumentation_scope - mock_aggregation = MagicMock() - mock_aggregation._create_aggregation.return_value = MagicMock() - - instrument_class_aggregation = MagicMock() - instrument_class_aggregation.__getitem__.return_value = ( - mock_aggregation - ) - view = View( - instrument_name="instrument1", - attribute_keys={"method", "user_id"}, - exclude_attribute_keys={"user_id"}, - ) - match = _ViewInstrumentMatch( - view, - instrument=instrument1, - instrument_class_aggregation=instrument_class_aggregation, + view_instrument_match.consume_measurement( + Measurement( + value=0, + time_unix_nano=time_ns(), + instrument=instrument1, + context=Context(), + attributes=None, + ) ) - measurement = Measurement( - value=1, - time_unix_nano=time_ns(), - instrument=instrument1, - context=Context(), - attributes={"method": "GET", "user_id": "u1", "x": "y"}, - ) - match.consume_measurement(measurement) - aggr_key = list(match._attributes_aggregation.keys())[0] - self.assertDictEqual( - dict(aggr_key), - {"method": "GET"}, + + self.assertEqual( + view_instrument_match._attributes_aggregation, + { + frozenset(): self.mock_created_aggregation, + }, ) def test_consume_measurement(self): From de4ed2c4def3db387f582a6d1f370c9c6eb32505 Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:13:28 +0000 Subject: [PATCH 15/16] Incorporated review comment from lzchen by materializing the iterators before working on them --- .../opentelemetry/sdk/metrics/_internal/view.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index c3ec2fc4c12..2e7eb2f9ef3 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -91,7 +91,7 @@ class 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. Applied after ``attribute_keys`` if both are provided. + the metric stream. This class is not intended to be subclassed by the user. @@ -142,11 +142,19 @@ def __init__( f"View {name} declared with wildcard " "characters in instrument_name" ) - + attribute_keys = ( + set(attribute_keys) if attribute_keys is not None else None + ) + exclude_attribute_keys = ( + set(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 = set(attribute_keys).intersection(exclude_attribute_keys) + overlap = attribute_keys.intersection(exclude_attribute_keys) if overlap: + # pylint: disable=broad-exception-raised raise Exception( "attribute_keys and exclude_attribute_keys " f"must be disjoint. Overlapping keys: " From f29dd82e000ff4f19e1ecb1527bcd20311e1577b Mon Sep 17 00:00:00 2001 From: Saurabh Saraswat <85618497+saurabh-saraswat@users.noreply.github.com> Date: Mon, 24 Aug 2026 08:55:51 +0000 Subject: [PATCH 16/16] Corrected header inclusion --- .../tests/test_meter_provider.py | 4 +-- .../sdk/metrics/_internal/view.py | 35 +++++-------------- .../metrics/test_view_instrument_match.py | 8 ++--- 3 files changed, 11 insertions(+), 36 deletions(-) diff --git a/opentelemetry-configuration/tests/test_meter_provider.py b/opentelemetry-configuration/tests/test_meter_provider.py index bf705749f03..6904062b41b 100644 --- a/opentelemetry-configuration/tests/test_meter_provider.py +++ b/opentelemetry-configuration/tests/test_meter_provider.py @@ -791,9 +791,7 @@ def test_stream_attribute_keys_included(self): self.assertEqual(view._attribute_keys, {"key1", "key2"}) def test_stream_attribute_keys_excluded_is_applied(self): - config = self._make_view_config( - stream_kwargs={"attribute_keys": IncludeExclude(excluded=["key1"])} - ) + config = self._make_view_config(stream_kwargs={"attribute_keys": IncludeExclude(excluded=["key1"])}) meter_provider = create_meter_provider(config) views = meter_provider._sdk_config.views self.assertEqual(len(views), 1) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index f1b12980aee..05f8de2ae8f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -3,7 +3,7 @@ from collections.abc import Callable, Iterable -from fnmatch import fnmatch +from fnmatch import fnmatchcase from logging import getLogger from opentelemetry.metrics import Instrument @@ -128,27 +128,16 @@ def __init__( if name is not None and instrument_name is not None and ("*" in instrument_name or "?" in instrument_name): # pylint: disable=broad-exception-raised - raise Exception( - f"View {name} declared with wildcard " - "characters in instrument_name" - ) - attribute_keys = ( - set(attribute_keys) if attribute_keys is not None else None - ) - exclude_attribute_keys = ( - set(exclude_attribute_keys) - if exclude_attribute_keys is not None - else None - ) + raise Exception(f"View {name} declared with wildcard characters in instrument_name") + attribute_keys = set(attribute_keys) if attribute_keys is not None else None + exclude_attribute_keys = set(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_keys.intersection(exclude_attribute_keys) if overlap: # pylint: disable=broad-exception-raised raise Exception( - "attribute_keys and exclude_attribute_keys " - f"must be disjoint. Overlapping keys: " - f"{sorted(overlap)}" + f"attribute_keys and exclude_attribute_keys must be disjoint. Overlapping keys: {sorted(overlap)}" ) # _name, _description, _aggregation, _exemplar_reservoir_factory and # _attribute_keys will be accessed when instantiating a _ViewInstrumentMatch. @@ -161,18 +150,10 @@ def __init__( self._meter_schema_url = meter_schema_url self._description = description - self._attribute_keys = ( - frozenset(attribute_keys) if attribute_keys is not None else None - ) + self._attribute_keys = frozenset(attribute_keys) if attribute_keys is not None else None self._aggregation = aggregation or self._default_aggregation - self._exemplar_reservoir_factory = ( - exemplar_reservoir_factory or _default_reservoir_factory - ) - self._exclude_attribute_keys = ( - frozenset(exclude_attribute_keys) - if exclude_attribute_keys is not None - else None - ) + self._exemplar_reservoir_factory = exemplar_reservoir_factory or _default_reservoir_factory + self._exclude_attribute_keys = frozenset(exclude_attribute_keys) if exclude_attribute_keys is not None else None # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 05d5c5ed62d..c66dacf6d52 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -86,9 +86,7 @@ def test_consume_measurement_with_exclude_attribute_keys(self): exclude_attribute_keys={"f"}, ), instrument=instrument1, - instrument_class_aggregation=MagicMock( - **{"__getitem__.return_value": DefaultAggregation()} - ), + instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) view_instrument_match.consume_measurement( @@ -117,9 +115,7 @@ def test_consume_measurement_with_exclude_attribute_keys(self): exclude_attribute_keys={"f"}, ), instrument=instrument1, - instrument_class_aggregation=MagicMock( - **{"__getitem__.return_value": DefaultAggregation()} - ), + instrument_class_aggregation=MagicMock(**{"__getitem__.return_value": DefaultAggregation()}), ) view_instrument_match.consume_measurement(