1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-06-19 21:45:50 +02:00
Files
Robert Pająk 4ef740f17f sdk: deduplicate MAP attribute keys (#8471)
Fixes https://github.com/open-telemetry/opentelemetry-go/issues/7957.

- Remove duplicate keys from `attribute.MAP` values by default across
SDK resource, log, trace, and metric export paths, including top-level
MAP-valued attributes and MAP values nested inside MAP or SLICE values.
- Update `log.WithAllowKeyDuplication()` behavior and docs.
- Always deduplicate MAP values in resources;
`go.opentelemetry.io/otel/sdk/resource` does not provide an opt-out.
Resources are typically evaluated once at startup or occasionally
updated. The ~12.90% overhead shown in benchmarks will have low impact
on an application's runtime latency, and having a strictly normalized
Resource map prevents downstream exporter issues.
- Preserve top-level `attribute.Set` behavior for resources, scopes, and
metric datapoint attributes; this change normalizes MAP-valued attribute
values, not the outer attribute collection semantics.
- Use last-value-wins semantics and do not increment dropped-attribute
counts for MAP duplicate removal.
- Keep internal helper code scoped per module: shared source lives in
`internal/shared/attrdedup`, then `gotmpl` renders module-local copies
under `sdk/internal`, `sdk/log/internal`, and `sdk/metric/internal`.

## Benchmarks

Baseline benchmarks were run from a local `main` worktree at
`/tmp/opentelemetry-go-main`; branch benchmarks were run from this
branch. All comparisons use `-benchmem -count=10`.

New helper benchmarks:

```console
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/sdk/internal/attrdedup
cpu: 13th Gen Intel(R) Core(TM) i7-13800H
                          │   this.txt   │
                          │    sec/op    │
Value/FastPath-20           186.4n ± 19%
Value/DuplicateMap-20       363.4n ± 82%
Value/NestedMapInSlice-20   607.4n ± 20%
geomean                     345.2n

                          │   this.txt   │
                          │     B/op     │
Value/FastPath-20           0.000 ± 0%
Value/DuplicateMap-20       320.0 ± 0%
Value/NestedMapInSlice-20   416.0 ± 0%
geomean                                ¹
¹ summaries must be >0 to compute geomean

                          │   this.txt   │
                          │  allocs/op   │
Value/FastPath-20           0.000 ± 0%
Value/DuplicateMap-20       2.000 ± 0%
Value/NestedMapInSlice-20   4.000 ± 0%
geomean                                ¹
¹ summaries must be >0 to compute geomean
```

```console
pkg: go.opentelemetry.io/otel/sdk/resource
                    │ main │ branch │
                    │ sec/op │ sec/op vs base │
MergeResource_1-20    587.9n ± 28%   677.7n ± 17%  +15.28% (p=0.043 n=10)
MergeResource_2-20    938.5n ± 11%  1011.8n ± 18%        ~ (p=0.393 n=10)
MergeResource_3-20    1.301µ ±  8%   1.522µ ± 21%  +16.95% (p=0.023 n=10)
MergeResource_4-20    1.487µ ± 31%   2.006µ ± 12%  +34.90% (p=0.000 n=10)
MergeResource_6-20    2.409µ ± 12%   2.962µ ± 15%  +22.96% (p=0.001 n=10)
MergeResource_8-20    3.336µ ± 10%   3.104µ ± 16%        ~ (p=0.247 n=10)
MergeResource_16-20   7.750µ ±  6%   8.076µ ±  9%        ~ (p=0.190 n=10)
geomean               1.821µ         2.056µ        +12.90%

pkg: go.opentelemetry.io/otel/sdk/trace
                                              │ main │ branch │
                                              │ sec/op │ sec/op vs base │
RecordingSpanSetAttributes/WithLimit/false-20  7.907µ ±  6%  11.311µ ±  6%  +43.06% (p=0.000 n=10)
RecordingSpanSetAttributes/WithLimit/true-20   13.29µ ± 15%   17.13µ ±  6%  +28.97% (p=0.002 n=10)
TraceStart/with_a_simple_span-20               402.1n ±  2%   406.4n ±  2%        ~ (p=0.218 n=10)
TraceStart/with_several_links-20               521.8n ±  1%   532.4n ±  2%  +2.03% (p=0.000 n=10)
TraceStart/with_attributes-20                  570.5n ±  2%   594.1n ±  2%  +4.12% (p=0.000 n=10)
TraceStart/ObservabilityEnabled-20             463.5n ±  1%   469.9n ±  1%  +1.38% (p=0.003 n=10)
SpanSetAttributesOverCapacity-20               2.539µ ±  8%   2.501µ ±  8%       ~ (p=0.684 n=10)
SpanWithAttributes_4/AlwaysSample-20           1.650µ ± 23%   1.867µ ± 12%       ~ (p=0.063 n=10)
SpanWithAttributes_4/NeverSample-20            634.5n ± 11%   683.4n ± 19%       ~ (p=0.315 n=10)
geomean                                        1.356µ         1.495µ       +10.27%
```

Resource and trace allocation counts were unchanged in the same
benchstat run.

```console
pkg: go.opentelemetry.io/otel/sdk/log
                                              │ main │ branch │
                                              │ sec/op │ sec/op vs base │
LoggerProviderLogger-20                        505.1n ± 8%  473.9n ± 15%       ~ (p=0.123 n=10)
SetAttributes/Unique/NoLimits-20               230.9n ± 9%  238.1n ±  7%       ~ (p=0.343 n=10)
SetAttributes/NestedDeduplication/Enabled-20   836.0n ± 6%  758.1n ±  8%  -9.32% (p=0.011 n=10)
SetAttributes/NestedDeduplication/Disabled-20  119.7n ± 1%  127.7n ±  8%       ~ (p=0.157 n=10)
geomean                                        328.7n       323.3n        -1.65%
```

Log allocation counts were unchanged in the same benchstat run.

```console
pkg: go.opentelemetry.io/otel/sdk/metric
                                                         │ main │ branch │
                                                         │ sec/op │ sec/op vs base │
MeasureNewAttributeSet/AlwaysOn/Int64Counter-20           725.7n ± 114%  1504.5n ± 15%  +107.32% (p=0.043 n=10)
MeasureNewAttributeSet/AlwaysOn/Float64Counter-20         1.106µ ±  14%   1.480µ ± 15%   +33.82% (p=0.000 n=10)
MeasureNewAttributeSet/AlwaysOn/Int64UpDownCounter-20     1.354µ ±   9%   1.334µ ± 13%         ~ (p=0.529 n=10)
MeasureNewAttributeSet/AlwaysOn/Float64UpDownCounter-20   1.445µ ±  29%   1.439µ ± 14%         ~ (p=0.542 n=10)
MeasureNewAttributeSet/AlwaysOn/Int64Histogram-20         1.423µ ±  31%   1.453µ ± 29%         ~ (p=0.631 n=10)
MeasureNewAttributeSet/AlwaysOn/Float64Histogram-20       1.383µ ±  41%   1.416µ ± 20%         ~ (p=0.796 n=10)
MeasureNewAttributeSet/AlwaysOn/Int64Gauge-20             1.105µ ±  42%   1.527µ ±  8%   +38.21% (p=0.023 n=10)
MeasureNewAttributeSet/AlwaysOn/Float64Gauge-20           1.598µ ±  10%   1.355µ ± 37%         ~ (p=0.247 n=10)
MeasureNewAttributeSet/AlwaysOff/Int64Counter-20          739.1n ±  16%   830.0n ± 17%         ~ (p=0.280 n=10)
MeasureNewAttributeSet/AlwaysOff/Float64Counter-20        299.2n ±  24%   766.7n ± 29%  +156.19% (p=0.000 n=10)
MeasureNewAttributeSet/AlwaysOff/Int64UpDownCounter-20    322.2n ± 104%   873.2n ±  7%  +171.01% (p=0.000 n=10)
MeasureNewAttributeSet/AlwaysOff/Float64UpDownCounter-20  740.5n ±  49%   713.8n ± 21%         ~ (p=0.481 n=10)
MeasureNewAttributeSet/AlwaysOff/Int64Histogram-20        901.1n ±  33%   988.8n ± 12%         ~ (p=0.481 n=10)
MeasureNewAttributeSet/AlwaysOff/Float64Histogram-20      1.059µ ±  23%   1.009µ ± 21%         ~ (p=0.393 n=10)
MeasureNewAttributeSet/AlwaysOff/Int64Gauge-20            408.9n ±  50%   788.0n ± 23%   +92.71% (p=0.000 n=10)
MeasureNewAttributeSet/AlwaysOff/Float64Gauge-20          544.5n ±  37%   662.1n ± 39%         ~ (p=0.725 n=10)
AsyncMeasureNewAttributeSet/AlwaysOn-20                   6.199µ ±  20%   5.766µ ± 26%         ~ (p=0.247 n=10)
AsyncMeasureNewAttributeSet/TraceBased-20                 1.811µ ±  10%   1.806µ ± 11%         ~ (p=0.912 n=10)
Instrument/instrumentImpl/aggregate-20                    2.577µ ±  10%   2.378µ ± 14%         ~ (p=0.247 n=10)
Instrument/observable/observe-20                          1.863µ ±  17%   1.762µ ±  8%         ~ (p=0.255 n=10)
InstrumentCreation-20                                     2.346µ ±  17%   2.215µ ±  8%         ~ (p=0.315 n=10)
geomean                                                   1.102µ          1.324µ         +20.12%
```

Metric B/op geomean was +0.10% and allocs/op geomean was unchanged in
the same benchstat run.

## Rejected implementations

### Seen-Pool

I tested an alternative `deduplicateMapValue` implementation using a
pooled `map[string]struct{}` for duplicate detection, modeled after
`sdk/log.Record`'s `seenPool`. The candidate was not kept because it
regressed all non-opt-out helper benchmark paths.

```console
pkg: go.opentelemetry.io/otel/sdk/internal/attrdedup
                             │ baseline │ seenpool │
                             │ sec/op │ sec/op vs base │
Value/FastPath-20              143.8n ± 0%   201.6n ±  0%  +40.15% (p=0.000 n=20)
Value/DuplicateMap-20          669.8n ± 9%   862.9n ± 12%  +28.83% (p=0.000 n=20)
Value/NestedMapInSlice-20      918.2n ± 9%  1115.0n ±  5%  +21.43% (p=0.000 n=20)
Value/AllowKeyDuplication-20   1.324n ± 0%   1.324n ±  0%        ~ (p=0.389 n=20)
geomean                        104.0n        126.6n        +21.68%
```

The candidate did not change allocation counts, but the `sync.Pool` and
map operations added measurable CPU overhead.

### Needs-Style Preflight

I tested an alternative implementation modeled after
`sdk/log.Record.needsValueLimitsOrDedup`: first scan attribute values to
determine whether normalization is needed, then normalize only when the
scan says a value would change.

The refined candidate preserved the opt-out fast path and avoided an
extra slice preflight pass, but was not kept because package-level
benchmark results were mixed: resource improved, but trace and metric
regressed.

```console
pkg: go.opentelemetry.io/otel/sdk/internal/attrdedup
                             │ baseline │ candidate │
                             │ sec/op │ sec/op vs base │
Value/FastPath-20              144.10n ± 0%   92.52n ± 0%  -35.79% (p=0.000 n=10)
Value/DuplicateMap-20           682.8n ± 19%  677.1n ± 34%       ~ (p=0.796 n=10)
Value/NestedMapInSlice-20       900.5n ±  7%  975.5n ±  8%  +8.33% (p=0.019 n=10)
Value/AllowKeyDuplication-20    1.325n ±  0%  1.325n ±  1%       ~ (p=0.860 n=10)
geomean                         104.1n        94.85n       -8.88%
```

```console
pkg: go.opentelemetry.io/otel/sdk/resource
MergeResource geomean: 2.209µ -> 1.896µ (-14.17%)

pkg: go.opentelemetry.io/otel/sdk/trace
Trace benchmark geomean: 1.474µ -> 1.576µ (+6.90%)
```

```console
pkg: go.opentelemetry.io/otel/sdk/log
Log benchmark geomean: 330.0n -> 330.6n (+0.18%)
```

```console
pkg: go.opentelemetry.io/otel/sdk/metric
Metric benchmark geomean: 1.355µ -> 1.455µ (+7.38%)
```

All candidate allocation geomeans were unchanged. Because the candidate
did not improve performance consistently across affected export paths,
this PR keeps the original lazy allocation-free no-op implementation.
2026-06-18 06:25:50 +02:00
..
2026-06-08 16:25:23 +02:00
2026-06-08 16:25:23 +02:00
2026-05-27 18:42:37 +02:00