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)