open-telemetry / open-telemetry/opentelemetry-python

BoundedAttributes: _clean_extended_attribute returns None for both invalid input and valid AnyValue(None), causing invalid attributes to be written instead of dropped

Open Beginner friendly
#5,432 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
2.6k
Forks
1k
Avg merge
4d 15h
Merged PRs (30d)
19

Description

Bug Description

_clean_extended_attribute() in opentelemetry-api/src/opentelemetry/attributes/__init__.py uses None as a dual-purpose return value:

  1. Sentinel for invalid input — returned when the key is empty/non-string, or the value cannot be serialized as an AnyValue.
  2. Valid attribute valueAnyValue(None) is a legitimate attribute value per the OpenTelemetry spec, and _clean_extended_attribute_value can legitimately return None when the input is None.

The non-extended path (_clean_attribute) correctly guards against this with if value is None: return immediately after the call. The extended path (extended_attributes=True) has no such guard in either __setitem__ or _set_items. As a result:

  • An invalid attribute (e.g. empty-string key "") is written into the backing dict with value None under the invalid key, instead of being silently dropped.
  • self.dropped is never incremented for the rejected attribute.

Affected Code

opentelemetry-api/src/opentelemetry/attributes/__init__.py

_clean_extended_attribute (lines ~212–229) returns None for invalid input:

if not (key and isinstance(key, str)):
    _logger.warning("invalid key `%s`. must be non-empty string.", key)
    return None

__setitem__ (lines ~284–291) — missing guard for the extended path:

if self._extended_attributes:
    value = _clean_extended_attribute(key, value, self.max_value_len)
    # ← no `if value is None: return` here
else:
    value = _clean_attribute(key, value, self.max_value_len)
    if value is None:      # ← guard exists only for non-extended path
        return

Same omission in _set_items (lines ~302–307).

Reproduction

from opentelemetry.attributes import BoundedAttributes

ba = BoundedAttributes(maxlen=10, extended_attributes=True, immutable=False)
ba[""] = "hello"

# Expect: empty-string key rejected, dropped counter incremented
assert "" not in ba, f"Invalid key '' was written into dict: {dict(ba._dict)}"
assert ba.dropped == 1, f"dropped counter not incremented: {ba.dropped}"

Actual: "" is present in ba._dict with value None, and ba.dropped == 0.

Root Cause

None is an ambiguous return value: it cannot distinguish "this input was invalid, drop it" from "this is a valid AnyValue whose value happens to be None". The non-extended path sidesteps this accidentally because _clean_attribute never accepts None as a valid attribute value, so the if value is None guard works there. The extended path does not have the same assumption.

Suggested Fix

Introduce a private sentinel:

_INVALID_ATTRIBUTE = object()

Return _INVALID_ATTRIBUTE (instead of None) from _clean_extended_attribute on invalid key or unserializable value. Then guard both __setitem__ and _set_items:

if self._extended_attributes:
    value = _clean_extended_attribute(key, value, self.max_value_len)
    if value is _INVALID_ATTRIBUTE:
        with self._lock:
            self.dropped += 1
        return

And in _set_items:

cv = _clean_extended_attribute(key, value, self.max_value_len)
if cv is _INVALID_ATTRIBUTE:
    continue

This keeps None available as a valid AnyValue while giving the caller an unambiguous signal for rejection.

Component

opentelemetry-apiopentelemetry/attributes/__init__.py

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in opentelemetry-api/src/opentelemetry/attributes/init.py, reading _clean_extended_attribute alongside BoundedAttributes.setitem and _set_items. Run the reproduction for an empty key and inspect the existing attribute tests. Done means invalid extended attributes are absent, valid AnyValue(None) remains usable, and dropped is incremented for rejected input.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.