From e72081287dcd1fb288b715e88253abe558e0c954 Mon Sep 17 00:00:00 2001 From: tejasae-afk Date: Wed, 15 Apr 2026 08:49:39 -0400 Subject: [PATCH 1/2] sdk/metrics: copy attributes dict in consume_measurement to prevent mutation When a caller retains a reference to the attributes dict passed to counter.add() (or any instrument record/add call), mutating that dict after the call would silently corrupt the attributes stored on the aggregation and subsequently on exported data points. The fix copies the dict at the point where it is first stored as the canonical attributes for a new aggregation bucket, so downstream mutations by the caller have no effect. Fixes #4610 --- CHANGELOG.md | 2 + .../_internal/_view_instrument_match.py | 2 +- .../metrics/test_view_instrument_match.py | 45 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a3c35150..261694d466 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-sdk`: Fix mutable attributes reference in metrics: attributes passed to instrument `add`/`record` are now copied so that subsequent mutations to the caller's dict do not affect recorded data points + ([#4610](https://github.com/open-telemetry/opentelemetry-python/issues/4610)) - Apply fixes for `UP` ruff rule ([#5133](https://github.com/open-telemetry/opentelemetry-python/pull/5133)) - Switch to SPDX license headers and add CI enforcement 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 7e5eef49b9..d9ba05363b 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 @@ -94,7 +94,7 @@ def consume_measurement( if key in self._view._attribute_keys: attributes[key] = value elif measurement.attributes is not None: - attributes = measurement.attributes + attributes = dict(measurement.attributes) else: attributes = {} diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py index 729a04486f..5be1c7efa9 100644 --- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py +++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py @@ -255,6 +255,51 @@ def test_collect(self): self.assertEqual(number_data_point.attributes, {"c": "d"}) self.assertEqual(number_data_point.value, 0) + def test_consume_measurement_attributes_are_copied(self): + """Mutating the attributes dict after recording must not affect stored data points.""" + instrument1 = _Counter( + "instrument1", + Mock(), + Mock(), + description="description", + unit="unit", + ) + instrument1.instrumentation_scope = self.mock_instrumentation_scope + view_instrument_match = _ViewInstrumentMatch( + view=View( + instrument_name="instrument1", + name="name", + aggregation=DefaultAggregation(), + ), + instrument=instrument1, + instrument_class_aggregation=MagicMock( + **{"__getitem__.return_value": DefaultAggregation()} + ), + ) + + attributes = {"key": "original"} + view_instrument_match.consume_measurement( + Measurement( + value=1, + time_unix_nano=time_ns(), + instrument=instrument1, + context=Context(), + attributes=attributes, + ) + ) + + # Mutate the original dict after recording + attributes["key"] = "mutated" + + number_data_points = view_instrument_match.collect( + AggregationTemporality.CUMULATIVE, 0 + ) + number_data_points = list(number_data_points) + self.assertEqual(len(number_data_points), 1) + self.assertEqual( + number_data_points[0].attributes, {"key": "original"} + ) + @patch( "opentelemetry.sdk.metrics._internal._view_instrument_match.time_ns", side_effect=[0, 1, 2], From 24b14a6390d9146455e1f14f0f7bf169b9420f19 Mon Sep 17 00:00:00 2001 From: Tejas Date: Fri, 17 Apr 2026 21:32:20 -0400 Subject: [PATCH 2/2] fix changelog link to point to PR instead of issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 261694d466..33aac94681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased - `opentelemetry-sdk`: Fix mutable attributes reference in metrics: attributes passed to instrument `add`/`record` are now copied so that subsequent mutations to the caller's dict do not affect recorded data points - ([#4610](https://github.com/open-telemetry/opentelemetry-python/issues/4610)) + ([#5106](https://github.com/open-telemetry/opentelemetry-python/pull/5106)) - Apply fixes for `UP` ruff rule ([#5133](https://github.com/open-telemetry/opentelemetry-python/pull/5133)) - Switch to SPDX license headers and add CI enforcement