From 6303d820691ca4fa22440f2e87245e87c0902f5f Mon Sep 17 00:00:00 2001 From: Aditya_Nalge Date: Mon, 5 Oct 2026 11:28:18 +0530 Subject: [PATCH] Fix BoundedAttributes to log drop warning only once per record The spec requires the attribute limit log to be emitted at most once per record on which an attribute is set, but a warning was logged for every dropped attribute, which floods the logs when a record exceeds its limits. Gate the warning on a per-instance flag instead of the shared `dropped` counter. `dropped` is also incremented for invalid keys and for the `maxlen == 0` case, and it is propagated by `__deepcopy__` and by the log exception merge path, so using it as an "already warned" marker could suppress the warning for records that drop attributes. Fixes #5726 Assisted-by: OpenCode Big Pickle Signed-off-by: Aditya_Nalge --- .changelog/5727.fixed | 1 + .../src/opentelemetry/attributes/__init__.py | 11 ++++++--- .../tests/attributes/test_attributes.py | 23 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 .changelog/5727.fixed diff --git a/.changelog/5727.fixed b/.changelog/5727.fixed new file mode 100644 index 00000000000..39a0bad52be --- /dev/null +++ b/.changelog/5727.fixed @@ -0,0 +1 @@ +`opentelemetry-api`: log the ``BoundedAttributes`` drop warning only once per record diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 6bc3590f0f1..b94749330ab 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -153,6 +153,9 @@ def __init__( self.dropped = 0 self.max_value_len = max_value_len self._lock = threading.Lock() + # The spec allows the limit log to be emitted at most once per record, so track + # it separately from ``dropped``, which also counts other kinds of drops. + self._dropped_logged = False # setting False before adding items self._immutable = False if attributes: @@ -203,9 +206,11 @@ def _setitem_locked(self, key: str, value: types.AnyValue) -> None: if key in self._dict: del self._dict[key] if self.maxlen is not None and len(self._dict) >= self.maxlen: - _logger.warning( - "Attributes dict is full. Dropping the oldest key-value pair from attributes to make space for the new key-value pair.", - ) + if not self._dropped_logged: + self._dropped_logged = True + _logger.warning( + "Attributes dict is full. Dropping the oldest key-value pair from attributes to make space for the new key-value pair.", + ) # Dictionaries are insertion ordered in Python, this is the recommended way to get the oldest value. del self._dict[next(iter(self._dict.keys()))] self.dropped += 1 diff --git a/opentelemetry-api/tests/attributes/test_attributes.py b/opentelemetry-api/tests/attributes/test_attributes.py index 2d9c8cb9c29..8820b3d4252 100644 --- a/opentelemetry-api/tests/attributes/test_attributes.py +++ b/opentelemetry-api/tests/attributes/test_attributes.py @@ -112,6 +112,29 @@ def test_maxlen_reached(self): self.assertEqual(bdict["second"], "another") self.assertEqual(bdict.dropped, 1) + def test_maxlen_reached_logs_warning_once(self): + bdict = BoundedAttributes(2, immutable=False) + with self.assertLogs("opentelemetry", level="WARNING") as cm: + for idx in range(5): + bdict[f"key-{idx}"] = idx + + full_warnings = [warning for warning in cm.output if "Attributes dict is full" in warning] + self.assertEqual(len(full_warnings), 1) + self.assertEqual(len(bdict), 2) + self.assertEqual(bdict.dropped, 3) + + def test_maxlen_reached_logs_warning_after_invalid_key_drop(self): + bdict = BoundedAttributes(2, immutable=False) + with self.assertLogs("opentelemetry", level="WARNING") as cm: + bdict[1] = 2 + bdict["first"] = 1 + bdict["second"] = 2 + bdict["third"] = 3 + + full_warnings = [warning for warning in cm.output if "Attributes dict is full" in warning] + self.assertEqual(len(full_warnings), 1) + self.assertEqual(bdict.dropped, 2) + def test_negative_maxlen_not_allowed(self): with self.assertRaises(ValueError): BoundedAttributes(-1)