Skip to content

[OTLP EXPORTER] fix OTLP recordable attribute spec compliance#4166

Draft
dbarker wants to merge 3 commits into
open-telemetry:mainfrom
dbarker:fix_otlp_attribute_spec_compliance
Draft

[OTLP EXPORTER] fix OTLP recordable attribute spec compliance#4166
dbarker wants to merge 3 commits into
open-telemetry:mainfrom
dbarker:fix_otlp_attribute_spec_compliance

Conversation

@dbarker

@dbarker dbarker commented Jun 16, 2026

Copy link
Copy Markdown
Member

Fixes # (issue)

Fixes a few non-compliant areas of the otlp attribute spec 1) keys are not enforced to be unique, 2) byte values are not supported where required (due to an allow_bytes argument in the utils) .

See:

Implementation MUST by default enforce that the exported attribute collections contain only unique keys.

An Attribute is a key-value pair, which MUST have the following properties:

The attribute key MUST be a non-null and non-empty string.
Case sensitivity of keys is preserved. Keys that differ in casing are treated as distinct keys.
The attribute value MUST be one of types defined in AnyValue.

Resources, Instrumentation Scopes, Metric points, Spans, Span Events, Span Links and Log Records, contain a collection of attributes.

Changes

  • Check if an attribute key exists and update its value on calls to SetAttribute for logs and spans
  • Support byte values for all OTLP AnyValue attributes (spans, logs, metrics, resource, ...)
  • Use a variant visitor to set the attribute values into the protobuf AnyValue message (replacing an if statement using nostd::get)
  • adds test for otlp_populate_attribute_utils

For significant contributions please make sure you have completed the following items:

  • CHANGELOG.md updated for non-trivial changes
  • Unit tests have been added
  • Changes in public API reviewed

…otlp attribute AnyValue types. Use a variant vistor instead of sequential if/else conditions to set otlp attributes. Populate otlp attribute and AnyValue tests.
@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.19858% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.84%. Comparing base (de26178) to head (88e47f6).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...xporters/otlp/src/otlp_populate_attribute_utils.cc 90.75% 10 Missing ⚠️
sdk/src/logs/multi_recordable.cc 50.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4166      +/-   ##
==========================================
+ Coverage   82.83%   82.84%   +0.01%     
==========================================
  Files         406      406              
  Lines       16913    16951      +38     
==========================================
+ Hits        14009    14041      +32     
- Misses       2904     2910       +6     
Files with missing lines Coverage Δ
...opentelemetry/exporters/otlp/otlp_log_recordable.h 100.00% <ø> (ø)
...ude/opentelemetry/exporters/otlp/otlp_recordable.h 100.00% <ø> (ø)
exporters/otlp/src/otlp_log_recordable.cc 40.13% <100.00%> (+4.08%) ⬆️
exporters/otlp/src/otlp_metric_utils.cc 92.03% <100.00%> (ø)
exporters/otlp/src/otlp_recordable.cc 91.06% <100.00%> (+0.80%) ⬆️
sdk/src/trace/span.cc 89.10% <100.00%> (+0.21%) ⬆️
sdk/src/logs/multi_recordable.cc 89.34% <50.00%> (-1.20%) ⬇️
...xporters/otlp/src/otlp_populate_attribute_utils.cc 91.72% <90.75%> (-1.23%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

const opentelemetry::common::AttributeValue &value) noexcept
{
OtlpPopulateAttributeUtils::PopulateAttribute(proto_record_.add_attributes(), key, value, true);
for (auto &attr : *proto_record_.mutable_attributes())

@dbarker dbarker Jun 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Benchmark comparison:

This check to avoid duplicating attribute keys adds overhead:

  • ~2.3% (~14ns) overhead to nominal spans with 6 attributes (OtlpSpanFixture/Nominal)
  • ~136% (10us) to spans with max (128) attributes (OtlpSpanFixture/MaxAttributes)

This can be mitigated a few ways 1) add a SetAttributes method that takes key value iterable and does not check existing keys on creation of the span, 2) do not check attribute keys during span recording and perform deduplication when creating the protobuf request message on export.

