Skip to content

feat: cap span attributes at 128 keys - #38

Merged
bengl merged 1 commit into
nodejs:mainfrom
bengl:bengl/attribute-limits
Nov 4, 2026
Merged

bengl merged 1 commit into
nodejs:mainfrom
bengl:bengl/attribute-limits

Conversation

@bengl

@bengl bengl commented Nov 3, 2026

Copy link
Copy Markdown
Member

setAttribute() took any number of distinct keys, so one span could grow without bound. A span now keeps at most 128 distinct attributes, the OpenTelemetry SDK's default attributeCountLimit. A write to a key the span already has always lands. A new key past the cap is dropped and counted, and the count exports as the span's droppedAttributesCount when it's above zero.

Edge cases. These follow the SDK wherever microtel's setAttribute() lets a value through. A key counts toward the cap while it holds a value, since undefined values are never exported. Writing undefined to a key frees its slot, and dropping it counts nothing. A write of null lands and counts, since microtel exports it as a string.

Keeping it cheap. setAttribute() runs about ten times per span, so it can't afford a lookup per key on every call. A span below the cap only counts its writes, since distinct keys can never outnumber them. Once writes reach the cap, the same private field holds the exact key count and the dropped count, so an ordinary span initializes one field for the cap. A per-key check on every write cost 29% in the span benchmark, and three separate fields cost about twice as much on server spans as one. Past the cap, the field changes from a number to an object. In a process where one span went past the cap first, an ordinary span still cost the same, 88.1 ns against 87.4 ns, where three fields cost 89.5 ns and main 80.8 ns.

Performance. benchmark/span.bench.js on Node 26.12.1, 8 interleaved runs against main on a quiet machine:

Benchmark main This PR Change
client span construction 12.91M ops/s 11.80M ops/s -8.6%
server span from a traceparent 5.85M ops/s 5.68M ops/s -2.9%

That's about 7 ns per span, or roughly 0.04% of a traced HTTP request, which makes two spans.

Raw samples, M ops/s
  • client span construction, main: 12.62, 12.41, 12.99, 13.00, 12.87, 12.69, 13.02, 12.95
  • client span construction, this PR: 11.93, 12.12, 11.83, 11.73, 12.00, 11.58, 11.54, 11.77
  • server span from a traceparent, main: 5.52, 5.94, 6.05, 5.85, 5.93, 5.85, 5.72, 5.63
  • server span from a traceparent, this PR: 5.66, 5.67, 5.60, 5.69, 5.71, 5.73, 5.72, 5.58

Not taken.

  • Caps on event attributes and on value length. microtel writes its own events, and the SDK has no value length limit by default.
  • Counting only writes from outside microtel's instrumentations, which use a fixed set of keys. That would cost nothing on microtel's own writes, but every caller would have to say which kind of write it makes.

Tests. 200 distinct keys export 128 attributes and a droppedAttributesCount of 72. A write to an existing key lands past the cap, a cleared key frees its slot for itself or a new key, many writes to one key switch to exact counting correctly with a null value holding a slot, and a span under the cap exports no droppedAttributesCount.

Previously, setAttribute accepted any number of distinct keys, so a
span could grow without bound. Now a span keeps at most 128 distinct
attributes, the OpenTelemetry SDK's default attributeCountLimit.

A write to a key the span already has always lands. A new key past
the cap is dropped and counted, and the count is exported with the
span as the droppedAttributesCount OTLP field. Writing undefined to a
key frees its slot.

Spans below the cap track only the number of setAttribute calls,
since distinct keys can never exceed the number of writes. Once writes
reach the cap, the same field holds the exact key count and the
dropped count.

Signed-off-by: Bryan English <bryan@bryanenglish.com>
@bengl
bengl marked this pull request as ready for review November 3, 2026 19:52
@bengl
bengl requested a review from a team November 3, 2026 19:52

@AbhiPrasad AbhiPrasad left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we won't let this be configurable?

@bengl

bengl commented Nov 4, 2026

Copy link
Copy Markdown
Member Author

@AbhiPrasad I'll do that in another PR, since you've already approved this one.

@bengl
bengl merged commit 6ed02f6 into nodejs:main Nov 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants