Spec requirement
Spec: W3C Trace Context, reached through OpenTelemetry Specification v1.60.0
Requirement level: MUST (truncate whole entries) and SHOULD (the rest)
Pinned source: https://www.w3.org/TR/trace-context/#tracestate-limits
TraceState is an API type (specification/trace/api.md:278), and the spec delegates its rules rather than restating them:
These operations MUST follow the rules described in the W3C Trace Context specification.
specification/trace/api.md:289.
W3C §3.3.1.5 then says:
Vendors SHOULD propagate at least 512 characters of a combined header.
the vendor MUST truncate whole entries. Entries larger than 128 characters long SHOULD be removed first. Then entries SHOULD be removed starting from the end of tracestate.
The problem
We implement one third of the truncation procedure, and we drive it off the wrong trigger.
| Rule |
Level |
Implemented |
| Truncate whole entries |
MUST |
yes |
| Entries over 128 characters removed first |
SHOULD |
no |
| Then remove starting from the end |
SHOULD |
yes |
| Propagate at least 512 characters of a combined header |
SHOULD |
no |
There is no 512 and no 128-character handling anywhere in lib/. What we do instead is cap the member count at 32 (W3C §3.3.1.1, "There can be a maximum of 32 list-members in a list") and drop from the end when a put would exceed it.
The 32 cap is correct on its own terms, but it is a grammar limit on the list, not the truncation procedure. A tracestate with 32 short members can still exceed 512 characters and go out oversized. And when something does have to go, we drop a small oldest entry while keeping a 300-character one that the spec says should have gone first.
"Combined header" here means the tracestate header value itself, including the case where it arrives split across repeated header instances and is recombined. It is not traceparent plus tracestate. So this is measurable from TraceState alone and does not need any propagator context.
Offending code
The member cap is enforced in two places and the character budget in neither:
lib/src/api/trace/trace_state.dart:43 (fromString)
lib/src/api/trace/trace_state.dart:95 (put)
toString() at lib/src/api/trace/trace_state.dart serializes with no length check:
String toString() {
return _entries.entries.map((e) => '${e.key}=${e.value}').join(',');
}
How to correct it
This belongs here rather than in a propagator. The 32-member cap already lives in this class, toString() is the serializer, and every propagator that emits a tracestate header should get the same behavior without reimplementing it. See dartastic_opentelemetry#295 for the SDK half, where the W3C propagator currently duplicates this serializer instead of calling it.
- Apply §3.3.1.5 when producing the header value: remove whole entries only, entries over 128 characters first, then from the end.
- Keep the existing 32-member cap. It is a separate rule and it is correct.
- Decide whether
toString() truncates or a dedicated method does. A getter that silently drops data is surprising, so a separate method the propagator calls may be the better shape. Worth settling in this issue before implementation.
- Report dropped entries through
OTelErrorHandling, matching how invalid entries are handled today.
Open question worth resolving here: 512 is a floor on what vendors should propagate, not a ceiling we are obliged to impose. Truncating to 512 is the conservative reading. Whether the budget should be configurable is undecided.
Notes
Spec requirement
Spec: W3C Trace Context, reached through OpenTelemetry Specification v1.60.0
Requirement level: MUST (truncate whole entries) and SHOULD (the rest)
Pinned source: https://www.w3.org/TR/trace-context/#tracestate-limits
TraceStateis an API type (specification/trace/api.md:278), and the spec delegates its rules rather than restating them:specification/trace/api.md:289.W3C §3.3.1.5 then says:
The problem
We implement one third of the truncation procedure, and we drive it off the wrong trigger.
There is no
512and no 128-character handling anywhere inlib/. What we do instead is cap the member count at 32 (W3C §3.3.1.1, "There can be a maximum of 32list-members in alist") and drop from the end when aputwould exceed it.The 32 cap is correct on its own terms, but it is a grammar limit on the list, not the truncation procedure. A
tracestatewith 32 short members can still exceed 512 characters and go out oversized. And when something does have to go, we drop a small oldest entry while keeping a 300-character one that the spec says should have gone first."Combined header" here means the
tracestateheader value itself, including the case where it arrives split across repeated header instances and is recombined. It is not traceparent plus tracestate. So this is measurable fromTraceStatealone and does not need any propagator context.Offending code
The member cap is enforced in two places and the character budget in neither:
lib/src/api/trace/trace_state.dart:43(fromString)lib/src/api/trace/trace_state.dart:95(put)toString()atlib/src/api/trace/trace_state.dartserializes with no length check:How to correct it
This belongs here rather than in a propagator. The 32-member cap already lives in this class,
toString()is the serializer, and every propagator that emits atracestateheader should get the same behavior without reimplementing it. See dartastic_opentelemetry#295 for the SDK half, where the W3C propagator currently duplicates this serializer instead of calling it.toString()truncates or a dedicated method does. A getter that silently drops data is surprising, so a separate method the propagator calls may be the better shape. Worth settling in this issue before implementation.OTelErrorHandling, matching how invalid entries are handled today.Open question worth resolving here: 512 is a floor on what vendors should propagate, not a ceiling we are obliged to impose. Truncating to 512 is the conservative reading. Whether the budget should be configurable is undecided.
Notes