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
1 change: 1 addition & 0 deletions .changelog/5332.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for parameter exclude_attribute_keys in View
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,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)

Expand All @@ -249,6 +246,7 @@ def _create_view(config: ViewConfig) -> View:
description=stream.description,
attribute_keys=attribute_keys,
aggregation=aggregation,
exclude_attribute_keys=exclude_attribute_keys,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""

Expand All @@ -109,6 +115,7 @@ def __init__(
]
| None = None,
instrument_unit: str | None = None,
exclude_attribute_keys: set[str] | None = None,
):
if (
instrument_type
Expand Down Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions opentelemetry-sdk/tests/_configuration/test_meter_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,15 +776,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.sdk._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(
Expand Down
105 changes: 105 additions & 0 deletions opentelemetry-sdk/tests/metrics/test_view_instrument_match.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,111 @@ def setUpClass(cls):
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")
instrument1.instrumentation_scope = self.mock_instrumentation_scope
Expand Down