Skip to content

Commit cfad5eb

Browse files
amdadulbarixrmxlzchen
authored
Fix TraceState.update to only update already existing keys (#5543)
* Fix TraceState.update dropping all entries at the 32-key limit TraceState.update() builds a new entry list and passes it to the constructor, which discards everything and returns an empty TraceState when the list exceeds the 32-key W3C limit. As a result, upserting a new key into a tracestate that already holds 32 entries silently wiped all 32 existing key/value pairs, contradicting update()'s documented contract that "the same tracestate will be returned". Guard the limit the way add() already does, but only for genuinely new keys, so updates to existing keys at capacity keep working. Add tests for both cases. Signed-off-by: Md. Amdadul Bari Imad <amdadulbari@gmail.com> * Preserve upsert in TraceState.update; guard only new key at 32-key limit Address review feedback: instead of restricting update() to existing keys (a breaking change for samplers relying on upsert), keep upsert behavior and only return the tracestate unchanged when adding a *new* key would exceed the 32-key maximum. Fixes the silent drop of all entries at capacity. * Document upsert behaviour of TraceState.update Note in the update() docstring that it performs an upsert and that this intentionally goes beyond the spec's update definition, as requested in review. Docstring-only change; no behavior change. * Fix precommit: format update() length guard onto one line --------- Signed-off-by: Md. Amdadul Bari Imad <amdadulbari@gmail.com> Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> Co-authored-by: Leighton Chen <lechen@microsoft.com>
1 parent ab22674 commit cfad5eb

3 files changed

Lines changed: 46 additions & 3 deletions

File tree

.changelog/5543.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-api`: fix `TraceState.update` dropping all entries when adding a new key at the 32-key limit

opentelemetry-api/src/opentelemetry/trace/span.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -314,20 +314,35 @@ def update(self, key: str, value: str) -> TraceState:
314314
"""Updates a key-value pair in tracestate. The provided pair should
315315
adhere to w3c tracestate identifiers format.
316316
317+
Note:
318+
This method performs an "upsert": if ``key`` is not already present
319+
it is added (when the tracestate is below the 32-entry limit),
320+
otherwise its value is updated. This upsert behaviour is intentional
321+
but goes beyond what the OpenTelemetry specification defines for
322+
``update`` (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#tracestate),
323+
and is kept for backwards compatibility with callers that rely on it.
324+
317325
Args:
318326
key: A valid tracestate key to update
319327
value: A valid tracestate value to update for key
320328
321329
Returns:
322330
A new TraceState with the modifications applied.
323331
324-
If the provided key-value pair is invalid or results in tracestate
325-
that violates tracecontext specification, they are discarded and
326-
same tracestate will be returned.
332+
If the provided pair is invalid, or adding a new key would exceed
333+
the maximum of 32 key/value pairs, they are discarded and the same
334+
tracestate is returned unchanged. Updating an existing key is always
335+
allowed, even at the maximum.
327336
"""
328337
if not _is_valid_pair(key, value):
329338
_logger.warning("Invalid key/value pair (%s, %s) found.", key, value)
330339
return self
340+
# Adding a new key at the maximum would push the tracestate over the
341+
# limit and cause the constructor to drop every entry. Return unchanged
342+
# instead of silently discarding existing state.
343+
if key not in self._dict and len(self._dict) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
344+
_logger.warning("There can't be more 32 key/value pairs.")
345+
return self
331346
prev_state = self._dict.copy()
332347
prev_state.pop(key, None)
333348
new_state = [(key, value), *prev_state.items()]

opentelemetry-api/tests/trace/test_tracestate.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,33 @@ def test_tracestate_update_invalid(self):
5151
self.assertNotEqual(new_state.get("a"), ",,2,,f")
5252
self.assertEqual(new_state.get("a"), "1")
5353

54+
def test_tracestate_update_new_key_added_below_capacity(self):
55+
# update() keeps its upsert behavior: a key that is not present is added
56+
# as long as there is room below the 32-entry limit.
57+
small_state = TraceState([("a", "1")])
58+
new_state = small_state.update("b", "2")
59+
self.assertEqual(new_state.get("b"), "2")
60+
self.assertEqual(new_state.get("a"), "1")
61+
62+
def test_tracestate_update_at_capacity_new_key_preserved(self):
63+
# Guards the previous bug: adding a new key at the 32-entry limit used to
64+
# push the list to 33 entries and cause the constructor to wipe them all.
65+
# Now the tracestate is returned unchanged, preserving existing entries.
66+
pairs = [(f"key{i}", f"value{i}") for i in range(32)]
67+
state = TraceState(pairs)
68+
new_state = state.update("newkey", "newvalue")
69+
self.assertEqual(len(new_state), 32)
70+
self.assertIsNone(new_state.get("newkey"))
71+
self.assertEqual(new_state.get("key0"), "value0")
72+
73+
def test_tracestate_update_existing_key_at_capacity(self):
74+
# Updating an existing key while at the limit stays within the limit.
75+
pairs = [(f"key{i}", f"value{i}") for i in range(32)]
76+
state = TraceState(pairs)
77+
new_state = state.update("key0", "changed")
78+
self.assertEqual(len(new_state), 32)
79+
self.assertEqual(new_state.get("key0"), "changed")
80+
5481
def test_tracestate_delete_preserved(self):
5582
state = TraceState([("a", "1"), ("b", "2"), ("c", "3")])
5683
new_state = state.delete("b")

0 commit comments

Comments
 (0)