branch: dbarker:add_otlp_recordable_benchmark (#4165)


-----------------------------------------------------------------------------------
Benchmark                                         Time             CPU   Iterations
-----------------------------------------------------------------------------------
OtlpSpanFixture/Minimal_mean                    342 ns          345 ns            5
OtlpSpanFixture/Minimal_median                  343 ns          345 ns            5
OtlpSpanFixture/Minimal_stddev                 2.23 ns         1.88 ns            5
OtlpSpanFixture/Minimal_cv                     0.65 %          0.55 %             5
OtlpSpanFixture/Nominal_mean                    596 ns          598 ns            5
OtlpSpanFixture/Nominal_median                  594 ns          596 ns            5
OtlpSpanFixture/Nominal_stddev                 10.5 ns         10.6 ns            5
OtlpSpanFixture/Nominal_cv                     1.76 %          1.77 %             5
OtlpSpanFixture/MaxAttributes_mean             7517 ns         7521 ns            5 items_per_second=17.0197M/s
OtlpSpanFixture/MaxAttributes_median           7489 ns         7492 ns            5 items_per_second=17.0841M/s
OtlpSpanFixture/MaxAttributes_stddev           87.8 ns         87.8 ns            5 items_per_second=196.123k/s
OtlpSpanFixture/MaxAttributes_cv               1.17 %          1.17 %             5 items_per_second=1.15%
BM_PopulateRequest/span_count:1_mean          0.378 us        0.378 us            5 items_per_second=2.6486M/s
BM_PopulateRequest/span_count:1_median        0.378 us        0.378 us            5 items_per_second=2.64307M/s
BM_PopulateRequest/span_count:1_stddev        0.002 us        0.002 us            5 items_per_second=11.8787k/s
BM_PopulateRequest/span_count:1_cv             0.45 %          0.45 %             5 items_per_second=0.45%
BM_PopulateRequest/span_count:512_mean         25.5 us         25.5 us            5 items_per_second=20.0764M/s
BM_PopulateRequest/span_count:512_median       25.4 us         25.4 us            5 items_per_second=20.1812M/s
BM_PopulateRequest/span_count:512_stddev      0.248 us        0.248 us            5 items_per_second=194k/s
BM_PopulateRequest/span_count:512_cv           0.97 %          0.97 %             5 items_per_second=0.97%

branch: dbarker:fix_otlp_attribute_spec_compliance (this PR #4166)

-----------------------------------------------------------------------------------
Benchmark                                         Time             CPU   Iterations
-----------------------------------------------------------------------------------
OtlpSpanFixture/Minimal_mean                    340 ns          343 ns            5
OtlpSpanFixture/Minimal_median                  340 ns          344 ns            5
OtlpSpanFixture/Minimal_stddev                0.711 ns        0.795 ns            5
OtlpSpanFixture/Minimal_cv                     0.21 %          0.23 %             5
OtlpSpanFixture/Nominal_mean                    610 ns          614 ns            5
OtlpSpanFixture/Nominal_median                  607 ns          611 ns            5
OtlpSpanFixture/Nominal_stddev                 6.20 ns         6.58 ns            5
OtlpSpanFixture/Nominal_cv                     1.02 %          1.07 %             5
OtlpSpanFixture/MaxAttributes_mean            17773 ns        17776 ns            5 items_per_second=7.20295M/s
OtlpSpanFixture/MaxAttributes_median          17727 ns        17730 ns            5 items_per_second=7.21921M/s
OtlpSpanFixture/MaxAttributes_stddev            337 ns          336 ns            5 items_per_second=134.931k/s
OtlpSpanFixture/MaxAttributes_cv               1.89 %          1.89 %             5 items_per_second=1.87%
BM_PopulateRequest/span_count:1_mean          0.349 us        0.349 us            5 items_per_second=2.86195M/s
BM_PopulateRequest/span_count:1_median        0.349 us        0.349 us            5 items_per_second=2.86567M/s
BM_PopulateRequest/span_count:1_stddev        0.001 us        0.001 us            5 items_per_second=10.7304k/s
BM_PopulateRequest/span_count:1_cv             0.37 %          0.38 %             5 items_per_second=0.37%
BM_PopulateRequest/span_count:512_mean         26.1 us         26.1 us            5 items_per_second=19.5965M/s
BM_PopulateRequest/span_count:512_median       26.1 us         26.1 us            5 items_per_second=19.5943M/s
BM_PopulateRequest/span_count:512_stddev      0.104 us        0.104 us            5 items_per_second=77.6371k/s
BM_PopulateRequest/span_count:512_cv           0.40 %          0.40 %             5 items_per_second=0.40%

@marcalff

marcalff commented Jun 17, 2026

Copy link
Copy Markdown
Member

Implementation MUST by default enforce that the exported attribute collections contain only unique keys.

My reading is as follows:

  • (a) Implementation MUST have code to enforce that the exported attribute collections contain only unique keys.
  • (b) Such code is executed by default, but there is a way to disable it.

For applications that trust the instrumentation to be correct, having the de duplication logic is overhead.

This hints at the existence of a configuration parameter, which is not defined in the spec.

Implementations MAY have an option to allow exporting attribute collections with duplicate keys (e.g. for better performance).

OtlpPopulateAttributeUtils::PopulateAttribute(proto_record_.add_attributes(), key, value, true);
for (auto &attr : *proto_record_.mutable_attributes())
{
if (attr.key() == key)

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.

Can we use a hash map (e.g., unordered_map) to track existing keys? The current linear scan on every SetAttribute call is O(n) per insertion—O(n²) total—and the overhead compounds quickly at scale.

It's not just the number of attributes: string comparison cost grows with key length. Each loop iteration does a full character-by-character comparison, whereas a hash map typically checks hash codes first, only falling back to full comparison on collisions.

@dbarker dbarker Jun 18, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

88e47f6

This implements the hash map for attribute keys.

Compared to the linear search (1525b4c), the performance is better in the max attributes case by -5.5us or -31%, but worse for the nominal attribute case (6 attributes) by ~220 ns or +37%.

Benchmark main linear search (1525b4c) hash map (88e47f6)
Minimal 342 ns 340 ns 359 ns
Nominal 596 ns 610 ns 835 ns
MaxAttributes 7,517 ns 17,773 ns 12,256 ns
PopulateRequest/1 378 ns 349 ns 367 ns
PopulateRequest/512 25.5 µs 26.1 µs 26.0 µs
-----------------------------------------------------------------------------------
Benchmark                                         Time             CPU   Iterations
-----------------------------------------------------------------------------------
OtlpSpanFixture/Minimal_mean                    359 ns          361 ns            5
OtlpSpanFixture/Minimal_median                  359 ns          361 ns            5
OtlpSpanFixture/Minimal_stddev                0.440 ns        0.477 ns            5
OtlpSpanFixture/Minimal_cv                     0.12 %          0.13 %             5
OtlpSpanFixture/Nominal_mean                    835 ns          839 ns            5
OtlpSpanFixture/Nominal_median                  834 ns          839 ns            5
OtlpSpanFixture/Nominal_stddev                 5.53 ns         5.61 ns            5
OtlpSpanFixture/Nominal_cv                     0.66 %          0.67 %             5
OtlpSpanFixture/MaxAttributes_mean            12256 ns        12261 ns            5 items_per_second=10.4397M/s
OtlpSpanFixture/MaxAttributes_median          12233 ns        12238 ns            5 items_per_second=10.4593M/s
OtlpSpanFixture/MaxAttributes_stddev           62.0 ns         61.9 ns            5 items_per_second=52.5819k/s
OtlpSpanFixture/MaxAttributes_cv               0.51 %          0.50 %             5 items_per_second=0.50%
BM_PopulateRequest/span_count:1_mean          0.367 us        0.367 us            5 items_per_second=2.72565M/s
BM_PopulateRequest/span_count:1_median        0.367 us        0.367 us            5 items_per_second=2.72547M/s
BM_PopulateRequest/span_count:1_stddev        0.001 us        0.001 us            5 items_per_second=4.4662k/s
BM_PopulateRequest/span_count:1_cv             0.16 %          0.16 %             5 items_per_second=0.16%
BM_PopulateRequest/span_count:512_mean         26.0 us         26.0 us            5 items_per_second=19.6842M/s
BM_PopulateRequest/span_count:512_median       26.0 us         26.0 us            5 items_per_second=19.7045M/s
BM_PopulateRequest/span_count:512_stddev      0.212 us        0.212 us            5 items_per_second=160.233k/s
BM_PopulateRequest/span_count:512_cv           0.82 %          0.82 %             5 items_per_second=0.81%

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.

3 participants