You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-19 21:45:50 +02:00
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.
This commit is contained in:
@@ -16,6 +16,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
- Support `MAP` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric`. (#8453)
|
||||
- Support `MAP` attributes in `go.opentelemetry.io/otel/exporters/zipkin`. (#8453)
|
||||
- Apply `AttributeValueLengthLimit` to `attribute.MAP` type attribute values in `go.opentelemetry.io/otel/sdk/trace`, recursively truncating contained values. (#8454)
|
||||
- Add duplicate-key removal for `attribute.MAP` values in `go.opentelemetry.io/otel/sdk/resource` using last-value-wins semantics. (#8471)
|
||||
- Add default duplicate-key removal for `attribute.MAP` values in instrumentation scope attributes in `go.opentelemetry.io/otel/sdk/log` using last-value-wins semantics. (#8471)
|
||||
- Add default duplicate-key removal for `attribute.MAP` values in span, event, link, and instrumentation scope attributes in `go.opentelemetry.io/otel/sdk/trace` using last-value-wins semantics. (#8471)
|
||||
- Add default duplicate-key removal for `attribute.MAP` values in measurement and instrumentation scope attributes in `go.opentelemetry.io/otel/sdk/metric` using last-value-wins semantics. (#8471)
|
||||
- Extend `WithAllowKeyDuplication` in `go.opentelemetry.io/otel/sdk/log` to disable duplicate-key removal in `attribute.MAP` values for instrumentation scope attributes. (#8471)
|
||||
- Add `WithUnsafeAttributes` to `go.opentelemetry.io/otel/metric/x` as an experimental no-copy attribute option intended for future performance work. This is a work in progress. (#8251)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/attrdedup/dedup.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package attrdedup deduplicates attribute map values.
|
||||
package attrdedup
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
var (
|
||||
keyValueType = reflect.TypeFor[attribute.KeyValue]()
|
||||
valueType = reflect.TypeFor[attribute.Value]()
|
||||
)
|
||||
|
||||
// rawValue mirrors attribute.Value. It is used only to read immutable slice
|
||||
// storage without calling AsMap or AsSlice on no-op paths.
|
||||
type rawValue struct {
|
||||
vtype attribute.Type
|
||||
numeric uint64
|
||||
stringly string
|
||||
slice any
|
||||
}
|
||||
|
||||
// Value returns value with all map values deduplicated.
|
||||
//
|
||||
// Duplicate map keys are resolved using last-value-wins semantics.
|
||||
func Value(value attribute.Value) attribute.Value {
|
||||
value, _ = deduplicateValue(value)
|
||||
return value
|
||||
}
|
||||
|
||||
// KeyValue returns kv with all map values deduplicated.
|
||||
func KeyValue(kv attribute.KeyValue) attribute.KeyValue {
|
||||
kv, _ = deduplicateKeyValue(kv)
|
||||
return kv
|
||||
}
|
||||
|
||||
// KeyValues returns kvs with all map values deduplicated.
|
||||
//
|
||||
// The returned slice is the original kvs slice if no value needs
|
||||
// deduplication. Top-level keys in kvs are not deduplicated.
|
||||
func KeyValues(kvs []attribute.KeyValue) []attribute.KeyValue {
|
||||
kvs, _ = deduplicateKeyValues(kvs)
|
||||
return kvs
|
||||
}
|
||||
|
||||
// Set returns set with all map values deduplicated.
|
||||
//
|
||||
// The returned Set is the original set if no value needs deduplication.
|
||||
// Top-level key uniqueness remains attribute.Set's responsibility; this only
|
||||
// normalizes map attribute values.
|
||||
func Set(set attribute.Set) attribute.Set {
|
||||
if set.Len() == 0 {
|
||||
return set
|
||||
}
|
||||
|
||||
// Most attribute sets contain no duplicate map keys. Delay allocation until
|
||||
// the first changed value so the no-op path returns the original Set.
|
||||
var normalized []attribute.KeyValue
|
||||
for i := 0; i < set.Len(); i++ {
|
||||
kv, _ := set.Get(i)
|
||||
kv, changed := deduplicateKeyValue(kv)
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, 0, set.Len())
|
||||
for j := 0; j < i; j++ {
|
||||
prior, _ := set.Get(j)
|
||||
normalized = append(normalized, prior)
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
if normalized == nil {
|
||||
return set
|
||||
}
|
||||
|
||||
return attribute.NewSet(normalized...)
|
||||
}
|
||||
|
||||
func deduplicateKeyValues(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
// Preserve the caller's slice on the common no-op path. Once a changed
|
||||
// value is found, copy the prior values exactly once and fill the rest in
|
||||
// place as the scan continues.
|
||||
var normalized []attribute.KeyValue
|
||||
for i, kv := range kvs {
|
||||
kv, changed := deduplicateKeyValue(kv)
|
||||
if normalized != nil {
|
||||
normalized[i] = kv
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, len(kvs))
|
||||
copy(normalized, kvs[:i])
|
||||
normalized[i] = kv
|
||||
}
|
||||
if normalized == nil {
|
||||
return kvs, false
|
||||
}
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
func deduplicateKeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) {
|
||||
value, changed := deduplicateValue(kv.Value)
|
||||
if changed {
|
||||
kv.Value = value
|
||||
}
|
||||
return kv, changed
|
||||
}
|
||||
|
||||
func deduplicateValue(value attribute.Value) (attribute.Value, bool) {
|
||||
switch value.Type() {
|
||||
case attribute.SLICE:
|
||||
return deduplicateSliceValue(value)
|
||||
case attribute.MAP:
|
||||
return deduplicateMapValue(value)
|
||||
default:
|
||||
return value, false
|
||||
}
|
||||
}
|
||||
|
||||
func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := valueLen(storage)
|
||||
|
||||
// Slice values can contain map values, so recurse into each element while
|
||||
// keeping the original attribute.Value when no element changes.
|
||||
var normalized []attribute.Value
|
||||
for i := 0; i < length; i++ {
|
||||
elem := valueAt(storage, i)
|
||||
elem, changed := deduplicateValue(elem)
|
||||
if normalized != nil {
|
||||
normalized[i] = elem
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.Value, length)
|
||||
for j := 0; j < i; j++ {
|
||||
normalized[j] = valueAt(storage, j)
|
||||
}
|
||||
normalized[i] = elem
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.SliceValue(normalized...), true
|
||||
}
|
||||
|
||||
func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := keyValueLen(storage)
|
||||
if length <= 1 {
|
||||
// A single map entry cannot duplicate its own key, but its value might
|
||||
// contain a map or slice that needs recursive normalization.
|
||||
if length == 1 {
|
||||
kv, changed := deduplicateKeyValue(keyValueAt(storage, 0))
|
||||
if changed {
|
||||
return attribute.MapValue(kv), true
|
||||
}
|
||||
}
|
||||
return value, false
|
||||
}
|
||||
|
||||
var normalized []attribute.KeyValue
|
||||
for i := 0; i < length; {
|
||||
// attribute.MapValue stores key-values sorted by key using a stable
|
||||
// sort. Equal keys therefore form a contiguous run, and the last
|
||||
// element in that run is the last value provided by the caller.
|
||||
first := keyValueAt(storage, i)
|
||||
j := i + 1
|
||||
for j < length && keyValueAt(storage, j).Key == first.Key {
|
||||
j++
|
||||
}
|
||||
|
||||
kv, nestedChanged := deduplicateKeyValue(keyValueAt(storage, j-1))
|
||||
// j-i > 1 means the current key run contained duplicates.
|
||||
changed := nestedChanged || j-i > 1
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
} else if changed {
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
for k := 0; k < i; k++ {
|
||||
normalized = append(normalized, keyValueAt(storage, k))
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
i = j
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.MapValue(normalized...), true
|
||||
}
|
||||
|
||||
func valueStorage(value attribute.Value) any {
|
||||
// attribute.Value does not expose allocation-free map/slice iteration.
|
||||
// The raw mirror lets us read the immutable backing array directly and
|
||||
// reserve AsMap/AsSlice-style allocation for paths that actually change.
|
||||
return (*rawValue)(unsafe.Pointer(&value)).slice //nolint:gosec // Read-only mirror of attribute.Value for allocation-free iteration.
|
||||
}
|
||||
|
||||
func valueLen(storage any) int {
|
||||
// attribute.Value stores small slices in fixed-size array values. Handle
|
||||
// the common sizes directly and fall back to reflection for larger arrays.
|
||||
switch storage.(type) {
|
||||
case [0]attribute.Value:
|
||||
return 0
|
||||
case [1]attribute.Value:
|
||||
return 1
|
||||
case [2]attribute.Value:
|
||||
return 2
|
||||
case [3]attribute.Value:
|
||||
return 3
|
||||
case [4]attribute.Value:
|
||||
return 4
|
||||
case [5]attribute.Value:
|
||||
return 5
|
||||
default:
|
||||
return arrayLen(storage, valueType)
|
||||
}
|
||||
}
|
||||
|
||||
func valueAt(storage any, i int) attribute.Value {
|
||||
switch values := storage.(type) {
|
||||
case [1]attribute.Value:
|
||||
return values[i]
|
||||
case [2]attribute.Value:
|
||||
return values[i]
|
||||
case [3]attribute.Value:
|
||||
return values[i]
|
||||
case [4]attribute.Value:
|
||||
return values[i]
|
||||
case [5]attribute.Value:
|
||||
return values[i]
|
||||
default:
|
||||
return arrayAt[attribute.Value](storage, valueType, i)
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueLen(storage any) int {
|
||||
// attribute.Value stores small maps in fixed-size key-value arrays. Handle
|
||||
// the common sizes directly and fall back to reflection for larger arrays.
|
||||
switch storage.(type) {
|
||||
case [0]attribute.KeyValue:
|
||||
return 0
|
||||
case [1]attribute.KeyValue:
|
||||
return 1
|
||||
case [2]attribute.KeyValue:
|
||||
return 2
|
||||
case [3]attribute.KeyValue:
|
||||
return 3
|
||||
case [4]attribute.KeyValue:
|
||||
return 4
|
||||
case [5]attribute.KeyValue:
|
||||
return 5
|
||||
default:
|
||||
return arrayLen(storage, keyValueType)
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueAt(storage any, i int) attribute.KeyValue {
|
||||
switch kvs := storage.(type) {
|
||||
case [1]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [2]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [3]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [4]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [5]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
default:
|
||||
return arrayAt[attribute.KeyValue](storage, keyValueType, i)
|
||||
}
|
||||
}
|
||||
|
||||
func arrayLen(storage any, elem reflect.Type) int {
|
||||
// Be defensive around invalid or unexpected Value storage. Returning zero
|
||||
// makes malformed storage a no-op instead of panicking in telemetry paths.
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != elem {
|
||||
return 0
|
||||
}
|
||||
return array.Len()
|
||||
}
|
||||
|
||||
func arrayAt[T any](storage any, elem reflect.Type, i int) T {
|
||||
// Match arrayLen's fail-closed behavior for unexpected storage.
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != elem || i < 0 || i >= array.Len() {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
return array.Index(i).Interface().(T)
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/attrdedup/dedup_test.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package attrdedup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
var cmpValue = cmp.AllowUnexported(attribute.Value{})
|
||||
|
||||
func TestValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value attribute.Value
|
||||
want attribute.Value
|
||||
}{
|
||||
{
|
||||
name: "unique map",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate map",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate map after prior key",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("b", "3"),
|
||||
attribute.String("c", "4"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "3"),
|
||||
attribute.String("c", "4"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "nested map",
|
||||
value: attribute.MapValue(
|
||||
attribute.Map(
|
||||
"outer",
|
||||
attribute.String("inner", "1"),
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.Map(
|
||||
"outer",
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "map inside slice",
|
||||
value: attribute.SliceValue(
|
||||
attribute.StringValue("prior"),
|
||||
attribute.MapValue(
|
||||
attribute.String("inner", "1"),
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
attribute.StringValue("tail"),
|
||||
),
|
||||
want: attribute.SliceValue(
|
||||
attribute.StringValue("prior"),
|
||||
attribute.MapValue(
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
attribute.StringValue("tail"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "unique slice",
|
||||
value: attribute.SliceValue(
|
||||
attribute.StringValue("one"),
|
||||
attribute.IntValue(2),
|
||||
),
|
||||
want: attribute.SliceValue(
|
||||
attribute.StringValue("one"),
|
||||
attribute.IntValue(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "empty and invalid keys",
|
||||
value: attribute.MapValue(
|
||||
attribute.KeyValue{},
|
||||
attribute.String("", "empty"),
|
||||
attribute.String("valid", "value"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("", "empty"),
|
||||
attribute.String("valid", "value"),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := Value(test.value)
|
||||
if diff := cmp.Diff(test.want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueNoopAllocationFree(t *testing.T) {
|
||||
value := attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
)
|
||||
var got attribute.Value
|
||||
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
got = Value(value)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Fatalf("Value() allocations = %v, want 0", allocs)
|
||||
}
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueStorageShapes(t *testing.T) {
|
||||
for n := 0; n <= 6; n++ {
|
||||
t.Run("map", func(t *testing.T) {
|
||||
kvs := make([]attribute.KeyValue, n)
|
||||
for i := range kvs {
|
||||
kvs[i] = attribute.Int(string(rune('a'+i)), i)
|
||||
}
|
||||
value := attribute.MapValue(kvs...)
|
||||
|
||||
got := Value(value)
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("slice", func(t *testing.T) {
|
||||
values := make([]attribute.Value, n)
|
||||
for i := range values {
|
||||
values[i] = attribute.IntValue(i)
|
||||
}
|
||||
value := attribute.SliceValue(values...)
|
||||
|
||||
got := Value(value)
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValue(t *testing.T) {
|
||||
kv := attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
)
|
||||
want := attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "second"),
|
||||
)
|
||||
|
||||
got := KeyValue(kv)
|
||||
if diff := cmp.Diff(want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("KeyValue() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValuesNoopReturnsInput(t *testing.T) {
|
||||
kvs := []attribute.KeyValue{
|
||||
attribute.String("one", "1"),
|
||||
attribute.Map("two", attribute.String("nested", "value")),
|
||||
}
|
||||
|
||||
got := KeyValues(kvs)
|
||||
if len(got) != len(kvs) {
|
||||
t.Fatalf("KeyValues() length = %d, want %d", len(got), len(kvs))
|
||||
}
|
||||
if &got[0] != &kvs[0] {
|
||||
t.Fatal("KeyValues() copied a no-op input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValues(t *testing.T) {
|
||||
kvs := []attribute.KeyValue{
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("tail", "value"),
|
||||
}
|
||||
want := []attribute.KeyValue{
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("tail", "value"),
|
||||
}
|
||||
|
||||
got := KeyValues(kvs)
|
||||
if diff := cmp.Diff(want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("KeyValues() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
set := attribute.NewSet(
|
||||
attribute.String("a-top", "value"),
|
||||
attribute.Map(
|
||||
"m-map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("z-tail", "value"),
|
||||
)
|
||||
want := attribute.NewSet(
|
||||
attribute.String("a-top", "value"),
|
||||
attribute.Map(
|
||||
"m-map",
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("z-tail", "value"),
|
||||
)
|
||||
|
||||
got := Set(set)
|
||||
if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" {
|
||||
t.Fatalf("Set() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNoop(t *testing.T) {
|
||||
set := attribute.NewSet(
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map("map", attribute.String("nested", "value")),
|
||||
)
|
||||
|
||||
got := Set(set)
|
||||
if !got.Equals(&set) {
|
||||
t.Fatal("Set() changed a no-op input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEmpty(t *testing.T) {
|
||||
set := attribute.Set{}
|
||||
|
||||
got := Set(set)
|
||||
if !got.Equals(&set) {
|
||||
t.Fatal("Set() changed an empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidArrayStorage(t *testing.T) {
|
||||
if got := arrayLen("invalid", valueType); got != 0 {
|
||||
t.Fatalf("arrayLen() = %d, want 0", got)
|
||||
}
|
||||
|
||||
if got := arrayAt[attribute.Value]("invalid", valueType, 0); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
if got := arrayAt[attribute.Value](
|
||||
[1]attribute.Value{attribute.StringValue("value")},
|
||||
keyValueType,
|
||||
0,
|
||||
); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
if got := arrayAt[attribute.Value](
|
||||
[1]attribute.Value{attribute.StringValue("value")},
|
||||
valueType,
|
||||
-1,
|
||||
); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/attrdedup/dedup.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package attrdedup deduplicates attribute map values.
|
||||
package attrdedup // import "go.opentelemetry.io/otel/sdk/internal/attrdedup"
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
var (
|
||||
keyValueType = reflect.TypeFor[attribute.KeyValue]()
|
||||
valueType = reflect.TypeFor[attribute.Value]()
|
||||
)
|
||||
|
||||
// rawValue mirrors attribute.Value. It is used only to read immutable slice
|
||||
// storage without calling AsMap or AsSlice on no-op paths.
|
||||
type rawValue struct {
|
||||
vtype attribute.Type
|
||||
numeric uint64
|
||||
stringly string
|
||||
slice any
|
||||
}
|
||||
|
||||
// Value returns value with all map values deduplicated.
|
||||
//
|
||||
// Duplicate map keys are resolved using last-value-wins semantics.
|
||||
func Value(value attribute.Value) attribute.Value {
|
||||
value, _ = deduplicateValue(value)
|
||||
return value
|
||||
}
|
||||
|
||||
// KeyValue returns kv with all map values deduplicated.
|
||||
func KeyValue(kv attribute.KeyValue) attribute.KeyValue {
|
||||
kv, _ = deduplicateKeyValue(kv)
|
||||
return kv
|
||||
}
|
||||
|
||||
// KeyValues returns kvs with all map values deduplicated.
|
||||
//
|
||||
// The returned slice is the original kvs slice if no value needs
|
||||
// deduplication. Top-level keys in kvs are not deduplicated.
|
||||
func KeyValues(kvs []attribute.KeyValue) []attribute.KeyValue {
|
||||
kvs, _ = deduplicateKeyValues(kvs)
|
||||
return kvs
|
||||
}
|
||||
|
||||
// Set returns set with all map values deduplicated.
|
||||
//
|
||||
// The returned Set is the original set if no value needs deduplication.
|
||||
// Top-level key uniqueness remains attribute.Set's responsibility; this only
|
||||
// normalizes map attribute values.
|
||||
func Set(set attribute.Set) attribute.Set {
|
||||
if set.Len() == 0 {
|
||||
return set
|
||||
}
|
||||
|
||||
// Most attribute sets contain no duplicate map keys. Delay allocation until
|
||||
// the first changed value so the no-op path returns the original Set.
|
||||
var normalized []attribute.KeyValue
|
||||
for i := 0; i < set.Len(); i++ {
|
||||
kv, _ := set.Get(i)
|
||||
kv, changed := deduplicateKeyValue(kv)
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, 0, set.Len())
|
||||
for j := 0; j < i; j++ {
|
||||
prior, _ := set.Get(j)
|
||||
normalized = append(normalized, prior)
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
if normalized == nil {
|
||||
return set
|
||||
}
|
||||
|
||||
return attribute.NewSet(normalized...)
|
||||
}
|
||||
|
||||
func deduplicateKeyValues(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
// Preserve the caller's slice on the common no-op path. Once a changed
|
||||
// value is found, copy the prior values exactly once and fill the rest in
|
||||
// place as the scan continues.
|
||||
var normalized []attribute.KeyValue
|
||||
for i, kv := range kvs {
|
||||
kv, changed := deduplicateKeyValue(kv)
|
||||
if normalized != nil {
|
||||
normalized[i] = kv
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, len(kvs))
|
||||
copy(normalized, kvs[:i])
|
||||
normalized[i] = kv
|
||||
}
|
||||
if normalized == nil {
|
||||
return kvs, false
|
||||
}
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
func deduplicateKeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) {
|
||||
value, changed := deduplicateValue(kv.Value)
|
||||
if changed {
|
||||
kv.Value = value
|
||||
}
|
||||
return kv, changed
|
||||
}
|
||||
|
||||
func deduplicateValue(value attribute.Value) (attribute.Value, bool) {
|
||||
switch value.Type() {
|
||||
case attribute.SLICE:
|
||||
return deduplicateSliceValue(value)
|
||||
case attribute.MAP:
|
||||
return deduplicateMapValue(value)
|
||||
default:
|
||||
return value, false
|
||||
}
|
||||
}
|
||||
|
||||
func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := valueLen(storage)
|
||||
|
||||
// Slice values can contain map values, so recurse into each element while
|
||||
// keeping the original attribute.Value when no element changes.
|
||||
var normalized []attribute.Value
|
||||
for i := 0; i < length; i++ {
|
||||
elem := valueAt(storage, i)
|
||||
elem, changed := deduplicateValue(elem)
|
||||
if normalized != nil {
|
||||
normalized[i] = elem
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.Value, length)
|
||||
for j := 0; j < i; j++ {
|
||||
normalized[j] = valueAt(storage, j)
|
||||
}
|
||||
normalized[i] = elem
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.SliceValue(normalized...), true
|
||||
}
|
||||
|
||||
func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := keyValueLen(storage)
|
||||
if length <= 1 {
|
||||
// A single map entry cannot duplicate its own key, but its value might
|
||||
// contain a map or slice that needs recursive normalization.
|
||||
if length == 1 {
|
||||
kv, changed := deduplicateKeyValue(keyValueAt(storage, 0))
|
||||
if changed {
|
||||
return attribute.MapValue(kv), true
|
||||
}
|
||||
}
|
||||
return value, false
|
||||
}
|
||||
|
||||
var normalized []attribute.KeyValue
|
||||
for i := 0; i < length; {
|
||||
// attribute.MapValue stores key-values sorted by key using a stable
|
||||
// sort. Equal keys therefore form a contiguous run, and the last
|
||||
// element in that run is the last value provided by the caller.
|
||||
first := keyValueAt(storage, i)
|
||||
j := i + 1
|
||||
for j < length && keyValueAt(storage, j).Key == first.Key {
|
||||
j++
|
||||
}
|
||||
|
||||
kv, nestedChanged := deduplicateKeyValue(keyValueAt(storage, j-1))
|
||||
// j-i > 1 means the current key run contained duplicates.
|
||||
changed := nestedChanged || j-i > 1
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
} else if changed {
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
for k := 0; k < i; k++ {
|
||||
normalized = append(normalized, keyValueAt(storage, k))
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
i = j
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.MapValue(normalized...), true
|
||||
}
|
||||
|
||||
func valueStorage(value attribute.Value) any {
|
||||
// attribute.Value does not expose allocation-free map/slice iteration.
|
||||
// The raw mirror lets us read the immutable backing array directly and
|
||||
// reserve AsMap/AsSlice-style allocation for paths that actually change.
|
||||
return (*rawValue)(unsafe.Pointer(&value)).slice //nolint:gosec // Read-only mirror of attribute.Value for allocation-free iteration.
|
||||
}
|
||||
|
||||
func valueLen(storage any) int {
|
||||
// attribute.Value stores small slices in fixed-size array values. Handle
|
||||
// the common sizes directly and fall back to reflection for larger arrays.
|
||||
switch storage.(type) {
|
||||
case [0]attribute.Value:
|
||||
return 0
|
||||
case [1]attribute.Value:
|
||||
return 1
|
||||
case [2]attribute.Value:
|
||||
return 2
|
||||
case [3]attribute.Value:
|
||||
return 3
|
||||
case [4]attribute.Value:
|
||||
return 4
|
||||
case [5]attribute.Value:
|
||||
return 5
|
||||
default:
|
||||
return arrayLen(storage, valueType)
|
||||
}
|
||||
}
|
||||
|
||||
func valueAt(storage any, i int) attribute.Value {
|
||||
switch values := storage.(type) {
|
||||
case [1]attribute.Value:
|
||||
return values[i]
|
||||
case [2]attribute.Value:
|
||||
return values[i]
|
||||
case [3]attribute.Value:
|
||||
return values[i]
|
||||
case [4]attribute.Value:
|
||||
return values[i]
|
||||
case [5]attribute.Value:
|
||||
return values[i]
|
||||
default:
|
||||
return arrayAt[attribute.Value](storage, valueType, i)
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueLen(storage any) int {
|
||||
// attribute.Value stores small maps in fixed-size key-value arrays. Handle
|
||||
// the common sizes directly and fall back to reflection for larger arrays.
|
||||
switch storage.(type) {
|
||||
case [0]attribute.KeyValue:
|
||||
return 0
|
||||
case [1]attribute.KeyValue:
|
||||
return 1
|
||||
case [2]attribute.KeyValue:
|
||||
return 2
|
||||
case [3]attribute.KeyValue:
|
||||
return 3
|
||||
case [4]attribute.KeyValue:
|
||||
return 4
|
||||
case [5]attribute.KeyValue:
|
||||
return 5
|
||||
default:
|
||||
return arrayLen(storage, keyValueType)
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueAt(storage any, i int) attribute.KeyValue {
|
||||
switch kvs := storage.(type) {
|
||||
case [1]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [2]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [3]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [4]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [5]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
default:
|
||||
return arrayAt[attribute.KeyValue](storage, keyValueType, i)
|
||||
}
|
||||
}
|
||||
|
||||
func arrayLen(storage any, elem reflect.Type) int {
|
||||
// Be defensive around invalid or unexpected Value storage. Returning zero
|
||||
// makes malformed storage a no-op instead of panicking in telemetry paths.
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != elem {
|
||||
return 0
|
||||
}
|
||||
return array.Len()
|
||||
}
|
||||
|
||||
func arrayAt[T any](storage any, elem reflect.Type, i int) T {
|
||||
// Match arrayLen's fail-closed behavior for unexpected storage.
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != elem || i < 0 || i >= array.Len() {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
return array.Index(i).Interface().(T)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package attrdedup_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/sdk/internal/attrdedup"
|
||||
)
|
||||
|
||||
func BenchmarkValue(b *testing.B) {
|
||||
values := []struct {
|
||||
name string
|
||||
value attribute.Value
|
||||
}{
|
||||
{
|
||||
name: "FastPath",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
attribute.String("three", "3"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "DuplicateMap",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "NestedMapInSlice",
|
||||
value: attribute.SliceValue(
|
||||
attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, value := range values {
|
||||
b.Run(value.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
attrdedup.Value(value.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/attrdedup/dedup_test.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package attrdedup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
var cmpValue = cmp.AllowUnexported(attribute.Value{})
|
||||
|
||||
func TestValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value attribute.Value
|
||||
want attribute.Value
|
||||
}{
|
||||
{
|
||||
name: "unique map",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate map",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate map after prior key",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("b", "3"),
|
||||
attribute.String("c", "4"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "3"),
|
||||
attribute.String("c", "4"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "nested map",
|
||||
value: attribute.MapValue(
|
||||
attribute.Map(
|
||||
"outer",
|
||||
attribute.String("inner", "1"),
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.Map(
|
||||
"outer",
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "map inside slice",
|
||||
value: attribute.SliceValue(
|
||||
attribute.StringValue("prior"),
|
||||
attribute.MapValue(
|
||||
attribute.String("inner", "1"),
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
attribute.StringValue("tail"),
|
||||
),
|
||||
want: attribute.SliceValue(
|
||||
attribute.StringValue("prior"),
|
||||
attribute.MapValue(
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
attribute.StringValue("tail"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "unique slice",
|
||||
value: attribute.SliceValue(
|
||||
attribute.StringValue("one"),
|
||||
attribute.IntValue(2),
|
||||
),
|
||||
want: attribute.SliceValue(
|
||||
attribute.StringValue("one"),
|
||||
attribute.IntValue(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "empty and invalid keys",
|
||||
value: attribute.MapValue(
|
||||
attribute.KeyValue{},
|
||||
attribute.String("", "empty"),
|
||||
attribute.String("valid", "value"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("", "empty"),
|
||||
attribute.String("valid", "value"),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := Value(test.value)
|
||||
if diff := cmp.Diff(test.want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueNoopAllocationFree(t *testing.T) {
|
||||
value := attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
)
|
||||
var got attribute.Value
|
||||
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
got = Value(value)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Fatalf("Value() allocations = %v, want 0", allocs)
|
||||
}
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueStorageShapes(t *testing.T) {
|
||||
for n := 0; n <= 6; n++ {
|
||||
t.Run("map", func(t *testing.T) {
|
||||
kvs := make([]attribute.KeyValue, n)
|
||||
for i := range kvs {
|
||||
kvs[i] = attribute.Int(string(rune('a'+i)), i)
|
||||
}
|
||||
value := attribute.MapValue(kvs...)
|
||||
|
||||
got := Value(value)
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("slice", func(t *testing.T) {
|
||||
values := make([]attribute.Value, n)
|
||||
for i := range values {
|
||||
values[i] = attribute.IntValue(i)
|
||||
}
|
||||
value := attribute.SliceValue(values...)
|
||||
|
||||
got := Value(value)
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValue(t *testing.T) {
|
||||
kv := attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
)
|
||||
want := attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "second"),
|
||||
)
|
||||
|
||||
got := KeyValue(kv)
|
||||
if diff := cmp.Diff(want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("KeyValue() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValuesNoopReturnsInput(t *testing.T) {
|
||||
kvs := []attribute.KeyValue{
|
||||
attribute.String("one", "1"),
|
||||
attribute.Map("two", attribute.String("nested", "value")),
|
||||
}
|
||||
|
||||
got := KeyValues(kvs)
|
||||
if len(got) != len(kvs) {
|
||||
t.Fatalf("KeyValues() length = %d, want %d", len(got), len(kvs))
|
||||
}
|
||||
if &got[0] != &kvs[0] {
|
||||
t.Fatal("KeyValues() copied a no-op input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValues(t *testing.T) {
|
||||
kvs := []attribute.KeyValue{
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("tail", "value"),
|
||||
}
|
||||
want := []attribute.KeyValue{
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("tail", "value"),
|
||||
}
|
||||
|
||||
got := KeyValues(kvs)
|
||||
if diff := cmp.Diff(want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("KeyValues() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
set := attribute.NewSet(
|
||||
attribute.String("a-top", "value"),
|
||||
attribute.Map(
|
||||
"m-map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("z-tail", "value"),
|
||||
)
|
||||
want := attribute.NewSet(
|
||||
attribute.String("a-top", "value"),
|
||||
attribute.Map(
|
||||
"m-map",
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("z-tail", "value"),
|
||||
)
|
||||
|
||||
got := Set(set)
|
||||
if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" {
|
||||
t.Fatalf("Set() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNoop(t *testing.T) {
|
||||
set := attribute.NewSet(
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map("map", attribute.String("nested", "value")),
|
||||
)
|
||||
|
||||
got := Set(set)
|
||||
if !got.Equals(&set) {
|
||||
t.Fatal("Set() changed a no-op input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEmpty(t *testing.T) {
|
||||
set := attribute.Set{}
|
||||
|
||||
got := Set(set)
|
||||
if !got.Equals(&set) {
|
||||
t.Fatal("Set() changed an empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidArrayStorage(t *testing.T) {
|
||||
if got := arrayLen("invalid", valueType); got != 0 {
|
||||
t.Fatalf("arrayLen() = %d, want 0", got)
|
||||
}
|
||||
|
||||
if got := arrayAt[attribute.Value]("invalid", valueType, 0); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
if got := arrayAt[attribute.Value](
|
||||
[1]attribute.Value{attribute.StringValue("value")},
|
||||
keyValueType,
|
||||
0,
|
||||
); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
if got := arrayAt[attribute.Value](
|
||||
[1]attribute.Value{attribute.StringValue("value")},
|
||||
valueType,
|
||||
-1,
|
||||
); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,5 @@ package internal // import "go.opentelemetry.io/otel/sdk/internal"
|
||||
|
||||
//go:generate gotmpl --body=../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/sdk\" }" --out=x/x.go
|
||||
//go:generate gotmpl --body=../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go
|
||||
//go:generate gotmpl --body=../../internal/shared/attrdedup/dedup.go.tmpl "--data={}" --out=attrdedup/dedup.go
|
||||
//go:generate gotmpl --body=../../internal/shared/attrdedup/dedup_test.go.tmpl "--data={}" --out=attrdedup/dedup_test.go
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/attrdedup/dedup.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package attrdedup deduplicates attribute map values.
|
||||
package attrdedup // import "go.opentelemetry.io/otel/sdk/log/internal/attrdedup"
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
var (
|
||||
keyValueType = reflect.TypeFor[attribute.KeyValue]()
|
||||
valueType = reflect.TypeFor[attribute.Value]()
|
||||
)
|
||||
|
||||
// rawValue mirrors attribute.Value. It is used only to read immutable slice
|
||||
// storage without calling AsMap or AsSlice on no-op paths.
|
||||
type rawValue struct {
|
||||
vtype attribute.Type
|
||||
numeric uint64
|
||||
stringly string
|
||||
slice any
|
||||
}
|
||||
|
||||
// Value returns value with all map values deduplicated.
|
||||
//
|
||||
// Duplicate map keys are resolved using last-value-wins semantics.
|
||||
func Value(value attribute.Value) attribute.Value {
|
||||
value, _ = deduplicateValue(value)
|
||||
return value
|
||||
}
|
||||
|
||||
// KeyValue returns kv with all map values deduplicated.
|
||||
func KeyValue(kv attribute.KeyValue) attribute.KeyValue {
|
||||
kv, _ = deduplicateKeyValue(kv)
|
||||
return kv
|
||||
}
|
||||
|
||||
// KeyValues returns kvs with all map values deduplicated.
|
||||
//
|
||||
// The returned slice is the original kvs slice if no value needs
|
||||
// deduplication. Top-level keys in kvs are not deduplicated.
|
||||
func KeyValues(kvs []attribute.KeyValue) []attribute.KeyValue {
|
||||
kvs, _ = deduplicateKeyValues(kvs)
|
||||
return kvs
|
||||
}
|
||||
|
||||
// Set returns set with all map values deduplicated.
|
||||
//
|
||||
// The returned Set is the original set if no value needs deduplication.
|
||||
// Top-level key uniqueness remains attribute.Set's responsibility; this only
|
||||
// normalizes map attribute values.
|
||||
func Set(set attribute.Set) attribute.Set {
|
||||
if set.Len() == 0 {
|
||||
return set
|
||||
}
|
||||
|
||||
// Most attribute sets contain no duplicate map keys. Delay allocation until
|
||||
// the first changed value so the no-op path returns the original Set.
|
||||
var normalized []attribute.KeyValue
|
||||
for i := 0; i < set.Len(); i++ {
|
||||
kv, _ := set.Get(i)
|
||||
kv, changed := deduplicateKeyValue(kv)
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, 0, set.Len())
|
||||
for j := 0; j < i; j++ {
|
||||
prior, _ := set.Get(j)
|
||||
normalized = append(normalized, prior)
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
if normalized == nil {
|
||||
return set
|
||||
}
|
||||
|
||||
return attribute.NewSet(normalized...)
|
||||
}
|
||||
|
||||
func deduplicateKeyValues(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
// Preserve the caller's slice on the common no-op path. Once a changed
|
||||
// value is found, copy the prior values exactly once and fill the rest in
|
||||
// place as the scan continues.
|
||||
var normalized []attribute.KeyValue
|
||||
for i, kv := range kvs {
|
||||
kv, changed := deduplicateKeyValue(kv)
|
||||
if normalized != nil {
|
||||
normalized[i] = kv
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, len(kvs))
|
||||
copy(normalized, kvs[:i])
|
||||
normalized[i] = kv
|
||||
}
|
||||
if normalized == nil {
|
||||
return kvs, false
|
||||
}
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
func deduplicateKeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) {
|
||||
value, changed := deduplicateValue(kv.Value)
|
||||
if changed {
|
||||
kv.Value = value
|
||||
}
|
||||
return kv, changed
|
||||
}
|
||||
|
||||
func deduplicateValue(value attribute.Value) (attribute.Value, bool) {
|
||||
switch value.Type() {
|
||||
case attribute.SLICE:
|
||||
return deduplicateSliceValue(value)
|
||||
case attribute.MAP:
|
||||
return deduplicateMapValue(value)
|
||||
default:
|
||||
return value, false
|
||||
}
|
||||
}
|
||||
|
||||
func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := valueLen(storage)
|
||||
|
||||
// Slice values can contain map values, so recurse into each element while
|
||||
// keeping the original attribute.Value when no element changes.
|
||||
var normalized []attribute.Value
|
||||
for i := 0; i < length; i++ {
|
||||
elem := valueAt(storage, i)
|
||||
elem, changed := deduplicateValue(elem)
|
||||
if normalized != nil {
|
||||
normalized[i] = elem
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.Value, length)
|
||||
for j := 0; j < i; j++ {
|
||||
normalized[j] = valueAt(storage, j)
|
||||
}
|
||||
normalized[i] = elem
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.SliceValue(normalized...), true
|
||||
}
|
||||
|
||||
func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := keyValueLen(storage)
|
||||
if length <= 1 {
|
||||
// A single map entry cannot duplicate its own key, but its value might
|
||||
// contain a map or slice that needs recursive normalization.
|
||||
if length == 1 {
|
||||
kv, changed := deduplicateKeyValue(keyValueAt(storage, 0))
|
||||
if changed {
|
||||
return attribute.MapValue(kv), true
|
||||
}
|
||||
}
|
||||
return value, false
|
||||
}
|
||||
|
||||
var normalized []attribute.KeyValue
|
||||
for i := 0; i < length; {
|
||||
// attribute.MapValue stores key-values sorted by key using a stable
|
||||
// sort. Equal keys therefore form a contiguous run, and the last
|
||||
// element in that run is the last value provided by the caller.
|
||||
first := keyValueAt(storage, i)
|
||||
j := i + 1
|
||||
for j < length && keyValueAt(storage, j).Key == first.Key {
|
||||
j++
|
||||
}
|
||||
|
||||
kv, nestedChanged := deduplicateKeyValue(keyValueAt(storage, j-1))
|
||||
// j-i > 1 means the current key run contained duplicates.
|
||||
changed := nestedChanged || j-i > 1
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
} else if changed {
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
for k := 0; k < i; k++ {
|
||||
normalized = append(normalized, keyValueAt(storage, k))
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
i = j
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.MapValue(normalized...), true
|
||||
}
|
||||
|
||||
func valueStorage(value attribute.Value) any {
|
||||
// attribute.Value does not expose allocation-free map/slice iteration.
|
||||
// The raw mirror lets us read the immutable backing array directly and
|
||||
// reserve AsMap/AsSlice-style allocation for paths that actually change.
|
||||
return (*rawValue)(unsafe.Pointer(&value)).slice //nolint:gosec // Read-only mirror of attribute.Value for allocation-free iteration.
|
||||
}
|
||||
|
||||
func valueLen(storage any) int {
|
||||
// attribute.Value stores small slices in fixed-size array values. Handle
|
||||
// the common sizes directly and fall back to reflection for larger arrays.
|
||||
switch storage.(type) {
|
||||
case [0]attribute.Value:
|
||||
return 0
|
||||
case [1]attribute.Value:
|
||||
return 1
|
||||
case [2]attribute.Value:
|
||||
return 2
|
||||
case [3]attribute.Value:
|
||||
return 3
|
||||
case [4]attribute.Value:
|
||||
return 4
|
||||
case [5]attribute.Value:
|
||||
return 5
|
||||
default:
|
||||
return arrayLen(storage, valueType)
|
||||
}
|
||||
}
|
||||
|
||||
func valueAt(storage any, i int) attribute.Value {
|
||||
switch values := storage.(type) {
|
||||
case [1]attribute.Value:
|
||||
return values[i]
|
||||
case [2]attribute.Value:
|
||||
return values[i]
|
||||
case [3]attribute.Value:
|
||||
return values[i]
|
||||
case [4]attribute.Value:
|
||||
return values[i]
|
||||
case [5]attribute.Value:
|
||||
return values[i]
|
||||
default:
|
||||
return arrayAt[attribute.Value](storage, valueType, i)
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueLen(storage any) int {
|
||||
// attribute.Value stores small maps in fixed-size key-value arrays. Handle
|
||||
// the common sizes directly and fall back to reflection for larger arrays.
|
||||
switch storage.(type) {
|
||||
case [0]attribute.KeyValue:
|
||||
return 0
|
||||
case [1]attribute.KeyValue:
|
||||
return 1
|
||||
case [2]attribute.KeyValue:
|
||||
return 2
|
||||
case [3]attribute.KeyValue:
|
||||
return 3
|
||||
case [4]attribute.KeyValue:
|
||||
return 4
|
||||
case [5]attribute.KeyValue:
|
||||
return 5
|
||||
default:
|
||||
return arrayLen(storage, keyValueType)
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueAt(storage any, i int) attribute.KeyValue {
|
||||
switch kvs := storage.(type) {
|
||||
case [1]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [2]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [3]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [4]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [5]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
default:
|
||||
return arrayAt[attribute.KeyValue](storage, keyValueType, i)
|
||||
}
|
||||
}
|
||||
|
||||
func arrayLen(storage any, elem reflect.Type) int {
|
||||
// Be defensive around invalid or unexpected Value storage. Returning zero
|
||||
// makes malformed storage a no-op instead of panicking in telemetry paths.
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != elem {
|
||||
return 0
|
||||
}
|
||||
return array.Len()
|
||||
}
|
||||
|
||||
func arrayAt[T any](storage any, elem reflect.Type, i int) T {
|
||||
// Match arrayLen's fail-closed behavior for unexpected storage.
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != elem || i < 0 || i >= array.Len() {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
return array.Index(i).Interface().(T)
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/attrdedup/dedup_test.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package attrdedup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
var cmpValue = cmp.AllowUnexported(attribute.Value{})
|
||||
|
||||
func TestValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value attribute.Value
|
||||
want attribute.Value
|
||||
}{
|
||||
{
|
||||
name: "unique map",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate map",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate map after prior key",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("b", "3"),
|
||||
attribute.String("c", "4"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "3"),
|
||||
attribute.String("c", "4"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "nested map",
|
||||
value: attribute.MapValue(
|
||||
attribute.Map(
|
||||
"outer",
|
||||
attribute.String("inner", "1"),
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.Map(
|
||||
"outer",
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "map inside slice",
|
||||
value: attribute.SliceValue(
|
||||
attribute.StringValue("prior"),
|
||||
attribute.MapValue(
|
||||
attribute.String("inner", "1"),
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
attribute.StringValue("tail"),
|
||||
),
|
||||
want: attribute.SliceValue(
|
||||
attribute.StringValue("prior"),
|
||||
attribute.MapValue(
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
attribute.StringValue("tail"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "unique slice",
|
||||
value: attribute.SliceValue(
|
||||
attribute.StringValue("one"),
|
||||
attribute.IntValue(2),
|
||||
),
|
||||
want: attribute.SliceValue(
|
||||
attribute.StringValue("one"),
|
||||
attribute.IntValue(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "empty and invalid keys",
|
||||
value: attribute.MapValue(
|
||||
attribute.KeyValue{},
|
||||
attribute.String("", "empty"),
|
||||
attribute.String("valid", "value"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("", "empty"),
|
||||
attribute.String("valid", "value"),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := Value(test.value)
|
||||
if diff := cmp.Diff(test.want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueNoopAllocationFree(t *testing.T) {
|
||||
value := attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
)
|
||||
var got attribute.Value
|
||||
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
got = Value(value)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Fatalf("Value() allocations = %v, want 0", allocs)
|
||||
}
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueStorageShapes(t *testing.T) {
|
||||
for n := 0; n <= 6; n++ {
|
||||
t.Run("map", func(t *testing.T) {
|
||||
kvs := make([]attribute.KeyValue, n)
|
||||
for i := range kvs {
|
||||
kvs[i] = attribute.Int(string(rune('a'+i)), i)
|
||||
}
|
||||
value := attribute.MapValue(kvs...)
|
||||
|
||||
got := Value(value)
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("slice", func(t *testing.T) {
|
||||
values := make([]attribute.Value, n)
|
||||
for i := range values {
|
||||
values[i] = attribute.IntValue(i)
|
||||
}
|
||||
value := attribute.SliceValue(values...)
|
||||
|
||||
got := Value(value)
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValue(t *testing.T) {
|
||||
kv := attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
)
|
||||
want := attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "second"),
|
||||
)
|
||||
|
||||
got := KeyValue(kv)
|
||||
if diff := cmp.Diff(want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("KeyValue() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValuesNoopReturnsInput(t *testing.T) {
|
||||
kvs := []attribute.KeyValue{
|
||||
attribute.String("one", "1"),
|
||||
attribute.Map("two", attribute.String("nested", "value")),
|
||||
}
|
||||
|
||||
got := KeyValues(kvs)
|
||||
if len(got) != len(kvs) {
|
||||
t.Fatalf("KeyValues() length = %d, want %d", len(got), len(kvs))
|
||||
}
|
||||
if &got[0] != &kvs[0] {
|
||||
t.Fatal("KeyValues() copied a no-op input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValues(t *testing.T) {
|
||||
kvs := []attribute.KeyValue{
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("tail", "value"),
|
||||
}
|
||||
want := []attribute.KeyValue{
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("tail", "value"),
|
||||
}
|
||||
|
||||
got := KeyValues(kvs)
|
||||
if diff := cmp.Diff(want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("KeyValues() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
set := attribute.NewSet(
|
||||
attribute.String("a-top", "value"),
|
||||
attribute.Map(
|
||||
"m-map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("z-tail", "value"),
|
||||
)
|
||||
want := attribute.NewSet(
|
||||
attribute.String("a-top", "value"),
|
||||
attribute.Map(
|
||||
"m-map",
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("z-tail", "value"),
|
||||
)
|
||||
|
||||
got := Set(set)
|
||||
if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" {
|
||||
t.Fatalf("Set() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNoop(t *testing.T) {
|
||||
set := attribute.NewSet(
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map("map", attribute.String("nested", "value")),
|
||||
)
|
||||
|
||||
got := Set(set)
|
||||
if !got.Equals(&set) {
|
||||
t.Fatal("Set() changed a no-op input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEmpty(t *testing.T) {
|
||||
set := attribute.Set{}
|
||||
|
||||
got := Set(set)
|
||||
if !got.Equals(&set) {
|
||||
t.Fatal("Set() changed an empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidArrayStorage(t *testing.T) {
|
||||
if got := arrayLen("invalid", valueType); got != 0 {
|
||||
t.Fatalf("arrayLen() = %d, want 0", got)
|
||||
}
|
||||
|
||||
if got := arrayAt[attribute.Value]("invalid", valueType, 0); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
if got := arrayAt[attribute.Value](
|
||||
[1]attribute.Value{attribute.StringValue("value")},
|
||||
keyValueType,
|
||||
0,
|
||||
); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
if got := arrayAt[attribute.Value](
|
||||
[1]attribute.Value{attribute.StringValue("value")},
|
||||
valueType,
|
||||
-1,
|
||||
); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
}
|
||||
@@ -6,3 +6,5 @@ package internal // import "go.opentelemetry.io/otel/sdk/log/internal"
|
||||
|
||||
//go:generate gotmpl --body=../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/sdk/log\" }" --out=x/x.go
|
||||
//go:generate gotmpl --body=../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go
|
||||
//go:generate gotmpl --body=../../../internal/shared/attrdedup/dedup.go.tmpl "--data={}" --out=attrdedup/dedup.go
|
||||
//go:generate gotmpl --body=../../../internal/shared/attrdedup/dedup_test.go.tmpl "--data={}" --out=attrdedup/dedup_test.go
|
||||
|
||||
+15
-6
@@ -15,6 +15,7 @@ import (
|
||||
"go.opentelemetry.io/otel/log/embedded"
|
||||
"go.opentelemetry.io/otel/log/noop"
|
||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||
"go.opentelemetry.io/otel/sdk/log/internal/attrdedup"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
)
|
||||
|
||||
@@ -118,11 +119,15 @@ func (p *LoggerProvider) Logger(name string, opts ...log.LoggerOption) log.Logge
|
||||
}
|
||||
|
||||
cfg := log.NewLoggerConfig(opts...)
|
||||
attrs := cfg.InstrumentationAttributes()
|
||||
if !p.allowDupKeys {
|
||||
attrs = attrdedup.Set(attrs)
|
||||
}
|
||||
scope := instrumentation.Scope{
|
||||
Name: name,
|
||||
Version: cfg.InstrumentationVersion(),
|
||||
SchemaURL: cfg.SchemaURL(),
|
||||
Attributes: cfg.InstrumentationAttributes(),
|
||||
Attributes: attrs,
|
||||
}
|
||||
|
||||
p.loggersMu.Lock()
|
||||
@@ -257,16 +262,20 @@ func WithAttributeValueLengthLimit(limit int) LoggerProviderOption {
|
||||
})
|
||||
}
|
||||
|
||||
// WithAllowKeyDuplication sets whether deduplication is skipped for log attributes or other key-value collections.
|
||||
// WithAllowKeyDuplication sets whether deduplication is skipped for log record
|
||||
// and instrumentation scope key-value collections.
|
||||
//
|
||||
// By default, the key-value collections within a log record are deduplicated to comply with the OpenTelemetry Specification.
|
||||
// Deduplication means that if multiple key–value pairs with the same key are present, only a single pair
|
||||
// is retained and others are discarded.
|
||||
// By default, the key-value collections within a log record and
|
||||
// instrumentation scope are deduplicated to comply with the OpenTelemetry
|
||||
// Specification.
|
||||
// Deduplication means that if multiple key-value pairs with the same key are
|
||||
// present, only a single pair is retained and others are discarded. Resource
|
||||
// attributes are always deduplicated by go.opentelemetry.io/otel/sdk/resource.
|
||||
//
|
||||
// Disabling deduplication with this option can improve performance e.g. of adding attributes to the log record.
|
||||
//
|
||||
// Note that if you disable deduplication, you are responsible for ensuring that duplicate
|
||||
// key-value pairs within in a single collection are not emitted,
|
||||
// key-value pairs within a single collection are not emitted,
|
||||
// or that the telemetry receiver can handle such duplicates.
|
||||
func WithAllowKeyDuplication() LoggerProviderOption {
|
||||
return loggerProviderOptionFunc(func(cfg providerConfig) providerConfig {
|
||||
|
||||
@@ -250,6 +250,52 @@ func TestWithResource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapDeduplication(t *testing.T) {
|
||||
dup := attribute.Map(
|
||||
"map",
|
||||
attribute.String("key", "first"),
|
||||
attribute.String("key", "second"),
|
||||
)
|
||||
dedup := attribute.Map("map", attribute.String("key", "second"))
|
||||
|
||||
res := resource.NewSchemaless(dup)
|
||||
|
||||
t.Run("Resource", func(t *testing.T) {
|
||||
got := newProviderConfig([]LoggerProviderOption{WithResource(res)}).resource
|
||||
assert.Equal(t, []attribute.KeyValue{dedup}, got.Attributes())
|
||||
})
|
||||
|
||||
t.Run("ResourceAlwaysDeduplicates", func(t *testing.T) {
|
||||
got := newProviderConfig([]LoggerProviderOption{
|
||||
WithResource(res),
|
||||
WithAllowKeyDuplication(),
|
||||
}).resource
|
||||
assert.Equal(t, []attribute.KeyValue{dedup}, got.Attributes())
|
||||
})
|
||||
|
||||
t.Run("Scope", func(t *testing.T) {
|
||||
p := newProcessor("processor")
|
||||
lp := NewLoggerProvider(WithProcessor(p))
|
||||
l := lp.Logger("scope", log.WithInstrumentationAttributes(dup))
|
||||
|
||||
l.Emit(t.Context(), log.Record{})
|
||||
|
||||
require.Len(t, p.records, 1)
|
||||
assert.Equal(t, attribute.NewSet(dedup), p.records[0].InstrumentationScope().Attributes)
|
||||
})
|
||||
|
||||
t.Run("ScopeWithAllowKeyDuplication", func(t *testing.T) {
|
||||
p := newProcessor("processor")
|
||||
lp := NewLoggerProvider(WithProcessor(p), WithAllowKeyDuplication())
|
||||
l := lp.Logger("scope", log.WithInstrumentationAttributes(dup))
|
||||
|
||||
l.Emit(t.Context(), log.Record{})
|
||||
|
||||
require.Len(t, p.records, 1)
|
||||
assert.Equal(t, attribute.NewSet(dup), p.records[0].InstrumentationScope().Attributes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoggerProviderConcurrentSafe(t *testing.T) {
|
||||
const goRoutineN = 10
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"go.opentelemetry.io/otel/metric/embedded"
|
||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||
"go.opentelemetry.io/otel/sdk/metric/internal/aggregate"
|
||||
"go.opentelemetry.io/otel/sdk/metric/internal/attrdedup"
|
||||
)
|
||||
|
||||
var zeroScope instrumentation.Scope
|
||||
@@ -206,9 +207,11 @@ func extractRawKVs[T any](opts []T) []attribute.KeyValue {
|
||||
}
|
||||
|
||||
func resolveAttributes(configAttrs attribute.Set, rawKVs []attribute.KeyValue) attribute.Set {
|
||||
configAttrs = attrdedup.Set(configAttrs)
|
||||
if len(rawKVs) == 0 {
|
||||
return configAttrs
|
||||
}
|
||||
rawKVs = attrdedup.KeyValues(rawKVs)
|
||||
merged := make([]attribute.KeyValue, 0, configAttrs.Len()+len(rawKVs))
|
||||
merged = append(merged, configAttrs.ToSlice()...)
|
||||
// rawKVs are appended after configAttrs, meaning they will override any duplicate keys in configAttrs.
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/attrdedup/dedup.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package attrdedup deduplicates attribute map values.
|
||||
package attrdedup // import "go.opentelemetry.io/otel/sdk/metric/internal/attrdedup"
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
var (
|
||||
keyValueType = reflect.TypeFor[attribute.KeyValue]()
|
||||
valueType = reflect.TypeFor[attribute.Value]()
|
||||
)
|
||||
|
||||
// rawValue mirrors attribute.Value. It is used only to read immutable slice
|
||||
// storage without calling AsMap or AsSlice on no-op paths.
|
||||
type rawValue struct {
|
||||
vtype attribute.Type
|
||||
numeric uint64
|
||||
stringly string
|
||||
slice any
|
||||
}
|
||||
|
||||
// Value returns value with all map values deduplicated.
|
||||
//
|
||||
// Duplicate map keys are resolved using last-value-wins semantics.
|
||||
func Value(value attribute.Value) attribute.Value {
|
||||
value, _ = deduplicateValue(value)
|
||||
return value
|
||||
}
|
||||
|
||||
// KeyValue returns kv with all map values deduplicated.
|
||||
func KeyValue(kv attribute.KeyValue) attribute.KeyValue {
|
||||
kv, _ = deduplicateKeyValue(kv)
|
||||
return kv
|
||||
}
|
||||
|
||||
// KeyValues returns kvs with all map values deduplicated.
|
||||
//
|
||||
// The returned slice is the original kvs slice if no value needs
|
||||
// deduplication. Top-level keys in kvs are not deduplicated.
|
||||
func KeyValues(kvs []attribute.KeyValue) []attribute.KeyValue {
|
||||
kvs, _ = deduplicateKeyValues(kvs)
|
||||
return kvs
|
||||
}
|
||||
|
||||
// Set returns set with all map values deduplicated.
|
||||
//
|
||||
// The returned Set is the original set if no value needs deduplication.
|
||||
// Top-level key uniqueness remains attribute.Set's responsibility; this only
|
||||
// normalizes map attribute values.
|
||||
func Set(set attribute.Set) attribute.Set {
|
||||
if set.Len() == 0 {
|
||||
return set
|
||||
}
|
||||
|
||||
// Most attribute sets contain no duplicate map keys. Delay allocation until
|
||||
// the first changed value so the no-op path returns the original Set.
|
||||
var normalized []attribute.KeyValue
|
||||
for i := 0; i < set.Len(); i++ {
|
||||
kv, _ := set.Get(i)
|
||||
kv, changed := deduplicateKeyValue(kv)
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, 0, set.Len())
|
||||
for j := 0; j < i; j++ {
|
||||
prior, _ := set.Get(j)
|
||||
normalized = append(normalized, prior)
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
if normalized == nil {
|
||||
return set
|
||||
}
|
||||
|
||||
return attribute.NewSet(normalized...)
|
||||
}
|
||||
|
||||
func deduplicateKeyValues(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) {
|
||||
// Preserve the caller's slice on the common no-op path. Once a changed
|
||||
// value is found, copy the prior values exactly once and fill the rest in
|
||||
// place as the scan continues.
|
||||
var normalized []attribute.KeyValue
|
||||
for i, kv := range kvs {
|
||||
kv, changed := deduplicateKeyValue(kv)
|
||||
if normalized != nil {
|
||||
normalized[i] = kv
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.KeyValue, len(kvs))
|
||||
copy(normalized, kvs[:i])
|
||||
normalized[i] = kv
|
||||
}
|
||||
if normalized == nil {
|
||||
return kvs, false
|
||||
}
|
||||
return normalized, true
|
||||
}
|
||||
|
||||
func deduplicateKeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) {
|
||||
value, changed := deduplicateValue(kv.Value)
|
||||
if changed {
|
||||
kv.Value = value
|
||||
}
|
||||
return kv, changed
|
||||
}
|
||||
|
||||
func deduplicateValue(value attribute.Value) (attribute.Value, bool) {
|
||||
switch value.Type() {
|
||||
case attribute.SLICE:
|
||||
return deduplicateSliceValue(value)
|
||||
case attribute.MAP:
|
||||
return deduplicateMapValue(value)
|
||||
default:
|
||||
return value, false
|
||||
}
|
||||
}
|
||||
|
||||
func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := valueLen(storage)
|
||||
|
||||
// Slice values can contain map values, so recurse into each element while
|
||||
// keeping the original attribute.Value when no element changes.
|
||||
var normalized []attribute.Value
|
||||
for i := 0; i < length; i++ {
|
||||
elem := valueAt(storage, i)
|
||||
elem, changed := deduplicateValue(elem)
|
||||
if normalized != nil {
|
||||
normalized[i] = elem
|
||||
continue
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
normalized = make([]attribute.Value, length)
|
||||
for j := 0; j < i; j++ {
|
||||
normalized[j] = valueAt(storage, j)
|
||||
}
|
||||
normalized[i] = elem
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.SliceValue(normalized...), true
|
||||
}
|
||||
|
||||
func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) {
|
||||
storage := valueStorage(value)
|
||||
length := keyValueLen(storage)
|
||||
if length <= 1 {
|
||||
// A single map entry cannot duplicate its own key, but its value might
|
||||
// contain a map or slice that needs recursive normalization.
|
||||
if length == 1 {
|
||||
kv, changed := deduplicateKeyValue(keyValueAt(storage, 0))
|
||||
if changed {
|
||||
return attribute.MapValue(kv), true
|
||||
}
|
||||
}
|
||||
return value, false
|
||||
}
|
||||
|
||||
var normalized []attribute.KeyValue
|
||||
for i := 0; i < length; {
|
||||
// attribute.MapValue stores key-values sorted by key using a stable
|
||||
// sort. Equal keys therefore form a contiguous run, and the last
|
||||
// element in that run is the last value provided by the caller.
|
||||
first := keyValueAt(storage, i)
|
||||
j := i + 1
|
||||
for j < length && keyValueAt(storage, j).Key == first.Key {
|
||||
j++
|
||||
}
|
||||
|
||||
kv, nestedChanged := deduplicateKeyValue(keyValueAt(storage, j-1))
|
||||
// j-i > 1 means the current key run contained duplicates.
|
||||
changed := nestedChanged || j-i > 1
|
||||
if normalized != nil {
|
||||
normalized = append(normalized, kv)
|
||||
} else if changed {
|
||||
normalized = make([]attribute.KeyValue, 0, length)
|
||||
for k := 0; k < i; k++ {
|
||||
normalized = append(normalized, keyValueAt(storage, k))
|
||||
}
|
||||
normalized = append(normalized, kv)
|
||||
}
|
||||
i = j
|
||||
}
|
||||
if normalized == nil {
|
||||
return value, false
|
||||
}
|
||||
return attribute.MapValue(normalized...), true
|
||||
}
|
||||
|
||||
func valueStorage(value attribute.Value) any {
|
||||
// attribute.Value does not expose allocation-free map/slice iteration.
|
||||
// The raw mirror lets us read the immutable backing array directly and
|
||||
// reserve AsMap/AsSlice-style allocation for paths that actually change.
|
||||
return (*rawValue)(unsafe.Pointer(&value)).slice //nolint:gosec // Read-only mirror of attribute.Value for allocation-free iteration.
|
||||
}
|
||||
|
||||
func valueLen(storage any) int {
|
||||
// attribute.Value stores small slices in fixed-size array values. Handle
|
||||
// the common sizes directly and fall back to reflection for larger arrays.
|
||||
switch storage.(type) {
|
||||
case [0]attribute.Value:
|
||||
return 0
|
||||
case [1]attribute.Value:
|
||||
return 1
|
||||
case [2]attribute.Value:
|
||||
return 2
|
||||
case [3]attribute.Value:
|
||||
return 3
|
||||
case [4]attribute.Value:
|
||||
return 4
|
||||
case [5]attribute.Value:
|
||||
return 5
|
||||
default:
|
||||
return arrayLen(storage, valueType)
|
||||
}
|
||||
}
|
||||
|
||||
func valueAt(storage any, i int) attribute.Value {
|
||||
switch values := storage.(type) {
|
||||
case [1]attribute.Value:
|
||||
return values[i]
|
||||
case [2]attribute.Value:
|
||||
return values[i]
|
||||
case [3]attribute.Value:
|
||||
return values[i]
|
||||
case [4]attribute.Value:
|
||||
return values[i]
|
||||
case [5]attribute.Value:
|
||||
return values[i]
|
||||
default:
|
||||
return arrayAt[attribute.Value](storage, valueType, i)
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueLen(storage any) int {
|
||||
// attribute.Value stores small maps in fixed-size key-value arrays. Handle
|
||||
// the common sizes directly and fall back to reflection for larger arrays.
|
||||
switch storage.(type) {
|
||||
case [0]attribute.KeyValue:
|
||||
return 0
|
||||
case [1]attribute.KeyValue:
|
||||
return 1
|
||||
case [2]attribute.KeyValue:
|
||||
return 2
|
||||
case [3]attribute.KeyValue:
|
||||
return 3
|
||||
case [4]attribute.KeyValue:
|
||||
return 4
|
||||
case [5]attribute.KeyValue:
|
||||
return 5
|
||||
default:
|
||||
return arrayLen(storage, keyValueType)
|
||||
}
|
||||
}
|
||||
|
||||
func keyValueAt(storage any, i int) attribute.KeyValue {
|
||||
switch kvs := storage.(type) {
|
||||
case [1]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [2]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [3]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [4]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
case [5]attribute.KeyValue:
|
||||
return kvs[i]
|
||||
default:
|
||||
return arrayAt[attribute.KeyValue](storage, keyValueType, i)
|
||||
}
|
||||
}
|
||||
|
||||
func arrayLen(storage any, elem reflect.Type) int {
|
||||
// Be defensive around invalid or unexpected Value storage. Returning zero
|
||||
// makes malformed storage a no-op instead of panicking in telemetry paths.
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != elem {
|
||||
return 0
|
||||
}
|
||||
return array.Len()
|
||||
}
|
||||
|
||||
func arrayAt[T any](storage any, elem reflect.Type, i int) T {
|
||||
// Match arrayLen's fail-closed behavior for unexpected storage.
|
||||
array := reflect.ValueOf(storage)
|
||||
if array.Kind() != reflect.Array || array.Type().Elem() != elem || i < 0 || i >= array.Len() {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
return array.Index(i).Interface().(T)
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
// Code generated by gotmpl. DO NOT MODIFY.
|
||||
// source: internal/shared/attrdedup/dedup_test.go.tmpl
|
||||
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package attrdedup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
)
|
||||
|
||||
var cmpValue = cmp.AllowUnexported(attribute.Value{})
|
||||
|
||||
func TestValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value attribute.Value
|
||||
want attribute.Value
|
||||
}{
|
||||
{
|
||||
name: "unique map",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate map",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("one", "2"),
|
||||
attribute.String("two", "3"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "duplicate map after prior key",
|
||||
value: attribute.MapValue(
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("b", "3"),
|
||||
attribute.String("c", "4"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "3"),
|
||||
attribute.String("c", "4"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "nested map",
|
||||
value: attribute.MapValue(
|
||||
attribute.Map(
|
||||
"outer",
|
||||
attribute.String("inner", "1"),
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.Map(
|
||||
"outer",
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "map inside slice",
|
||||
value: attribute.SliceValue(
|
||||
attribute.StringValue("prior"),
|
||||
attribute.MapValue(
|
||||
attribute.String("inner", "1"),
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
attribute.StringValue("tail"),
|
||||
),
|
||||
want: attribute.SliceValue(
|
||||
attribute.StringValue("prior"),
|
||||
attribute.MapValue(
|
||||
attribute.String("inner", "2"),
|
||||
),
|
||||
attribute.StringValue("tail"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "unique slice",
|
||||
value: attribute.SliceValue(
|
||||
attribute.StringValue("one"),
|
||||
attribute.IntValue(2),
|
||||
),
|
||||
want: attribute.SliceValue(
|
||||
attribute.StringValue("one"),
|
||||
attribute.IntValue(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "empty and invalid keys",
|
||||
value: attribute.MapValue(
|
||||
attribute.KeyValue{},
|
||||
attribute.String("", "empty"),
|
||||
attribute.String("valid", "value"),
|
||||
),
|
||||
want: attribute.MapValue(
|
||||
attribute.String("", "empty"),
|
||||
attribute.String("valid", "value"),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := Value(test.value)
|
||||
if diff := cmp.Diff(test.want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueNoopAllocationFree(t *testing.T) {
|
||||
value := attribute.MapValue(
|
||||
attribute.String("one", "1"),
|
||||
attribute.String("two", "2"),
|
||||
)
|
||||
var got attribute.Value
|
||||
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
got = Value(value)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Fatalf("Value() allocations = %v, want 0", allocs)
|
||||
}
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueStorageShapes(t *testing.T) {
|
||||
for n := 0; n <= 6; n++ {
|
||||
t.Run("map", func(t *testing.T) {
|
||||
kvs := make([]attribute.KeyValue, n)
|
||||
for i := range kvs {
|
||||
kvs[i] = attribute.Int(string(rune('a'+i)), i)
|
||||
}
|
||||
value := attribute.MapValue(kvs...)
|
||||
|
||||
got := Value(value)
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("slice", func(t *testing.T) {
|
||||
values := make([]attribute.Value, n)
|
||||
for i := range values {
|
||||
values[i] = attribute.IntValue(i)
|
||||
}
|
||||
value := attribute.SliceValue(values...)
|
||||
|
||||
got := Value(value)
|
||||
if diff := cmp.Diff(value, got, cmpValue); diff != "" {
|
||||
t.Fatalf("Value() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValue(t *testing.T) {
|
||||
kv := attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
)
|
||||
want := attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "second"),
|
||||
)
|
||||
|
||||
got := KeyValue(kv)
|
||||
if diff := cmp.Diff(want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("KeyValue() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValuesNoopReturnsInput(t *testing.T) {
|
||||
kvs := []attribute.KeyValue{
|
||||
attribute.String("one", "1"),
|
||||
attribute.Map("two", attribute.String("nested", "value")),
|
||||
}
|
||||
|
||||
got := KeyValues(kvs)
|
||||
if len(got) != len(kvs) {
|
||||
t.Fatalf("KeyValues() length = %d, want %d", len(got), len(kvs))
|
||||
}
|
||||
if &got[0] != &kvs[0] {
|
||||
t.Fatal("KeyValues() copied a no-op input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyValues(t *testing.T) {
|
||||
kvs := []attribute.KeyValue{
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("tail", "value"),
|
||||
}
|
||||
want := []attribute.KeyValue{
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map(
|
||||
"map",
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("tail", "value"),
|
||||
}
|
||||
|
||||
got := KeyValues(kvs)
|
||||
if diff := cmp.Diff(want, got, cmpValue); diff != "" {
|
||||
t.Fatalf("KeyValues() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
set := attribute.NewSet(
|
||||
attribute.String("a-top", "value"),
|
||||
attribute.Map(
|
||||
"m-map",
|
||||
attribute.String("nested", "first"),
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("z-tail", "value"),
|
||||
)
|
||||
want := attribute.NewSet(
|
||||
attribute.String("a-top", "value"),
|
||||
attribute.Map(
|
||||
"m-map",
|
||||
attribute.String("nested", "second"),
|
||||
),
|
||||
attribute.String("z-tail", "value"),
|
||||
)
|
||||
|
||||
got := Set(set)
|
||||
if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" {
|
||||
t.Fatalf("Set() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetNoop(t *testing.T) {
|
||||
set := attribute.NewSet(
|
||||
attribute.String("top", "value"),
|
||||
attribute.Map("map", attribute.String("nested", "value")),
|
||||
)
|
||||
|
||||
got := Set(set)
|
||||
if !got.Equals(&set) {
|
||||
t.Fatal("Set() changed a no-op input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetEmpty(t *testing.T) {
|
||||
set := attribute.Set{}
|
||||
|
||||
got := Set(set)
|
||||
if !got.Equals(&set) {
|
||||
t.Fatal("Set() changed an empty input")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidArrayStorage(t *testing.T) {
|
||||
if got := arrayLen("invalid", valueType); got != 0 {
|
||||
t.Fatalf("arrayLen() = %d, want 0", got)
|
||||
}
|
||||
|
||||
if got := arrayAt[attribute.Value]("invalid", valueType, 0); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
if got := arrayAt[attribute.Value](
|
||||
[1]attribute.Value{attribute.StringValue("value")},
|
||||
keyValueType,
|
||||
0,
|
||||
); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
if got := arrayAt[attribute.Value](
|
||||
[1]attribute.Value{attribute.StringValue("value")},
|
||||
valueType,
|
||||
-1,
|
||||
); got.Type() != attribute.EMPTY {
|
||||
t.Fatalf("arrayAt() = %v, want empty value", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright The OpenTelemetry Authors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package internal provides internal functionality for the sdk/metric package.
|
||||
package internal // import "go.opentelemetry.io/otel/sdk/metric/internal"
|
||||
|
||||
//go:generate gotmpl --body=../../../internal/shared/x/x.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/sdk/metric\" }" --out=x/x.go
|
||||
//go:generate gotmpl --body=../../../internal/shared/x/x_test.go.tmpl "--data={}" --out=x/x_test.go
|
||||
//go:generate gotmpl --body=../../../internal/shared/attrdedup/dedup.go.tmpl "--data={}" --out=attrdedup/dedup.go
|
||||
//go:generate gotmpl --body=../../../internal/shared/attrdedup/dedup_test.go.tmpl "--data={}" --out=attrdedup/dedup_test.go
|
||||
@@ -89,6 +89,38 @@ func TestMeterInstrumentConcurrentSafe(*testing.T) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestMapDeduplication(t *testing.T) {
|
||||
t.Setenv(envVarResourceAttributes, "")
|
||||
|
||||
dup := attribute.Map(
|
||||
"map",
|
||||
attribute.String("key", "first"),
|
||||
attribute.String("key", "second"),
|
||||
)
|
||||
dedup := attribute.Map("map", attribute.String("key", "second"))
|
||||
res := resource.NewSchemaless(dup)
|
||||
|
||||
reader := NewManualReader()
|
||||
mp := NewMeterProvider(WithReader(reader), WithResource(res))
|
||||
meter := mp.Meter("scope", metric.WithInstrumentationAttributes(dup))
|
||||
counter, err := meter.Int64Counter("counter")
|
||||
require.NoError(t, err)
|
||||
|
||||
counter.Add(t.Context(), 1, metric.WithAttributes(dup))
|
||||
|
||||
var rm metricdata.ResourceMetrics
|
||||
require.NoError(t, reader.Collect(t.Context(), &rm))
|
||||
|
||||
assert.Equal(t, []attribute.KeyValue{dedup}, rm.Resource.Attributes())
|
||||
require.Len(t, rm.ScopeMetrics, 1)
|
||||
assert.Equal(t, attribute.NewSet(dedup), rm.ScopeMetrics[0].Scope.Attributes)
|
||||
require.Len(t, rm.ScopeMetrics[0].Metrics, 1)
|
||||
sum, ok := rm.ScopeMetrics[0].Metrics[0].Data.(metricdata.Sum[int64])
|
||||
require.True(t, ok)
|
||||
require.Len(t, sum.DataPoints, 1)
|
||||
assert.Equal(t, attribute.NewSet(dedup), sum.DataPoints[0].Attributes)
|
||||
}
|
||||
|
||||
var emptyCallback metric.Callback = func(context.Context, metric.Observer) error { return nil }
|
||||
|
||||
// A Meter Should be able register Callbacks Concurrently.
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"go.opentelemetry.io/otel/metric/embedded"
|
||||
"go.opentelemetry.io/otel/metric/noop"
|
||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||
"go.opentelemetry.io/otel/sdk/metric/internal/attrdedup"
|
||||
)
|
||||
|
||||
// MeterProvider handles the creation and coordination of Meters. All Meters
|
||||
@@ -76,11 +77,12 @@ func (mp *MeterProvider) Meter(name string, options ...metric.MeterOption) metri
|
||||
}
|
||||
|
||||
c := metric.NewMeterConfig(options...)
|
||||
attrs := attrdedup.Set(c.InstrumentationAttributes())
|
||||
s := instrumentation.Scope{
|
||||
Name: name,
|
||||
Version: c.InstrumentationVersion(),
|
||||
SchemaURL: c.SchemaURL(),
|
||||
Attributes: c.InstrumentationAttributes(),
|
||||
Attributes: attrs,
|
||||
}
|
||||
|
||||
global.Info(
|
||||
|
||||
@@ -23,7 +23,9 @@ type Option interface {
|
||||
apply(config) config
|
||||
}
|
||||
|
||||
// WithAttributes adds attributes to the configured Resource.
|
||||
// WithAttributes adds attributes to the configured Resource. Duplicate
|
||||
// top-level attribute keys and duplicate keys inside map values are
|
||||
// resolved using last-value-wins semantics.
|
||||
func WithAttributes(attributes ...attribute.KeyValue) Option {
|
||||
return WithDetectors(detectAttributes{attributes})
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/sdk/internal/attrdedup"
|
||||
"go.opentelemetry.io/otel/sdk/internal/x"
|
||||
)
|
||||
|
||||
@@ -47,6 +48,8 @@ var (
|
||||
var ErrSchemaURLConflict = errors.New("conflicting Schema URL")
|
||||
|
||||
// New returns a [Resource] built using opts.
|
||||
// Duplicate top-level attribute keys and duplicate keys inside map
|
||||
// values are resolved using last-value-wins semantics.
|
||||
//
|
||||
// This may return a partial Resource along with an error containing
|
||||
// [ErrPartialResource] if options that provide a [Detector] are used and that
|
||||
@@ -66,25 +69,29 @@ func New(ctx context.Context, opts ...Option) (*Resource, error) {
|
||||
return r, detect(ctx, r, cfg.detectors)
|
||||
}
|
||||
|
||||
// NewWithAttributes creates a resource from attrs and associates the resource with a
|
||||
// schema URL. If attrs contains duplicate keys, the last value will be used. If attrs
|
||||
// contains any invalid items those items will be dropped. The attrs are assumed to be
|
||||
// in a schema identified by schemaURL.
|
||||
// NewWithAttributes creates a resource from attrs and associates the resource
|
||||
// with a schema URL. If attrs contains duplicate top-level attribute keys or
|
||||
// duplicate keys inside map values, the last value will be used. If attrs
|
||||
// contains any invalid items those items will be dropped. The attrs are assumed
|
||||
// to be in a schema identified by schemaURL.
|
||||
func NewWithAttributes(schemaURL string, attrs ...attribute.KeyValue) *Resource {
|
||||
resource := NewSchemaless(attrs...)
|
||||
resource.schemaURL = schemaURL
|
||||
return resource
|
||||
}
|
||||
|
||||
// NewSchemaless creates a resource from attrs. If attrs contains duplicate keys,
|
||||
// the last value will be used. If attrs contains any invalid items those items will
|
||||
// be dropped. The resource will not be associated with a schema URL. If the schema
|
||||
// NewSchemaless creates a resource from attrs. If attrs contains duplicate
|
||||
// top-level attribute keys or duplicate keys inside map values, the last
|
||||
// value will be used. If attrs contains any invalid items those items will be
|
||||
// dropped. The resource will not be associated with a schema URL. If the schema
|
||||
// of the attrs is known use NewWithAttributes instead.
|
||||
func NewSchemaless(attrs ...attribute.KeyValue) *Resource {
|
||||
if len(attrs) == 0 {
|
||||
return &Resource{}
|
||||
}
|
||||
|
||||
attrs = attrdedup.KeyValues(attrs)
|
||||
|
||||
// Ensure attributes comply with the specification:
|
||||
// https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/common/README.md#attribute
|
||||
s, _ := attribute.NewSetWithFiltered(attrs, func(kv attribute.KeyValue) bool {
|
||||
|
||||
@@ -70,6 +70,46 @@ func TestNewWithAttributes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapDeduplication(t *testing.T) {
|
||||
attr := attribute.Map(
|
||||
"map",
|
||||
attribute.String("key", "first"),
|
||||
attribute.String("key", "second"),
|
||||
)
|
||||
want := attribute.Map("map", attribute.String("key", "second"))
|
||||
|
||||
t.Run("NewSchemaless", func(t *testing.T) {
|
||||
res := resource.NewSchemaless(attr)
|
||||
assert.Equal(t, []attribute.KeyValue{want}, res.Attributes())
|
||||
})
|
||||
|
||||
t.Run("NewWithAttributes", func(t *testing.T) {
|
||||
res := resource.NewWithAttributes(v121, attr)
|
||||
assert.Equal(t, []attribute.KeyValue{want}, res.Attributes())
|
||||
})
|
||||
|
||||
t.Run("NewWithAttributesOption", func(t *testing.T) {
|
||||
res, err := resource.New(t.Context(), resource.WithAttributes(attr))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []attribute.KeyValue{want}, res.Attributes())
|
||||
})
|
||||
|
||||
t.Run("NewWithDetector", func(t *testing.T) {
|
||||
detectorResource := resource.NewSchemaless(attr)
|
||||
res, err := resource.New(t.Context(), resource.WithDetectors(staticDetector{res: detectorResource}))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []attribute.KeyValue{want}, res.Attributes())
|
||||
})
|
||||
}
|
||||
|
||||
type staticDetector struct {
|
||||
res *resource.Resource
|
||||
}
|
||||
|
||||
func (d staticDetector) Detect(context.Context) (*resource.Resource, error) {
|
||||
return d.res, nil
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||
"go.opentelemetry.io/otel/sdk/internal/attrdedup"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
"go.opentelemetry.io/otel/sdk/trace/internal/observ"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -142,6 +143,7 @@ func (p *TracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.T
|
||||
return noop.NewTracerProvider().Tracer(name, opts...)
|
||||
}
|
||||
c := trace.NewTracerConfig(opts...)
|
||||
attrs := attrdedup.Set(c.InstrumentationAttributes())
|
||||
if name == "" {
|
||||
name = defaultTracerName
|
||||
}
|
||||
@@ -149,7 +151,7 @@ func (p *TracerProvider) Tracer(name string, opts ...trace.TracerOption) trace.T
|
||||
Name: name,
|
||||
Version: c.InstrumentationVersion(),
|
||||
SchemaURL: c.SchemaURL(),
|
||||
Attributes: c.InstrumentationAttributes(),
|
||||
Attributes: attrs,
|
||||
}
|
||||
|
||||
t, ok := func() (trace.Tracer, bool) {
|
||||
|
||||
+17
-2
@@ -19,6 +19,7 @@ import (
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/internal/global"
|
||||
"go.opentelemetry.io/otel/sdk/instrumentation"
|
||||
"go.opentelemetry.io/otel/sdk/internal/attrdedup"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
@@ -269,6 +270,7 @@ func (s *recordingSpan) SetAttributes(attributes ...attribute.KeyValue) {
|
||||
s.addDroppedAttr(1)
|
||||
continue
|
||||
}
|
||||
a = dedupAttr(a)
|
||||
a = truncateAttr(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a)
|
||||
s.attributes = append(s.attributes, a)
|
||||
}
|
||||
@@ -329,6 +331,7 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) {
|
||||
|
||||
if idx, ok := exists[a.Key]; ok {
|
||||
// Perform all updates before dropping, even when at capacity.
|
||||
a = dedupAttr(a)
|
||||
a = truncateAttr(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a)
|
||||
s.attributes[idx] = a
|
||||
continue
|
||||
@@ -339,6 +342,7 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) {
|
||||
// updates are checked and performed.
|
||||
s.addDroppedAttr(1)
|
||||
} else {
|
||||
a = dedupAttr(a)
|
||||
a = truncateAttr(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a)
|
||||
s.attributes = append(s.attributes, a)
|
||||
exists[a.Key] = len(s.attributes) - 1
|
||||
@@ -346,6 +350,15 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) {
|
||||
}
|
||||
}
|
||||
|
||||
func dedupAttr(attr attribute.KeyValue) attribute.KeyValue {
|
||||
switch attr.Value.Type() {
|
||||
case attribute.SLICE, attribute.MAP:
|
||||
return attrdedup.KeyValue(attr)
|
||||
default:
|
||||
return attr
|
||||
}
|
||||
}
|
||||
|
||||
// truncateAttr returns a truncated version of attr. Only string, string
|
||||
// slice, byte slice, slice, and map attribute values are truncated. String
|
||||
// values are truncated to at most a length of limit. Each string slice value
|
||||
@@ -717,7 +730,8 @@ func (s *recordingSpan) AddEvent(name string, o ...trace.EventOption) {
|
||||
// This method assumes s.mu.Lock is held by the caller.
|
||||
func (s *recordingSpan) addEvent(name string, o ...trace.EventOption) {
|
||||
c := trace.NewEventConfig(o...)
|
||||
e := Event{Name: name, Attributes: c.Attributes(), Time: c.Timestamp()}
|
||||
attrs := attrdedup.KeyValues(c.Attributes())
|
||||
e := Event{Name: name, Attributes: attrs, Time: c.Timestamp()}
|
||||
|
||||
// Discard attributes over limit.
|
||||
limit := s.tracer.provider.spanLimits.AttributePerEventCountLimit
|
||||
@@ -890,7 +904,8 @@ func (s *recordingSpan) AddLink(link trace.Link) {
|
||||
return
|
||||
}
|
||||
|
||||
l := Link{SpanContext: link.SpanContext, Attributes: link.Attributes}
|
||||
attrs := attrdedup.KeyValues(link.Attributes)
|
||||
l := Link{SpanContext: link.SpanContext, Attributes: attrs}
|
||||
|
||||
// Discard attributes over limit.
|
||||
limit := s.tracer.provider.spanLimits.AttributePerLinkCountLimit
|
||||
|
||||
@@ -1513,6 +1513,55 @@ func TestWithResource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapDeduplication(t *testing.T) {
|
||||
dup := attribute.Map(
|
||||
"map",
|
||||
attribute.String("key", "first"),
|
||||
attribute.String("key", "second"),
|
||||
)
|
||||
dedup := attribute.Map("map", attribute.String("key", "second"))
|
||||
res := resource.NewSchemaless(dup)
|
||||
|
||||
linkSC := trace.NewSpanContext(trace.SpanContextConfig{
|
||||
TraceID: trace.TraceID{1},
|
||||
SpanID: trace.SpanID{1},
|
||||
})
|
||||
|
||||
te := NewTestExporter()
|
||||
tp := NewTracerProvider(
|
||||
WithSyncer(te),
|
||||
WithSampler(AlwaysSample()),
|
||||
WithResource(res),
|
||||
)
|
||||
|
||||
_, span := tp.Tracer(
|
||||
"scope",
|
||||
trace.WithInstrumentationAttributes(dup),
|
||||
).Start(
|
||||
t.Context(),
|
||||
"span0",
|
||||
trace.WithAttributes(dup),
|
||||
)
|
||||
span.AddEvent("event", trace.WithAttributes(dup))
|
||||
span.AddLink(trace.Link{
|
||||
SpanContext: linkSC,
|
||||
Attributes: []attribute.KeyValue{dup},
|
||||
})
|
||||
|
||||
got, err := endSpan(te, span)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, []attribute.KeyValue{dedup}, got.Attributes())
|
||||
require.Len(t, got.Events(), 1)
|
||||
assert.Equal(t, []attribute.KeyValue{dedup}, got.Events()[0].Attributes)
|
||||
assert.Zero(t, got.Events()[0].DroppedAttributeCount)
|
||||
require.Len(t, got.Links(), 1)
|
||||
assert.Equal(t, []attribute.KeyValue{dedup}, got.Links()[0].Attributes)
|
||||
assert.Zero(t, got.Links()[0].DroppedAttributeCount)
|
||||
assert.Equal(t, []attribute.KeyValue{dedup}, got.Resource().Attributes())
|
||||
assert.Equal(t, attribute.NewSet(dedup), got.InstrumentationScope().Attributes)
|
||||
}
|
||||
|
||||
func TestWithInstrumentationVersionAndSchema(t *testing.T) {
|
||||
te := NewTestExporter()
|
||||
tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))
|
||||
|
||||
Reference in New Issue
Block a user