diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d30bafba53..f4c6f36ebf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - 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) +- Add `WithAttributeValueDepthLimit`, `DefaultAttributeValueDepthLimit`, and `SpanLimits.AttributeValueDepthLimit` in `go.opentelemetry.io/otel/sdk/trace` to configure nested attribute value depth. (#8534) +- Add `WithAttributeValueDepthLimit` in `go.opentelemetry.io/otel/sdk/log` to configure nested depth for log record and instrumentation scope attributes. (#8534) - Add `go.opentelemetry.io/otel/semconv/v1.42.0` package. (#8484) The package contains semantic conventions from the `v1.42.0` version of the OpenTelemetry Semantic Conventions. See the [migration documentation](./semconv/v1.42.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.41.0`. diff --git a/internal/shared/attrdedup/dedup.go.tmpl b/internal/shared/attrdedup/dedup.go.tmpl index c5ef89dd7af..36b7815bf2e 100644 --- a/internal/shared/attrdedup/dedup.go.tmpl +++ b/internal/shared/attrdedup/dedup.go.tmpl @@ -4,8 +4,8 @@ // DO NOT MODIFY. Generated by gotmpl. // source: internal/shared/attrdedup/dedup.go.tmpl -// Package attrdedup deduplicates attribute map values. -package attrdedup +// Package attrnorm normalizes attribute values. +package attrnorm import ( "reflect" @@ -28,40 +28,174 @@ type rawValue struct { slice any } -// Value returns value with all map values deduplicated and whether it changed. +// ValueDedup returns value with all map values deduplicated and whether it +// changed. // // Duplicate map keys are resolved using last-value-wins semantics. -func Value(value attribute.Value) (attribute.Value, bool) { +func ValueDedup(value attribute.Value) (attribute.Value, bool) { switch value.Type() { case attribute.SLICE: - return deduplicateSliceValue(value) + return sliceValueDedup(value) case attribute.MAP: - return deduplicateMapValue(value) + return mapValueDedup(value) default: return value, false } } -// KeyValue returns kv with all map values deduplicated and whether it changed. -func KeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) { - value, changed := Value(kv.Value) +// ValueWithDepthLimit returns value with all map values deduplicated and all +// slice and map values limited to depth levels. +// +// Duplicate map keys are resolved using last-value-wins semantics. When a +// slice or map value would exceed a non-negative depth limit, that value is +// replaced by an empty value. A negative depth limit disables depth limiting. +func ValueWithDepthLimit(value attribute.Value, depthLimit int) (attribute.Value, bool) { + return valueDedupWithDepthLimit(value, depthLimit, 1) +} + +// ValueLimitDepth returns value with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +// +// When a slice or map value would exceed a non-negative depth limit, that +// value is replaced by an empty value. A negative depth limit disables depth +// limiting. +func ValueLimitDepth(value attribute.Value, depthLimit int) (attribute.Value, bool) { + return valueLimitDepth(value, depthLimit, 1) +} + +func valueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + switch value.Type() { + case attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE, attribute.STRINGSLICE, attribute.BYTESLICE: + return primitiveSliceValueLimitDepth(value, depthLimit, depth) + case attribute.SLICE: + return sliceValueDedupWithDepthLimit(value, depthLimit, depth) + case attribute.MAP: + return mapValueDedupWithDepthLimit(value, depthLimit, depth) + default: + return value, false + } +} + +func valueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + switch value.Type() { + case attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE, attribute.STRINGSLICE, attribute.BYTESLICE: + return primitiveSliceValueLimitDepth(value, depthLimit, depth) + case attribute.SLICE: + return sliceValueLimitDepth(value, depthLimit, depth) + case attribute.MAP: + return mapValueLimitDepth(value, depthLimit, depth) + default: + return value, false + } +} + +// KeyValueDedup returns kv with all map values deduplicated and whether it +// changed. +func KeyValueDedup(kv attribute.KeyValue) (attribute.KeyValue, bool) { + value, changed := ValueDedup(kv.Value) + if changed { + kv.Value = value + } + return kv, changed +} + +// KeyValueWithDepthLimit returns kv with all map values deduplicated and all +// slice and map values limited to depth levels. +func KeyValueWithDepthLimit(kv attribute.KeyValue, depthLimit int) (attribute.KeyValue, bool) { + return keyValueDedupWithDepthLimit(kv, depthLimit, 1) +} + +// KeyValueLimitDepth returns kv with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +func KeyValueLimitDepth(kv attribute.KeyValue, depthLimit int) (attribute.KeyValue, bool) { + return keyValueLimitDepth(kv, depthLimit, 1) +} + +func keyValueDedupWithDepthLimit(kv attribute.KeyValue, depthLimit, depth int) (attribute.KeyValue, bool) { + value, changed := valueDedupWithDepthLimit(kv.Value, depthLimit, depth) if changed { kv.Value = value } return kv, changed } -// KeyValues returns kvs with all map values deduplicated and whether they changed. +func keyValueLimitDepth(kv attribute.KeyValue, depthLimit, depth int) (attribute.KeyValue, bool) { + value, changed := valueLimitDepth(kv.Value, depthLimit, depth) + if changed { + kv.Value = value + } + return kv, changed +} + +// KeyValuesDedup returns kvs with all map values deduplicated and whether they +// changed. // // 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, bool) { +func KeyValuesDedup(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 := KeyValueDedup(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 +} + +// KeyValuesWithDepthLimit returns kvs with all map values deduplicated and all +// slice and map values limited to depth levels. +// +// The returned slice is the original kvs slice if no value needs +// normalization. Top-level keys in kvs are not deduplicated. +func KeyValuesWithDepthLimit(kvs []attribute.KeyValue, depthLimit int) ([]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 := keyValueDedupWithDepthLimit(kv, depthLimit, 1) + 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 +} + +// KeyValuesLimitDepth returns kvs with all slice and map values limited to +// depth levels. Map keys are not deduplicated. +func KeyValuesLimitDepth(kvs []attribute.KeyValue, depthLimit int) ([]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 := KeyValue(kv) + kv, changed := keyValueLimitDepth(kv, depthLimit, 1) if normalized != nil { normalized[i] = kv continue @@ -80,12 +214,86 @@ func KeyValues(kvs []attribute.KeyValue) ([]attribute.KeyValue, bool) { return normalized, true } -// Set returns set with all map values deduplicated and whether it changed. +// SetDedup returns set with all map values deduplicated and whether it changed. // // 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, bool) { +func SetDedup(set attribute.Set) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := KeyValueDedup(kv) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +// SetWithDepthLimit returns set with all map values deduplicated and all slice +// and map values limited to depth levels. +// +// The returned Set is the original set if no value needs normalization. +// Top-level key uniqueness remains attribute.Set's responsibility; this only +// normalizes attribute values. +func SetWithDepthLimit(set attribute.Set, depthLimit int) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := keyValueDedupWithDepthLimit(kv, depthLimit, 1) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +// SetLimitDepth returns set with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +func SetLimitDepth(set attribute.Set, depthLimit int) (attribute.Set, bool) { if set.Len() == 0 { return set, false } @@ -95,7 +303,7 @@ func Set(set attribute.Set) (attribute.Set, bool) { var normalized []attribute.KeyValue for i := range set.Len() { kv, _ := set.Get(i) - kv, changed := KeyValue(kv) + kv, changed := keyValueLimitDepth(kv, depthLimit, 1) if normalized != nil { normalized = append(normalized, kv) continue @@ -118,7 +326,7 @@ func Set(set attribute.Set) (attribute.Set, bool) { return attribute.NewSet(normalized...), true } -func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) { +func sliceValueDedup(value attribute.Value) (attribute.Value, bool) { storage := valueStorage(value) length := valueLen(storage) @@ -127,7 +335,7 @@ func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) { var normalized []attribute.Value for i := range length { elem := valueAt(storage, i) - elem, changed := Value(elem) + elem, changed := ValueDedup(elem) if normalized != nil { normalized[i] = elem continue @@ -148,14 +356,137 @@ func deduplicateSliceValue(value attribute.Value) (attribute.Value, bool) { return attribute.SliceValue(normalized...), true } -func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) { +func primitiveSliceValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if !exceedsDepthLimit(depthLimit, depth) { + return value, false + } + return attribute.Value{}, true +} + +func sliceValueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + 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 := range length { + elem := valueAt(storage, i) + elem, changed := valueDedupWithDepthLimit(elem, depthLimit, depth+1) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func sliceValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + storage := valueStorage(value) + length := valueLen(storage) + + var normalized []attribute.Value + for i := range length { + elem := valueAt(storage, i) + elem, changed := valueLimitDepth(elem, depthLimit, depth+1) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func mapValueDedup(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 := KeyValueDedup(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 := KeyValueDedup(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 := range i { + normalized = append(normalized, keyValueAt(storage, k)) + } + normalized = append(normalized, kv) + } + i = j + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func mapValueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + 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 := KeyValue(keyValueAt(storage, 0)) + kv, changed := keyValueDedupWithDepthLimit(keyValueAt(storage, 0), depthLimit, depth+1) if changed { return attribute.MapValue(kv), true } @@ -174,7 +505,7 @@ func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) { j++ } - kv, nestedChanged := KeyValue(keyValueAt(storage, j-1)) + kv, nestedChanged := keyValueDedupWithDepthLimit(keyValueAt(storage, j-1), depthLimit, depth+1) // j-i > 1 means the current key run contained duplicates. changed := nestedChanged || j-i > 1 if normalized != nil { @@ -194,6 +525,42 @@ func deduplicateMapValue(value attribute.Value) (attribute.Value, bool) { return attribute.MapValue(normalized...), true } +func mapValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + storage := valueStorage(value) + length := keyValueLen(storage) + + var normalized []attribute.KeyValue + for i := range length { + kv := keyValueAt(storage, i) + kv, changed := keyValueLimitDepth(kv, depthLimit, depth+1) + if normalized != nil { + normalized[i] = kv + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, length) + for j := range i { + normalized[j] = keyValueAt(storage, j) + } + normalized[i] = kv + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func exceedsDepthLimit(depthLimit, depth int) bool { + return depthLimit >= 0 && depth > depthLimit +} + 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 diff --git a/internal/shared/attrdedup/dedup_test.go.tmpl b/internal/shared/attrdedup/dedup_test.go.tmpl index 82fab043ab4..038c90c5ad1 100644 --- a/internal/shared/attrdedup/dedup_test.go.tmpl +++ b/internal/shared/attrdedup/dedup_test.go.tmpl @@ -4,7 +4,7 @@ // DO NOT MODIFY. Generated by gotmpl. // source: internal/shared/attrdedup/dedup_test.go.tmpl -package attrdedup +package attrnorm import ( "testing" @@ -16,7 +16,7 @@ import ( var cmpValue = cmp.AllowUnexported(attribute.Value{}) -func TestValue(t *testing.T) { +func TestValueDedup(t *testing.T) { tests := []struct { name string value attribute.Value @@ -128,18 +128,591 @@ func TestValue(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got, changed := Value(test.value) + got, changed := ValueDedup(test.value) if changed != test.wantChanged { - t.Fatalf("Value() changed = %v, want %v", changed, test.wantChanged) + t.Fatalf("ValueDedup() changed = %v, want %v", changed, test.wantChanged) } if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { - t.Fatalf("Value() mismatch (-want +got):\n%s", diff) + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) } }) } } -func TestValueNoopAllocationFree(t *testing.T) { +func TestValueWithDepthLimit(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "below limit", + limit: 3, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + }, + { + name: "at limit", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + }, + { + name: "multi-entry map below limit", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + }, + { + name: "deduplicate multi-entry map", + limit: 2, + value: attribute.MapValue( + attribute.String("duplicate", "first"), + attribute.String("duplicate", "second"), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("duplicate", "second"), + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "deduplicate multi-entry map after prior key", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.String("middle", "first"), + attribute.String("middle", "second"), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.String("middle", "second"), + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "slice below limit", + limit: 2, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + }, + { + name: "map over limit", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.KeyValue{Key: "level2"}, + ), + ), + wantChanged: true, + }, + { + name: "slice over limit", + limit: 1, + value: attribute.SliceValue( + attribute.SliceValue(attribute.StringValue("over")), + attribute.StringValue("leaf"), + ), + want: attribute.SliceValue( + attribute.Value{}, + attribute.StringValue("leaf"), + ), + wantChanged: true, + }, + { + name: "slice over limit after scalar", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("over")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.Value{}, + attribute.StringValue("tail"), + ), + wantChanged: true, + }, + { + name: "zero allows scalar", + limit: 0, + value: attribute.StringValue("leaf"), + want: attribute.StringValue("leaf"), + wantChanged: false, + }, + { + name: "zero replaces slice", + limit: 0, + value: attribute.SliceValue(attribute.StringValue("leaf")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "zero replaces map", + limit: 0, + value: attribute.MapValue(attribute.String("leaf", "value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "negative disables depth limit", + limit: -1, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map( + "level3", + attribute.String("leaf", "value"), + ), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map( + "level3", + attribute.String("leaf", "value"), + ), + ), + ), + ), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueWithDepthLimit(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("ValueWithDepthLimit() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueWithDepthLimit() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueDepthLimitSpecCases(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "raw limit zero preserves scalar", + limit: 0, + value: attribute.StringValue("value"), + want: attribute.StringValue("value"), + wantChanged: false, + }, + { + name: "raw limit zero replaces top-level heterogeneous array", + limit: 0, + value: attribute.SliceValue(attribute.StringValue("value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "raw limit zero replaces top-level map", + limit: 0, + value: attribute.MapValue(attribute.String("key", "value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "limit one preserves top-level heterogeneous array and replaces nested collections", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.MapValue(attribute.String("nested", "value")), + ), + want: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.Value{}, + attribute.Value{}, + ), + wantChanged: true, + }, + { + name: "limit one replaces nested homogeneous array in heterogeneous array", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.StringSliceValue([]string{"nested"}), + ), + want: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.Value{}, + ), + wantChanged: true, + }, + { + name: "limit one preserves top-level map and replaces nested collections", + limit: 1, + value: attribute.MapValue( + attribute.Slice("array", attribute.StringValue("nested")), + attribute.Map("map", attribute.String("nested", "value")), + attribute.StringSlice("primitive-array", []string{"nested"}), + attribute.String("scalar", "value"), + ), + want: attribute.MapValue( + attribute.KeyValue{Key: "array"}, + attribute.KeyValue{Key: "map"}, + attribute.KeyValue{Key: "primitive-array"}, + attribute.String("scalar", "value"), + ), + wantChanged: true, + }, + { + name: "nested heterogeneous array beyond limit replaced", + limit: 2, + value: attribute.SliceValue( + attribute.SliceValue( + attribute.SliceValue(attribute.StringValue("over")), + ), + ), + want: attribute.SliceValue( + attribute.SliceValue( + attribute.Value{}, + ), + ), + wantChanged: true, + }, + { + name: "negative limit preserves nested collections", + limit: -1, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Slice( + "level2", + attribute.MapValue(attribute.String("leaf", "value")), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Slice( + "level2", + attribute.MapValue(attribute.String("leaf", "value")), + ), + ), + ), + wantChanged: false, + }, + { + name: "otherwise value is unchanged", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + wantChanged: false, + }, + } + + fns := []struct { + name string + fn func(attribute.Value, int) (attribute.Value, bool) + }{ + { + name: "ValueWithDepthLimit", + fn: ValueWithDepthLimit, + }, + { + name: "ValueLimitDepth", + fn: ValueLimitDepth, + }, + } + + for _, fn := range fns { + t.Run(fn.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := fn.fn(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("%s() changed = %v, want %v", fn.name, changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("%s() mismatch (-want +got):\n%s", fn.name, diff) + } + }) + } + }) + } +} + +func TestValueDepthLimitPrimitiveSliceTypes(t *testing.T) { + tests := []struct { + name string + value attribute.Value + want attribute.Value + }{ + { + name: "bool", + value: attribute.BoolSliceValue([]bool{true}), + want: attribute.Value{}, + }, + { + name: "int64", + value: attribute.Int64SliceValue([]int64{1}), + want: attribute.Value{}, + }, + { + name: "float64", + value: attribute.Float64SliceValue([]float64{1}), + want: attribute.Value{}, + }, + { + name: "string", + value: attribute.StringSliceValue([]string{"value"}), + want: attribute.Value{}, + }, + { + name: "bytes", + value: attribute.ByteSliceValue([]byte("value")), + want: attribute.Value{}, + }, + } + + fns := []struct { + name string + fn func(attribute.Value, int) (attribute.Value, bool) + }{ + { + name: "ValueWithDepthLimit", + fn: ValueWithDepthLimit, + }, + { + name: "ValueLimitDepth", + fn: ValueLimitDepth, + }, + } + + for _, fn := range fns { + t.Run(fn.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := fn.fn(test.value, 0) + if !changed { + t.Fatalf("%s() changed = false, want true", fn.name) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("%s() limit 0 mismatch (-want +got):\n%s", fn.name, diff) + } + + got, changed = fn.fn(test.value, 1) + if changed { + t.Fatalf("%s() changed = true, want false", fn.name) + } + if diff := cmp.Diff(test.value, got, cmpValue); diff != "" { + t.Fatalf("%s() limit 1 mismatch (-want +got):\n%s", fn.name, diff) + } + }) + } + }) + } +} + +func TestValueLimitDepth(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "scalar", + limit: 1, + value: attribute.StringValue("value"), + want: attribute.StringValue("value"), + wantChanged: false, + }, + { + name: "slice below limit", + limit: 2, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + wantChanged: false, + }, + { + name: "map below limit", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + wantChanged: false, + }, + { + name: "map over limit after prior key", + limit: 1, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map("middle", attribute.String("leaf", "value")), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.KeyValue{Key: "middle"}, + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "slice over limit", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.Value{}, + attribute.StringValue("tail"), + ), + wantChanged: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueLimitDepth(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("ValueLimitDepth() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueLimitDepth() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueLimitDepthPreservesDuplicateMapKeys(t *testing.T) { + value := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.KeyValue{Key: "over"}, + ) + + got, changed := ValueLimitDepth(value, 1) + if !changed { + t.Fatal("ValueLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("ValueLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestValueDedupNoopAllocationFree(t *testing.T) { value := attribute.MapValue( attribute.String("one", "1"), attribute.String("two", "2"), @@ -147,20 +720,20 @@ func TestValueNoopAllocationFree(t *testing.T) { var got attribute.Value allocs := testing.AllocsPerRun(1000, func() { - got, _ = Value(value) + got, _ = ValueDedup(value) }) if allocs != 0 { - t.Fatalf("Value() allocations = %v, want 0", allocs) + t.Fatalf("ValueDedup() allocations = %v, want 0", allocs) } - if _, changed := Value(value); changed { - t.Fatal("Value() changed a no-op input") + if _, changed := ValueDedup(value); changed { + t.Fatal("ValueDedup() changed a no-op input") } if diff := cmp.Diff(value, got, cmpValue); diff != "" { - t.Fatalf("Value() mismatch (-want +got):\n%s", diff) + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) } } -func TestValueStorageShapes(t *testing.T) { +func TestValueDedupStorageShapes(t *testing.T) { for n := 0; n <= 6; n++ { t.Run("map", func(t *testing.T) { kvs := make([]attribute.KeyValue, n) @@ -169,12 +742,12 @@ func TestValueStorageShapes(t *testing.T) { } value := attribute.MapValue(kvs...) - got, changed := Value(value) + got, changed := ValueDedup(value) if changed { - t.Fatal("Value() changed a no-op input") + t.Fatal("ValueDedup() changed a no-op input") } if diff := cmp.Diff(value, got, cmpValue); diff != "" { - t.Fatalf("Value() mismatch (-want +got):\n%s", diff) + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) } }) t.Run("slice", func(t *testing.T) { @@ -184,18 +757,18 @@ func TestValueStorageShapes(t *testing.T) { } value := attribute.SliceValue(values...) - got, changed := Value(value) + got, changed := ValueDedup(value) if changed { - t.Fatal("Value() changed a no-op input") + t.Fatal("ValueDedup() changed a no-op input") } if diff := cmp.Diff(value, got, cmpValue); diff != "" { - t.Fatalf("Value() mismatch (-want +got):\n%s", diff) + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) } }) } } -func TestKeyValue(t *testing.T) { +func TestKeyValueDedup(t *testing.T) { kv := attribute.Map( "map", attribute.String("nested", "first"), @@ -206,12 +779,60 @@ func TestKeyValue(t *testing.T) { attribute.String("nested", "second"), ) - got, changed := KeyValue(kv) + got, changed := KeyValueDedup(kv) + if !changed { + t.Fatal("KeyValueDedup() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValueWithDepthLimit(t *testing.T) { + kv := attribute.Map( + "map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ) + want := attribute.Map( + "map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ) + + got, changed := KeyValueWithDepthLimit(kv, 2) + if !changed { + t.Fatal("KeyValueWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValueLimitDepth(t *testing.T) { + kv := attribute.Map( + "map", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.Map( + "map", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.KeyValue{Key: "over"}, + ) + + got, changed := KeyValueLimitDepth(kv, 1) if !changed { - t.Fatal("KeyValue() changed = false, want true") + t.Fatal("KeyValueLimitDepth() changed = false, want true") } if diff := cmp.Diff(want, got, cmpValue); diff != "" { - t.Fatalf("KeyValue() mismatch (-want +got):\n%s", diff) + t.Fatalf("KeyValueLimitDepth() mismatch (-want +got):\n%s", diff) } } @@ -221,19 +842,19 @@ func TestKeyValuesNoopReturnsInput(t *testing.T) { attribute.Map("two", attribute.String("nested", "value")), } - got, changed := KeyValues(kvs) + got, changed := KeyValuesDedup(kvs) if changed { - t.Fatal("KeyValues() changed a no-op input") + t.Fatal("KeyValuesDedup() changed a no-op input") } if len(got) != len(kvs) { - t.Fatalf("KeyValues() length = %d, want %d", len(got), len(kvs)) + t.Fatalf("KeyValuesDedup() length = %d, want %d", len(got), len(kvs)) } if &got[0] != &kvs[0] { - t.Fatal("KeyValues() copied a no-op input") + t.Fatal("KeyValuesDedup() copied a no-op input") } } -func TestKeyValues(t *testing.T) { +func TestKeyValuesDedup(t *testing.T) { kvs := []attribute.KeyValue{ attribute.String("top", "value"), attribute.Map( @@ -252,16 +873,111 @@ func TestKeyValues(t *testing.T) { attribute.String("tail", "value"), } - got, changed := KeyValues(kvs) + got, changed := KeyValuesDedup(kvs) if !changed { - t.Fatal("KeyValues() changed = false, want true") + t.Fatal("KeyValuesDedup() changed = false, want true") } if diff := cmp.Diff(want, got, cmpValue); diff != "" { - t.Fatalf("KeyValues() mismatch (-want +got):\n%s", diff) + t.Fatalf("KeyValuesDedup() mismatch (-want +got):\n%s", diff) } } -func TestSet(t *testing.T) { +func TestKeyValuesWithDepthLimit(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + attribute.String("tail", "value"), + } + want := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ), + attribute.String("tail", "value"), + } + + got, changed := KeyValuesWithDepthLimit(kvs, 2) + if !changed { + t.Fatal("KeyValuesWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValuesDepthLimitNoopReturnsInput(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("one", "1"), + attribute.Map("two", attribute.String("nested", "value")), + } + tests := []struct { + name string + fn func([]attribute.KeyValue, int) ([]attribute.KeyValue, bool) + }{ + { + name: "KeyValuesWithDepthLimit", + fn: KeyValuesWithDepthLimit, + }, + { + name: "KeyValuesLimitDepth", + fn: KeyValuesLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(kvs, 2) + if changed { + t.Fatalf("%s() changed a no-op input", test.name) + } + if len(got) != len(kvs) { + t.Fatalf("%s() length = %d, want %d", test.name, len(got), len(kvs)) + } + if &got[0] != &kvs[0] { + t.Fatalf("%s() copied a no-op input", test.name) + } + }) + } +} + +func TestKeyValuesLimitDepth(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map("over", attribute.String("leaf", "value")), + ), + attribute.String("tail", "value"), + } + want := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.KeyValue{Key: "over"}, + ), + attribute.String("tail", "value"), + } + + got, changed := KeyValuesLimitDepth(kvs, 1) + if !changed { + t.Fatal("KeyValuesLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetDedup(t *testing.T) { set := attribute.NewSet( attribute.String("a-top", "value"), attribute.Map( @@ -280,39 +996,160 @@ func TestSet(t *testing.T) { attribute.String("z-tail", "value"), ) - got, changed := Set(set) + got, changed := SetDedup(set) if !changed { - t.Fatal("Set() changed = false, want true") + t.Fatal("SetDedup() changed = false, want true") } if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { - t.Fatalf("Set() mismatch (-want +got):\n%s", diff) + t.Fatalf("SetDedup() mismatch (-want +got):\n%s", diff) } } -func TestSetNoop(t *testing.T) { +func TestSetWithDepthLimit(t *testing.T) { + set := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + attribute.String("z-tail", "value"), + ) + want := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ), + attribute.String("z-tail", "value"), + ) + + got, changed := SetWithDepthLimit(set, 2) + if !changed { + t.Fatal("SetWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetLimitDepth(t *testing.T) { + set := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map("over", attribute.String("leaf", "value")), + ), + attribute.String("z-tail", "value"), + ) + want := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.KeyValue{Key: "over"}, + ), + attribute.String("z-tail", "value"), + ) + + got, changed := SetLimitDepth(set, 1) + if !changed { + t.Fatal("SetLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetDepthLimitNoop(t *testing.T) { + set := attribute.NewSet( + attribute.String("top", "value"), + attribute.Map("map", attribute.String("nested", "value")), + ) + tests := []struct { + name string + fn func(attribute.Set, int) (attribute.Set, bool) + }{ + { + name: "SetWithDepthLimit", + fn: SetWithDepthLimit, + }, + { + name: "SetLimitDepth", + fn: SetLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(set, 2) + if changed { + t.Fatalf("%s() changed a no-op input", test.name) + } + if !got.Equals(&set) { + t.Fatalf("%s() changed a no-op input", test.name) + } + }) + } +} + +func TestSetDedupNoop(t *testing.T) { set := attribute.NewSet( attribute.String("top", "value"), attribute.Map("map", attribute.String("nested", "value")), ) - got, changed := Set(set) + got, changed := SetDedup(set) if changed { - t.Fatal("Set() changed a no-op input") + t.Fatal("SetDedup() changed a no-op input") } if !got.Equals(&set) { - t.Fatal("Set() changed a no-op input") + t.Fatal("SetDedup() changed a no-op input") } } -func TestSetEmpty(t *testing.T) { +func TestSetDedupEmpty(t *testing.T) { set := attribute.Set{} - got, changed := Set(set) + got, changed := SetDedup(set) if changed { - t.Fatal("Set() changed an empty input") + t.Fatal("SetDedup() changed an empty input") } if !got.Equals(&set) { - t.Fatal("Set() changed an empty input") + t.Fatal("SetDedup() changed an empty input") + } +} + +func TestSetDepthLimitEmpty(t *testing.T) { + set := attribute.Set{} + tests := []struct { + name string + fn func(attribute.Set, int) (attribute.Set, bool) + }{ + { + name: "SetWithDepthLimit", + fn: SetWithDepthLimit, + }, + { + name: "SetLimitDepth", + fn: SetLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(set, 1) + if changed { + t.Fatalf("%s() changed an empty input", test.name) + } + if !got.Equals(&set) { + t.Fatalf("%s() changed an empty input", test.name) + } + }) } } diff --git a/sdk/internal/attrdedup/dedup.go b/sdk/internal/attrdedup/dedup.go deleted file mode 100644 index 524bb4bce77..00000000000 --- a/sdk/internal/attrdedup/dedup.go +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// DO NOT MODIFY. Generated by gotmpl. -// source: internal/shared/attrdedup/dedup.go.tmpl - -// 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 and whether it changed. -// -// Duplicate map keys are resolved using last-value-wins semantics. -func Value(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 - } -} - -// KeyValue returns kv with all map values deduplicated and whether it changed. -func KeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) { - value, changed := Value(kv.Value) - if changed { - kv.Value = value - } - return kv, changed -} - -// KeyValues returns kvs with all map values deduplicated and whether they changed. -// -// 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, 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 := KeyValue(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 -} - -// Set returns set with all map values deduplicated and whether it changed. -// -// 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, bool) { - if set.Len() == 0 { - return set, false - } - - // 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 := range set.Len() { - kv, _ := set.Get(i) - kv, changed := KeyValue(kv) - if normalized != nil { - normalized = append(normalized, kv) - continue - } - if !changed { - continue - } - - normalized = make([]attribute.KeyValue, 0, set.Len()) - for j := range i { - prior, _ := set.Get(j) - normalized = append(normalized, prior) - } - normalized = append(normalized, kv) - } - if normalized == nil { - return set, false - } - - return attribute.NewSet(normalized...), true -} - -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 := range length { - elem := valueAt(storage, i) - elem, changed := Value(elem) - if normalized != nil { - normalized[i] = elem - continue - } - if !changed { - continue - } - - normalized = make([]attribute.Value, length) - for j := range i { - 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 := KeyValue(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 := KeyValue(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 := range i { - 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) -} diff --git a/sdk/internal/attrdedup/dedup_benchmark_test.go b/sdk/internal/attrdedup/dedup_benchmark_test.go deleted file mode 100644 index 6d4c1fe37c2..00000000000 --- a/sdk/internal/attrdedup/dedup_benchmark_test.go +++ /dev/null @@ -1,54 +0,0 @@ -// 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) - } - }) - } -} diff --git a/sdk/internal/attrdedup/dedup_test.go b/sdk/internal/attrdedup/dedup_test.go deleted file mode 100644 index 82fab043ab4..00000000000 --- a/sdk/internal/attrdedup/dedup_test.go +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// DO NOT MODIFY. Generated by gotmpl. -// source: internal/shared/attrdedup/dedup_test.go.tmpl - -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 - wantChanged bool - }{ - { - 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"), - ), - wantChanged: false, - }, - { - 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"), - ), - wantChanged: true, - }, - { - 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"), - ), - wantChanged: true, - }, - { - 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"), - ), - ), - wantChanged: true, - }, - { - 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"), - ), - wantChanged: true, - }, - { - name: "unique slice", - value: attribute.SliceValue( - attribute.StringValue("one"), - attribute.IntValue(2), - ), - want: attribute.SliceValue( - attribute.StringValue("one"), - attribute.IntValue(2), - ), - wantChanged: false, - }, - { - 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"), - ), - wantChanged: true, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got, changed := Value(test.value) - if changed != test.wantChanged { - t.Fatalf("Value() changed = %v, want %v", changed, test.wantChanged) - } - 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 _, changed := Value(value); changed { - t.Fatal("Value() changed a no-op input") - } - 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, changed := Value(value) - if changed { - t.Fatal("Value() changed a no-op input") - } - 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, changed := Value(value) - if changed { - t.Fatal("Value() changed a no-op input") - } - 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, changed := KeyValue(kv) - if !changed { - t.Fatal("KeyValue() changed = false, want true") - } - 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, changed := KeyValues(kvs) - if changed { - t.Fatal("KeyValues() changed a no-op input") - } - 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, changed := KeyValues(kvs) - if !changed { - t.Fatal("KeyValues() changed = false, want true") - } - 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, changed := Set(set) - if !changed { - t.Fatal("Set() changed = false, want true") - } - 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, changed := Set(set) - if changed { - t.Fatal("Set() changed a no-op input") - } - if !got.Equals(&set) { - t.Fatal("Set() changed a no-op input") - } -} - -func TestSetEmpty(t *testing.T) { - set := attribute.Set{} - - got, changed := Set(set) - if changed { - t.Fatal("Set() changed an empty input") - } - 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) - } -} diff --git a/sdk/internal/attrnorm/dedup.go b/sdk/internal/attrnorm/dedup.go new file mode 100644 index 00000000000..3533b78ccee --- /dev/null +++ b/sdk/internal/attrnorm/dedup.go @@ -0,0 +1,667 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/attrdedup/dedup.go.tmpl + +// Package attrnorm normalizes attribute values. +package attrnorm // import "go.opentelemetry.io/otel/sdk/internal/attrnorm" + +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 +} + +// ValueDedup returns value with all map values deduplicated and whether it +// changed. +// +// Duplicate map keys are resolved using last-value-wins semantics. +func ValueDedup(value attribute.Value) (attribute.Value, bool) { + switch value.Type() { + case attribute.SLICE: + return sliceValueDedup(value) + case attribute.MAP: + return mapValueDedup(value) + default: + return value, false + } +} + +// ValueWithDepthLimit returns value with all map values deduplicated and all +// slice and map values limited to depth levels. +// +// Duplicate map keys are resolved using last-value-wins semantics. When a +// slice or map value would exceed a non-negative depth limit, that value is +// replaced by an empty value. A negative depth limit disables depth limiting. +func ValueWithDepthLimit(value attribute.Value, depthLimit int) (attribute.Value, bool) { + return valueDedupWithDepthLimit(value, depthLimit, 1) +} + +// ValueLimitDepth returns value with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +// +// When a slice or map value would exceed a non-negative depth limit, that +// value is replaced by an empty value. A negative depth limit disables depth +// limiting. +func ValueLimitDepth(value attribute.Value, depthLimit int) (attribute.Value, bool) { + return valueLimitDepth(value, depthLimit, 1) +} + +func valueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + switch value.Type() { + case attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE, attribute.STRINGSLICE, attribute.BYTESLICE: + return primitiveSliceValueLimitDepth(value, depthLimit, depth) + case attribute.SLICE: + return sliceValueDedupWithDepthLimit(value, depthLimit, depth) + case attribute.MAP: + return mapValueDedupWithDepthLimit(value, depthLimit, depth) + default: + return value, false + } +} + +func valueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + switch value.Type() { + case attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE, attribute.STRINGSLICE, attribute.BYTESLICE: + return primitiveSliceValueLimitDepth(value, depthLimit, depth) + case attribute.SLICE: + return sliceValueLimitDepth(value, depthLimit, depth) + case attribute.MAP: + return mapValueLimitDepth(value, depthLimit, depth) + default: + return value, false + } +} + +// KeyValueDedup returns kv with all map values deduplicated and whether it +// changed. +func KeyValueDedup(kv attribute.KeyValue) (attribute.KeyValue, bool) { + value, changed := ValueDedup(kv.Value) + if changed { + kv.Value = value + } + return kv, changed +} + +// KeyValueWithDepthLimit returns kv with all map values deduplicated and all +// slice and map values limited to depth levels. +func KeyValueWithDepthLimit(kv attribute.KeyValue, depthLimit int) (attribute.KeyValue, bool) { + return keyValueDedupWithDepthLimit(kv, depthLimit, 1) +} + +// KeyValueLimitDepth returns kv with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +func KeyValueLimitDepth(kv attribute.KeyValue, depthLimit int) (attribute.KeyValue, bool) { + return keyValueLimitDepth(kv, depthLimit, 1) +} + +func keyValueDedupWithDepthLimit(kv attribute.KeyValue, depthLimit, depth int) (attribute.KeyValue, bool) { + value, changed := valueDedupWithDepthLimit(kv.Value, depthLimit, depth) + if changed { + kv.Value = value + } + return kv, changed +} + +func keyValueLimitDepth(kv attribute.KeyValue, depthLimit, depth int) (attribute.KeyValue, bool) { + value, changed := valueLimitDepth(kv.Value, depthLimit, depth) + if changed { + kv.Value = value + } + return kv, changed +} + +// KeyValuesDedup returns kvs with all map values deduplicated and whether they +// changed. +// +// The returned slice is the original kvs slice if no value needs +// deduplication. Top-level keys in kvs are not deduplicated. +func KeyValuesDedup(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 := KeyValueDedup(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 +} + +// KeyValuesWithDepthLimit returns kvs with all map values deduplicated and all +// slice and map values limited to depth levels. +// +// The returned slice is the original kvs slice if no value needs +// normalization. Top-level keys in kvs are not deduplicated. +func KeyValuesWithDepthLimit(kvs []attribute.KeyValue, depthLimit int) ([]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 := keyValueDedupWithDepthLimit(kv, depthLimit, 1) + 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 +} + +// KeyValuesLimitDepth returns kvs with all slice and map values limited to +// depth levels. Map keys are not deduplicated. +func KeyValuesLimitDepth(kvs []attribute.KeyValue, depthLimit int) ([]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 := keyValueLimitDepth(kv, depthLimit, 1) + 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 +} + +// SetDedup returns set with all map values deduplicated and whether it changed. +// +// 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 SetDedup(set attribute.Set) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := KeyValueDedup(kv) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +// SetWithDepthLimit returns set with all map values deduplicated and all slice +// and map values limited to depth levels. +// +// The returned Set is the original set if no value needs normalization. +// Top-level key uniqueness remains attribute.Set's responsibility; this only +// normalizes attribute values. +func SetWithDepthLimit(set attribute.Set, depthLimit int) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := keyValueDedupWithDepthLimit(kv, depthLimit, 1) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +// SetLimitDepth returns set with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +func SetLimitDepth(set attribute.Set, depthLimit int) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := keyValueLimitDepth(kv, depthLimit, 1) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +func sliceValueDedup(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 := range length { + elem := valueAt(storage, i) + elem, changed := ValueDedup(elem) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func primitiveSliceValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if !exceedsDepthLimit(depthLimit, depth) { + return value, false + } + return attribute.Value{}, true +} + +func sliceValueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + 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 := range length { + elem := valueAt(storage, i) + elem, changed := valueDedupWithDepthLimit(elem, depthLimit, depth+1) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func sliceValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + storage := valueStorage(value) + length := valueLen(storage) + + var normalized []attribute.Value + for i := range length { + elem := valueAt(storage, i) + elem, changed := valueLimitDepth(elem, depthLimit, depth+1) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func mapValueDedup(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 := KeyValueDedup(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 := KeyValueDedup(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 := range i { + normalized = append(normalized, keyValueAt(storage, k)) + } + normalized = append(normalized, kv) + } + i = j + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func mapValueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + 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 := keyValueDedupWithDepthLimit(keyValueAt(storage, 0), depthLimit, depth+1) + 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 := keyValueDedupWithDepthLimit(keyValueAt(storage, j-1), depthLimit, depth+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 := range i { + normalized = append(normalized, keyValueAt(storage, k)) + } + normalized = append(normalized, kv) + } + i = j + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func mapValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + storage := valueStorage(value) + length := keyValueLen(storage) + + var normalized []attribute.KeyValue + for i := range length { + kv := keyValueAt(storage, i) + kv, changed := keyValueLimitDepth(kv, depthLimit, depth+1) + if normalized != nil { + normalized[i] = kv + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, length) + for j := range i { + normalized[j] = keyValueAt(storage, j) + } + normalized[i] = kv + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func exceedsDepthLimit(depthLimit, depth int) bool { + return depthLimit >= 0 && depth > depthLimit +} + +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) +} diff --git a/sdk/internal/attrnorm/dedup_benchmark_test.go b/sdk/internal/attrnorm/dedup_benchmark_test.go new file mode 100644 index 00000000000..a57d7a39391 --- /dev/null +++ b/sdk/internal/attrnorm/dedup_benchmark_test.go @@ -0,0 +1,216 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package attrnorm_test + +import ( + "testing" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/internal/attrnorm" +) + +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() { + attrnorm.ValueDedup(value.value) + } + }) + } +} + +func BenchmarkValueWithDepthLimit(b *testing.B) { + values := []struct { + name string + value attribute.Value + depthLimit int + }{ + { + name: "ScalarNoop", + value: attribute.StringValue("value"), + depthLimit: 2, + }, + { + name: "NestedNoop", + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + depthLimit: 2, + }, + { + name: "LimitHit", + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + depthLimit: 2, + }, + } + + for _, value := range values { + b.Run(value.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + attrnorm.ValueWithDepthLimit(value.value, value.depthLimit) + } + }) + } +} + +func BenchmarkKeyValuesWithDepthLimit(b *testing.B) { + values := []struct { + name string + values []attribute.KeyValue + depthLimit int + }{ + { + name: "ScalarNoop", + values: []attribute.KeyValue{ + attribute.String("one", "1"), + attribute.Int("two", 2), + attribute.Bool("three", true), + attribute.Float64("four", 4.0), + }, + depthLimit: 2, + }, + { + name: "NestedNoop", + values: []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + }, + depthLimit: 2, + }, + { + name: "LimitHit", + values: []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "nested", + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + attribute.String("tail", "value"), + }, + depthLimit: 2, + }, + } + + for _, value := range values { + b.Run(value.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + attrnorm.KeyValuesWithDepthLimit(value.values, value.depthLimit) + } + }) + } +} + +func BenchmarkSetWithDepthLimit(b *testing.B) { + values := []struct { + name string + set attribute.Set + depthLimit int + }{ + { + name: "ScalarNoop", + set: attribute.NewSet( + attribute.String("one", "1"), + attribute.Int("two", 2), + attribute.Bool("three", true), + attribute.Float64("four", 4.0), + ), + depthLimit: 2, + }, + { + name: "NestedNoop", + set: attribute.NewSet( + attribute.String("top", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + depthLimit: 2, + }, + { + name: "LimitHit", + set: attribute.NewSet( + attribute.String("top", "value"), + attribute.Map( + "nested", + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + attribute.String("tail", "value"), + ), + depthLimit: 2, + }, + } + + for _, value := range values { + b.Run(value.name, func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + attrnorm.SetWithDepthLimit(value.set, value.depthLimit) + } + }) + } +} diff --git a/sdk/internal/attrnorm/dedup_test.go b/sdk/internal/attrnorm/dedup_test.go new file mode 100644 index 00000000000..038c90c5ad1 --- /dev/null +++ b/sdk/internal/attrnorm/dedup_test.go @@ -0,0 +1,1178 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/attrdedup/dedup_test.go.tmpl + +package attrnorm + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "go.opentelemetry.io/otel/attribute" +) + +var cmpValue = cmp.AllowUnexported(attribute.Value{}) + +func TestValueDedup(t *testing.T) { + tests := []struct { + name string + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + 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"), + ), + wantChanged: false, + }, + { + 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"), + ), + wantChanged: true, + }, + { + 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"), + ), + wantChanged: true, + }, + { + 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"), + ), + ), + wantChanged: true, + }, + { + 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"), + ), + wantChanged: true, + }, + { + name: "unique slice", + value: attribute.SliceValue( + attribute.StringValue("one"), + attribute.IntValue(2), + ), + want: attribute.SliceValue( + attribute.StringValue("one"), + attribute.IntValue(2), + ), + wantChanged: false, + }, + { + 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"), + ), + wantChanged: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueDedup(test.value) + if changed != test.wantChanged { + t.Fatalf("ValueDedup() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueWithDepthLimit(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "below limit", + limit: 3, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + }, + { + name: "at limit", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + }, + { + name: "multi-entry map below limit", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + }, + { + name: "deduplicate multi-entry map", + limit: 2, + value: attribute.MapValue( + attribute.String("duplicate", "first"), + attribute.String("duplicate", "second"), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("duplicate", "second"), + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "deduplicate multi-entry map after prior key", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.String("middle", "first"), + attribute.String("middle", "second"), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.String("middle", "second"), + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "slice below limit", + limit: 2, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + }, + { + name: "map over limit", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.KeyValue{Key: "level2"}, + ), + ), + wantChanged: true, + }, + { + name: "slice over limit", + limit: 1, + value: attribute.SliceValue( + attribute.SliceValue(attribute.StringValue("over")), + attribute.StringValue("leaf"), + ), + want: attribute.SliceValue( + attribute.Value{}, + attribute.StringValue("leaf"), + ), + wantChanged: true, + }, + { + name: "slice over limit after scalar", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("over")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.Value{}, + attribute.StringValue("tail"), + ), + wantChanged: true, + }, + { + name: "zero allows scalar", + limit: 0, + value: attribute.StringValue("leaf"), + want: attribute.StringValue("leaf"), + wantChanged: false, + }, + { + name: "zero replaces slice", + limit: 0, + value: attribute.SliceValue(attribute.StringValue("leaf")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "zero replaces map", + limit: 0, + value: attribute.MapValue(attribute.String("leaf", "value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "negative disables depth limit", + limit: -1, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map( + "level3", + attribute.String("leaf", "value"), + ), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map( + "level3", + attribute.String("leaf", "value"), + ), + ), + ), + ), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueWithDepthLimit(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("ValueWithDepthLimit() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueWithDepthLimit() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueDepthLimitSpecCases(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "raw limit zero preserves scalar", + limit: 0, + value: attribute.StringValue("value"), + want: attribute.StringValue("value"), + wantChanged: false, + }, + { + name: "raw limit zero replaces top-level heterogeneous array", + limit: 0, + value: attribute.SliceValue(attribute.StringValue("value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "raw limit zero replaces top-level map", + limit: 0, + value: attribute.MapValue(attribute.String("key", "value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "limit one preserves top-level heterogeneous array and replaces nested collections", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.MapValue(attribute.String("nested", "value")), + ), + want: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.Value{}, + attribute.Value{}, + ), + wantChanged: true, + }, + { + name: "limit one replaces nested homogeneous array in heterogeneous array", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.StringSliceValue([]string{"nested"}), + ), + want: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.Value{}, + ), + wantChanged: true, + }, + { + name: "limit one preserves top-level map and replaces nested collections", + limit: 1, + value: attribute.MapValue( + attribute.Slice("array", attribute.StringValue("nested")), + attribute.Map("map", attribute.String("nested", "value")), + attribute.StringSlice("primitive-array", []string{"nested"}), + attribute.String("scalar", "value"), + ), + want: attribute.MapValue( + attribute.KeyValue{Key: "array"}, + attribute.KeyValue{Key: "map"}, + attribute.KeyValue{Key: "primitive-array"}, + attribute.String("scalar", "value"), + ), + wantChanged: true, + }, + { + name: "nested heterogeneous array beyond limit replaced", + limit: 2, + value: attribute.SliceValue( + attribute.SliceValue( + attribute.SliceValue(attribute.StringValue("over")), + ), + ), + want: attribute.SliceValue( + attribute.SliceValue( + attribute.Value{}, + ), + ), + wantChanged: true, + }, + { + name: "negative limit preserves nested collections", + limit: -1, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Slice( + "level2", + attribute.MapValue(attribute.String("leaf", "value")), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Slice( + "level2", + attribute.MapValue(attribute.String("leaf", "value")), + ), + ), + ), + wantChanged: false, + }, + { + name: "otherwise value is unchanged", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + wantChanged: false, + }, + } + + fns := []struct { + name string + fn func(attribute.Value, int) (attribute.Value, bool) + }{ + { + name: "ValueWithDepthLimit", + fn: ValueWithDepthLimit, + }, + { + name: "ValueLimitDepth", + fn: ValueLimitDepth, + }, + } + + for _, fn := range fns { + t.Run(fn.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := fn.fn(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("%s() changed = %v, want %v", fn.name, changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("%s() mismatch (-want +got):\n%s", fn.name, diff) + } + }) + } + }) + } +} + +func TestValueDepthLimitPrimitiveSliceTypes(t *testing.T) { + tests := []struct { + name string + value attribute.Value + want attribute.Value + }{ + { + name: "bool", + value: attribute.BoolSliceValue([]bool{true}), + want: attribute.Value{}, + }, + { + name: "int64", + value: attribute.Int64SliceValue([]int64{1}), + want: attribute.Value{}, + }, + { + name: "float64", + value: attribute.Float64SliceValue([]float64{1}), + want: attribute.Value{}, + }, + { + name: "string", + value: attribute.StringSliceValue([]string{"value"}), + want: attribute.Value{}, + }, + { + name: "bytes", + value: attribute.ByteSliceValue([]byte("value")), + want: attribute.Value{}, + }, + } + + fns := []struct { + name string + fn func(attribute.Value, int) (attribute.Value, bool) + }{ + { + name: "ValueWithDepthLimit", + fn: ValueWithDepthLimit, + }, + { + name: "ValueLimitDepth", + fn: ValueLimitDepth, + }, + } + + for _, fn := range fns { + t.Run(fn.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := fn.fn(test.value, 0) + if !changed { + t.Fatalf("%s() changed = false, want true", fn.name) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("%s() limit 0 mismatch (-want +got):\n%s", fn.name, diff) + } + + got, changed = fn.fn(test.value, 1) + if changed { + t.Fatalf("%s() changed = true, want false", fn.name) + } + if diff := cmp.Diff(test.value, got, cmpValue); diff != "" { + t.Fatalf("%s() limit 1 mismatch (-want +got):\n%s", fn.name, diff) + } + }) + } + }) + } +} + +func TestValueLimitDepth(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "scalar", + limit: 1, + value: attribute.StringValue("value"), + want: attribute.StringValue("value"), + wantChanged: false, + }, + { + name: "slice below limit", + limit: 2, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + wantChanged: false, + }, + { + name: "map below limit", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + wantChanged: false, + }, + { + name: "map over limit after prior key", + limit: 1, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map("middle", attribute.String("leaf", "value")), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.KeyValue{Key: "middle"}, + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "slice over limit", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.Value{}, + attribute.StringValue("tail"), + ), + wantChanged: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueLimitDepth(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("ValueLimitDepth() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueLimitDepth() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueLimitDepthPreservesDuplicateMapKeys(t *testing.T) { + value := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.KeyValue{Key: "over"}, + ) + + got, changed := ValueLimitDepth(value, 1) + if !changed { + t.Fatal("ValueLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("ValueLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestValueDedupNoopAllocationFree(t *testing.T) { + value := attribute.MapValue( + attribute.String("one", "1"), + attribute.String("two", "2"), + ) + var got attribute.Value + + allocs := testing.AllocsPerRun(1000, func() { + got, _ = ValueDedup(value) + }) + if allocs != 0 { + t.Fatalf("ValueDedup() allocations = %v, want 0", allocs) + } + if _, changed := ValueDedup(value); changed { + t.Fatal("ValueDedup() changed a no-op input") + } + if diff := cmp.Diff(value, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestValueDedupStorageShapes(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, changed := ValueDedup(value) + if changed { + t.Fatal("ValueDedup() changed a no-op input") + } + if diff := cmp.Diff(value, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() 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, changed := ValueDedup(value) + if changed { + t.Fatal("ValueDedup() changed a no-op input") + } + if diff := cmp.Diff(value, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestKeyValueDedup(t *testing.T) { + kv := attribute.Map( + "map", + attribute.String("nested", "first"), + attribute.String("nested", "second"), + ) + want := attribute.Map( + "map", + attribute.String("nested", "second"), + ) + + got, changed := KeyValueDedup(kv) + if !changed { + t.Fatal("KeyValueDedup() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValueWithDepthLimit(t *testing.T) { + kv := attribute.Map( + "map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ) + want := attribute.Map( + "map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ) + + got, changed := KeyValueWithDepthLimit(kv, 2) + if !changed { + t.Fatal("KeyValueWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValueLimitDepth(t *testing.T) { + kv := attribute.Map( + "map", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.Map( + "map", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.KeyValue{Key: "over"}, + ) + + got, changed := KeyValueLimitDepth(kv, 1) + if !changed { + t.Fatal("KeyValueLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueLimitDepth() 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, changed := KeyValuesDedup(kvs) + if changed { + t.Fatal("KeyValuesDedup() changed a no-op input") + } + if len(got) != len(kvs) { + t.Fatalf("KeyValuesDedup() length = %d, want %d", len(got), len(kvs)) + } + if &got[0] != &kvs[0] { + t.Fatal("KeyValuesDedup() copied a no-op input") + } +} + +func TestKeyValuesDedup(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, changed := KeyValuesDedup(kvs) + if !changed { + t.Fatal("KeyValuesDedup() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValuesWithDepthLimit(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + attribute.String("tail", "value"), + } + want := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ), + attribute.String("tail", "value"), + } + + got, changed := KeyValuesWithDepthLimit(kvs, 2) + if !changed { + t.Fatal("KeyValuesWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValuesDepthLimitNoopReturnsInput(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("one", "1"), + attribute.Map("two", attribute.String("nested", "value")), + } + tests := []struct { + name string + fn func([]attribute.KeyValue, int) ([]attribute.KeyValue, bool) + }{ + { + name: "KeyValuesWithDepthLimit", + fn: KeyValuesWithDepthLimit, + }, + { + name: "KeyValuesLimitDepth", + fn: KeyValuesLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(kvs, 2) + if changed { + t.Fatalf("%s() changed a no-op input", test.name) + } + if len(got) != len(kvs) { + t.Fatalf("%s() length = %d, want %d", test.name, len(got), len(kvs)) + } + if &got[0] != &kvs[0] { + t.Fatalf("%s() copied a no-op input", test.name) + } + }) + } +} + +func TestKeyValuesLimitDepth(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map("over", attribute.String("leaf", "value")), + ), + attribute.String("tail", "value"), + } + want := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.KeyValue{Key: "over"}, + ), + attribute.String("tail", "value"), + } + + got, changed := KeyValuesLimitDepth(kvs, 1) + if !changed { + t.Fatal("KeyValuesLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetDedup(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, changed := SetDedup(set) + if !changed { + t.Fatal("SetDedup() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetWithDepthLimit(t *testing.T) { + set := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + attribute.String("z-tail", "value"), + ) + want := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ), + attribute.String("z-tail", "value"), + ) + + got, changed := SetWithDepthLimit(set, 2) + if !changed { + t.Fatal("SetWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetLimitDepth(t *testing.T) { + set := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map("over", attribute.String("leaf", "value")), + ), + attribute.String("z-tail", "value"), + ) + want := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.KeyValue{Key: "over"}, + ), + attribute.String("z-tail", "value"), + ) + + got, changed := SetLimitDepth(set, 1) + if !changed { + t.Fatal("SetLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetDepthLimitNoop(t *testing.T) { + set := attribute.NewSet( + attribute.String("top", "value"), + attribute.Map("map", attribute.String("nested", "value")), + ) + tests := []struct { + name string + fn func(attribute.Set, int) (attribute.Set, bool) + }{ + { + name: "SetWithDepthLimit", + fn: SetWithDepthLimit, + }, + { + name: "SetLimitDepth", + fn: SetLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(set, 2) + if changed { + t.Fatalf("%s() changed a no-op input", test.name) + } + if !got.Equals(&set) { + t.Fatalf("%s() changed a no-op input", test.name) + } + }) + } +} + +func TestSetDedupNoop(t *testing.T) { + set := attribute.NewSet( + attribute.String("top", "value"), + attribute.Map("map", attribute.String("nested", "value")), + ) + + got, changed := SetDedup(set) + if changed { + t.Fatal("SetDedup() changed a no-op input") + } + if !got.Equals(&set) { + t.Fatal("SetDedup() changed a no-op input") + } +} + +func TestSetDedupEmpty(t *testing.T) { + set := attribute.Set{} + + got, changed := SetDedup(set) + if changed { + t.Fatal("SetDedup() changed an empty input") + } + if !got.Equals(&set) { + t.Fatal("SetDedup() changed an empty input") + } +} + +func TestSetDepthLimitEmpty(t *testing.T) { + set := attribute.Set{} + tests := []struct { + name string + fn func(attribute.Set, int) (attribute.Set, bool) + }{ + { + name: "SetWithDepthLimit", + fn: SetWithDepthLimit, + }, + { + name: "SetLimitDepth", + fn: SetLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(set, 1) + if changed { + t.Fatalf("%s() changed an empty input", test.name) + } + if !got.Equals(&set) { + t.Fatalf("%s() changed an empty input", test.name) + } + }) + } +} + +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) + } +} diff --git a/sdk/internal/gen.go b/sdk/internal/gen.go index 286b2b76907..ee93d50ae4b 100644 --- a/sdk/internal/gen.go +++ b/sdk/internal/gen.go @@ -6,5 +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 +//go:generate gotmpl --body=../../internal/shared/attrdedup/dedup.go.tmpl "--data={}" --out=attrnorm/dedup.go +//go:generate gotmpl --body=../../internal/shared/attrdedup/dedup_test.go.tmpl "--data={}" --out=attrnorm/dedup_test.go diff --git a/sdk/log/internal/attrdedup/dedup.go b/sdk/log/internal/attrdedup/dedup.go deleted file mode 100644 index 8ec8b5d892e..00000000000 --- a/sdk/log/internal/attrdedup/dedup.go +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// DO NOT MODIFY. Generated by gotmpl. -// source: internal/shared/attrdedup/dedup.go.tmpl - -// 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 and whether it changed. -// -// Duplicate map keys are resolved using last-value-wins semantics. -func Value(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 - } -} - -// KeyValue returns kv with all map values deduplicated and whether it changed. -func KeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) { - value, changed := Value(kv.Value) - if changed { - kv.Value = value - } - return kv, changed -} - -// KeyValues returns kvs with all map values deduplicated and whether they changed. -// -// 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, 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 := KeyValue(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 -} - -// Set returns set with all map values deduplicated and whether it changed. -// -// 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, bool) { - if set.Len() == 0 { - return set, false - } - - // 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 := range set.Len() { - kv, _ := set.Get(i) - kv, changed := KeyValue(kv) - if normalized != nil { - normalized = append(normalized, kv) - continue - } - if !changed { - continue - } - - normalized = make([]attribute.KeyValue, 0, set.Len()) - for j := range i { - prior, _ := set.Get(j) - normalized = append(normalized, prior) - } - normalized = append(normalized, kv) - } - if normalized == nil { - return set, false - } - - return attribute.NewSet(normalized...), true -} - -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 := range length { - elem := valueAt(storage, i) - elem, changed := Value(elem) - if normalized != nil { - normalized[i] = elem - continue - } - if !changed { - continue - } - - normalized = make([]attribute.Value, length) - for j := range i { - 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 := KeyValue(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 := KeyValue(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 := range i { - 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) -} diff --git a/sdk/log/internal/attrdedup/dedup_test.go b/sdk/log/internal/attrdedup/dedup_test.go deleted file mode 100644 index 82fab043ab4..00000000000 --- a/sdk/log/internal/attrdedup/dedup_test.go +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// DO NOT MODIFY. Generated by gotmpl. -// source: internal/shared/attrdedup/dedup_test.go.tmpl - -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 - wantChanged bool - }{ - { - 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"), - ), - wantChanged: false, - }, - { - 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"), - ), - wantChanged: true, - }, - { - 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"), - ), - wantChanged: true, - }, - { - 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"), - ), - ), - wantChanged: true, - }, - { - 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"), - ), - wantChanged: true, - }, - { - name: "unique slice", - value: attribute.SliceValue( - attribute.StringValue("one"), - attribute.IntValue(2), - ), - want: attribute.SliceValue( - attribute.StringValue("one"), - attribute.IntValue(2), - ), - wantChanged: false, - }, - { - 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"), - ), - wantChanged: true, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got, changed := Value(test.value) - if changed != test.wantChanged { - t.Fatalf("Value() changed = %v, want %v", changed, test.wantChanged) - } - 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 _, changed := Value(value); changed { - t.Fatal("Value() changed a no-op input") - } - 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, changed := Value(value) - if changed { - t.Fatal("Value() changed a no-op input") - } - 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, changed := Value(value) - if changed { - t.Fatal("Value() changed a no-op input") - } - 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, changed := KeyValue(kv) - if !changed { - t.Fatal("KeyValue() changed = false, want true") - } - 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, changed := KeyValues(kvs) - if changed { - t.Fatal("KeyValues() changed a no-op input") - } - 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, changed := KeyValues(kvs) - if !changed { - t.Fatal("KeyValues() changed = false, want true") - } - 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, changed := Set(set) - if !changed { - t.Fatal("Set() changed = false, want true") - } - 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, changed := Set(set) - if changed { - t.Fatal("Set() changed a no-op input") - } - if !got.Equals(&set) { - t.Fatal("Set() changed a no-op input") - } -} - -func TestSetEmpty(t *testing.T) { - set := attribute.Set{} - - got, changed := Set(set) - if changed { - t.Fatal("Set() changed an empty input") - } - 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) - } -} diff --git a/sdk/log/internal/attrnorm/dedup.go b/sdk/log/internal/attrnorm/dedup.go new file mode 100644 index 00000000000..6a4258e5a90 --- /dev/null +++ b/sdk/log/internal/attrnorm/dedup.go @@ -0,0 +1,667 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/attrdedup/dedup.go.tmpl + +// Package attrnorm normalizes attribute values. +package attrnorm // import "go.opentelemetry.io/otel/sdk/log/internal/attrnorm" + +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 +} + +// ValueDedup returns value with all map values deduplicated and whether it +// changed. +// +// Duplicate map keys are resolved using last-value-wins semantics. +func ValueDedup(value attribute.Value) (attribute.Value, bool) { + switch value.Type() { + case attribute.SLICE: + return sliceValueDedup(value) + case attribute.MAP: + return mapValueDedup(value) + default: + return value, false + } +} + +// ValueWithDepthLimit returns value with all map values deduplicated and all +// slice and map values limited to depth levels. +// +// Duplicate map keys are resolved using last-value-wins semantics. When a +// slice or map value would exceed a non-negative depth limit, that value is +// replaced by an empty value. A negative depth limit disables depth limiting. +func ValueWithDepthLimit(value attribute.Value, depthLimit int) (attribute.Value, bool) { + return valueDedupWithDepthLimit(value, depthLimit, 1) +} + +// ValueLimitDepth returns value with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +// +// When a slice or map value would exceed a non-negative depth limit, that +// value is replaced by an empty value. A negative depth limit disables depth +// limiting. +func ValueLimitDepth(value attribute.Value, depthLimit int) (attribute.Value, bool) { + return valueLimitDepth(value, depthLimit, 1) +} + +func valueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + switch value.Type() { + case attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE, attribute.STRINGSLICE, attribute.BYTESLICE: + return primitiveSliceValueLimitDepth(value, depthLimit, depth) + case attribute.SLICE: + return sliceValueDedupWithDepthLimit(value, depthLimit, depth) + case attribute.MAP: + return mapValueDedupWithDepthLimit(value, depthLimit, depth) + default: + return value, false + } +} + +func valueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + switch value.Type() { + case attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE, attribute.STRINGSLICE, attribute.BYTESLICE: + return primitiveSliceValueLimitDepth(value, depthLimit, depth) + case attribute.SLICE: + return sliceValueLimitDepth(value, depthLimit, depth) + case attribute.MAP: + return mapValueLimitDepth(value, depthLimit, depth) + default: + return value, false + } +} + +// KeyValueDedup returns kv with all map values deduplicated and whether it +// changed. +func KeyValueDedup(kv attribute.KeyValue) (attribute.KeyValue, bool) { + value, changed := ValueDedup(kv.Value) + if changed { + kv.Value = value + } + return kv, changed +} + +// KeyValueWithDepthLimit returns kv with all map values deduplicated and all +// slice and map values limited to depth levels. +func KeyValueWithDepthLimit(kv attribute.KeyValue, depthLimit int) (attribute.KeyValue, bool) { + return keyValueDedupWithDepthLimit(kv, depthLimit, 1) +} + +// KeyValueLimitDepth returns kv with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +func KeyValueLimitDepth(kv attribute.KeyValue, depthLimit int) (attribute.KeyValue, bool) { + return keyValueLimitDepth(kv, depthLimit, 1) +} + +func keyValueDedupWithDepthLimit(kv attribute.KeyValue, depthLimit, depth int) (attribute.KeyValue, bool) { + value, changed := valueDedupWithDepthLimit(kv.Value, depthLimit, depth) + if changed { + kv.Value = value + } + return kv, changed +} + +func keyValueLimitDepth(kv attribute.KeyValue, depthLimit, depth int) (attribute.KeyValue, bool) { + value, changed := valueLimitDepth(kv.Value, depthLimit, depth) + if changed { + kv.Value = value + } + return kv, changed +} + +// KeyValuesDedup returns kvs with all map values deduplicated and whether they +// changed. +// +// The returned slice is the original kvs slice if no value needs +// deduplication. Top-level keys in kvs are not deduplicated. +func KeyValuesDedup(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 := KeyValueDedup(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 +} + +// KeyValuesWithDepthLimit returns kvs with all map values deduplicated and all +// slice and map values limited to depth levels. +// +// The returned slice is the original kvs slice if no value needs +// normalization. Top-level keys in kvs are not deduplicated. +func KeyValuesWithDepthLimit(kvs []attribute.KeyValue, depthLimit int) ([]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 := keyValueDedupWithDepthLimit(kv, depthLimit, 1) + 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 +} + +// KeyValuesLimitDepth returns kvs with all slice and map values limited to +// depth levels. Map keys are not deduplicated. +func KeyValuesLimitDepth(kvs []attribute.KeyValue, depthLimit int) ([]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 := keyValueLimitDepth(kv, depthLimit, 1) + 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 +} + +// SetDedup returns set with all map values deduplicated and whether it changed. +// +// 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 SetDedup(set attribute.Set) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := KeyValueDedup(kv) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +// SetWithDepthLimit returns set with all map values deduplicated and all slice +// and map values limited to depth levels. +// +// The returned Set is the original set if no value needs normalization. +// Top-level key uniqueness remains attribute.Set's responsibility; this only +// normalizes attribute values. +func SetWithDepthLimit(set attribute.Set, depthLimit int) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := keyValueDedupWithDepthLimit(kv, depthLimit, 1) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +// SetLimitDepth returns set with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +func SetLimitDepth(set attribute.Set, depthLimit int) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := keyValueLimitDepth(kv, depthLimit, 1) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +func sliceValueDedup(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 := range length { + elem := valueAt(storage, i) + elem, changed := ValueDedup(elem) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func primitiveSliceValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if !exceedsDepthLimit(depthLimit, depth) { + return value, false + } + return attribute.Value{}, true +} + +func sliceValueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + 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 := range length { + elem := valueAt(storage, i) + elem, changed := valueDedupWithDepthLimit(elem, depthLimit, depth+1) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func sliceValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + storage := valueStorage(value) + length := valueLen(storage) + + var normalized []attribute.Value + for i := range length { + elem := valueAt(storage, i) + elem, changed := valueLimitDepth(elem, depthLimit, depth+1) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func mapValueDedup(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 := KeyValueDedup(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 := KeyValueDedup(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 := range i { + normalized = append(normalized, keyValueAt(storage, k)) + } + normalized = append(normalized, kv) + } + i = j + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func mapValueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + 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 := keyValueDedupWithDepthLimit(keyValueAt(storage, 0), depthLimit, depth+1) + 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 := keyValueDedupWithDepthLimit(keyValueAt(storage, j-1), depthLimit, depth+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 := range i { + normalized = append(normalized, keyValueAt(storage, k)) + } + normalized = append(normalized, kv) + } + i = j + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func mapValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + storage := valueStorage(value) + length := keyValueLen(storage) + + var normalized []attribute.KeyValue + for i := range length { + kv := keyValueAt(storage, i) + kv, changed := keyValueLimitDepth(kv, depthLimit, depth+1) + if normalized != nil { + normalized[i] = kv + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, length) + for j := range i { + normalized[j] = keyValueAt(storage, j) + } + normalized[i] = kv + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func exceedsDepthLimit(depthLimit, depth int) bool { + return depthLimit >= 0 && depth > depthLimit +} + +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) +} diff --git a/sdk/log/internal/attrnorm/dedup_test.go b/sdk/log/internal/attrnorm/dedup_test.go new file mode 100644 index 00000000000..038c90c5ad1 --- /dev/null +++ b/sdk/log/internal/attrnorm/dedup_test.go @@ -0,0 +1,1178 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/attrdedup/dedup_test.go.tmpl + +package attrnorm + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "go.opentelemetry.io/otel/attribute" +) + +var cmpValue = cmp.AllowUnexported(attribute.Value{}) + +func TestValueDedup(t *testing.T) { + tests := []struct { + name string + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + 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"), + ), + wantChanged: false, + }, + { + 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"), + ), + wantChanged: true, + }, + { + 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"), + ), + wantChanged: true, + }, + { + 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"), + ), + ), + wantChanged: true, + }, + { + 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"), + ), + wantChanged: true, + }, + { + name: "unique slice", + value: attribute.SliceValue( + attribute.StringValue("one"), + attribute.IntValue(2), + ), + want: attribute.SliceValue( + attribute.StringValue("one"), + attribute.IntValue(2), + ), + wantChanged: false, + }, + { + 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"), + ), + wantChanged: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueDedup(test.value) + if changed != test.wantChanged { + t.Fatalf("ValueDedup() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueWithDepthLimit(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "below limit", + limit: 3, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + }, + { + name: "at limit", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + }, + { + name: "multi-entry map below limit", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + }, + { + name: "deduplicate multi-entry map", + limit: 2, + value: attribute.MapValue( + attribute.String("duplicate", "first"), + attribute.String("duplicate", "second"), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("duplicate", "second"), + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "deduplicate multi-entry map after prior key", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.String("middle", "first"), + attribute.String("middle", "second"), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.String("middle", "second"), + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "slice below limit", + limit: 2, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + }, + { + name: "map over limit", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.KeyValue{Key: "level2"}, + ), + ), + wantChanged: true, + }, + { + name: "slice over limit", + limit: 1, + value: attribute.SliceValue( + attribute.SliceValue(attribute.StringValue("over")), + attribute.StringValue("leaf"), + ), + want: attribute.SliceValue( + attribute.Value{}, + attribute.StringValue("leaf"), + ), + wantChanged: true, + }, + { + name: "slice over limit after scalar", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("over")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.Value{}, + attribute.StringValue("tail"), + ), + wantChanged: true, + }, + { + name: "zero allows scalar", + limit: 0, + value: attribute.StringValue("leaf"), + want: attribute.StringValue("leaf"), + wantChanged: false, + }, + { + name: "zero replaces slice", + limit: 0, + value: attribute.SliceValue(attribute.StringValue("leaf")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "zero replaces map", + limit: 0, + value: attribute.MapValue(attribute.String("leaf", "value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "negative disables depth limit", + limit: -1, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map( + "level3", + attribute.String("leaf", "value"), + ), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map( + "level3", + attribute.String("leaf", "value"), + ), + ), + ), + ), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueWithDepthLimit(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("ValueWithDepthLimit() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueWithDepthLimit() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueDepthLimitSpecCases(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "raw limit zero preserves scalar", + limit: 0, + value: attribute.StringValue("value"), + want: attribute.StringValue("value"), + wantChanged: false, + }, + { + name: "raw limit zero replaces top-level heterogeneous array", + limit: 0, + value: attribute.SliceValue(attribute.StringValue("value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "raw limit zero replaces top-level map", + limit: 0, + value: attribute.MapValue(attribute.String("key", "value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "limit one preserves top-level heterogeneous array and replaces nested collections", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.MapValue(attribute.String("nested", "value")), + ), + want: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.Value{}, + attribute.Value{}, + ), + wantChanged: true, + }, + { + name: "limit one replaces nested homogeneous array in heterogeneous array", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.StringSliceValue([]string{"nested"}), + ), + want: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.Value{}, + ), + wantChanged: true, + }, + { + name: "limit one preserves top-level map and replaces nested collections", + limit: 1, + value: attribute.MapValue( + attribute.Slice("array", attribute.StringValue("nested")), + attribute.Map("map", attribute.String("nested", "value")), + attribute.StringSlice("primitive-array", []string{"nested"}), + attribute.String("scalar", "value"), + ), + want: attribute.MapValue( + attribute.KeyValue{Key: "array"}, + attribute.KeyValue{Key: "map"}, + attribute.KeyValue{Key: "primitive-array"}, + attribute.String("scalar", "value"), + ), + wantChanged: true, + }, + { + name: "nested heterogeneous array beyond limit replaced", + limit: 2, + value: attribute.SliceValue( + attribute.SliceValue( + attribute.SliceValue(attribute.StringValue("over")), + ), + ), + want: attribute.SliceValue( + attribute.SliceValue( + attribute.Value{}, + ), + ), + wantChanged: true, + }, + { + name: "negative limit preserves nested collections", + limit: -1, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Slice( + "level2", + attribute.MapValue(attribute.String("leaf", "value")), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Slice( + "level2", + attribute.MapValue(attribute.String("leaf", "value")), + ), + ), + ), + wantChanged: false, + }, + { + name: "otherwise value is unchanged", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + wantChanged: false, + }, + } + + fns := []struct { + name string + fn func(attribute.Value, int) (attribute.Value, bool) + }{ + { + name: "ValueWithDepthLimit", + fn: ValueWithDepthLimit, + }, + { + name: "ValueLimitDepth", + fn: ValueLimitDepth, + }, + } + + for _, fn := range fns { + t.Run(fn.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := fn.fn(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("%s() changed = %v, want %v", fn.name, changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("%s() mismatch (-want +got):\n%s", fn.name, diff) + } + }) + } + }) + } +} + +func TestValueDepthLimitPrimitiveSliceTypes(t *testing.T) { + tests := []struct { + name string + value attribute.Value + want attribute.Value + }{ + { + name: "bool", + value: attribute.BoolSliceValue([]bool{true}), + want: attribute.Value{}, + }, + { + name: "int64", + value: attribute.Int64SliceValue([]int64{1}), + want: attribute.Value{}, + }, + { + name: "float64", + value: attribute.Float64SliceValue([]float64{1}), + want: attribute.Value{}, + }, + { + name: "string", + value: attribute.StringSliceValue([]string{"value"}), + want: attribute.Value{}, + }, + { + name: "bytes", + value: attribute.ByteSliceValue([]byte("value")), + want: attribute.Value{}, + }, + } + + fns := []struct { + name string + fn func(attribute.Value, int) (attribute.Value, bool) + }{ + { + name: "ValueWithDepthLimit", + fn: ValueWithDepthLimit, + }, + { + name: "ValueLimitDepth", + fn: ValueLimitDepth, + }, + } + + for _, fn := range fns { + t.Run(fn.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := fn.fn(test.value, 0) + if !changed { + t.Fatalf("%s() changed = false, want true", fn.name) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("%s() limit 0 mismatch (-want +got):\n%s", fn.name, diff) + } + + got, changed = fn.fn(test.value, 1) + if changed { + t.Fatalf("%s() changed = true, want false", fn.name) + } + if diff := cmp.Diff(test.value, got, cmpValue); diff != "" { + t.Fatalf("%s() limit 1 mismatch (-want +got):\n%s", fn.name, diff) + } + }) + } + }) + } +} + +func TestValueLimitDepth(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "scalar", + limit: 1, + value: attribute.StringValue("value"), + want: attribute.StringValue("value"), + wantChanged: false, + }, + { + name: "slice below limit", + limit: 2, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + wantChanged: false, + }, + { + name: "map below limit", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + wantChanged: false, + }, + { + name: "map over limit after prior key", + limit: 1, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map("middle", attribute.String("leaf", "value")), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.KeyValue{Key: "middle"}, + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "slice over limit", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.Value{}, + attribute.StringValue("tail"), + ), + wantChanged: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueLimitDepth(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("ValueLimitDepth() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueLimitDepth() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueLimitDepthPreservesDuplicateMapKeys(t *testing.T) { + value := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.KeyValue{Key: "over"}, + ) + + got, changed := ValueLimitDepth(value, 1) + if !changed { + t.Fatal("ValueLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("ValueLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestValueDedupNoopAllocationFree(t *testing.T) { + value := attribute.MapValue( + attribute.String("one", "1"), + attribute.String("two", "2"), + ) + var got attribute.Value + + allocs := testing.AllocsPerRun(1000, func() { + got, _ = ValueDedup(value) + }) + if allocs != 0 { + t.Fatalf("ValueDedup() allocations = %v, want 0", allocs) + } + if _, changed := ValueDedup(value); changed { + t.Fatal("ValueDedup() changed a no-op input") + } + if diff := cmp.Diff(value, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestValueDedupStorageShapes(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, changed := ValueDedup(value) + if changed { + t.Fatal("ValueDedup() changed a no-op input") + } + if diff := cmp.Diff(value, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() 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, changed := ValueDedup(value) + if changed { + t.Fatal("ValueDedup() changed a no-op input") + } + if diff := cmp.Diff(value, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestKeyValueDedup(t *testing.T) { + kv := attribute.Map( + "map", + attribute.String("nested", "first"), + attribute.String("nested", "second"), + ) + want := attribute.Map( + "map", + attribute.String("nested", "second"), + ) + + got, changed := KeyValueDedup(kv) + if !changed { + t.Fatal("KeyValueDedup() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValueWithDepthLimit(t *testing.T) { + kv := attribute.Map( + "map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ) + want := attribute.Map( + "map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ) + + got, changed := KeyValueWithDepthLimit(kv, 2) + if !changed { + t.Fatal("KeyValueWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValueLimitDepth(t *testing.T) { + kv := attribute.Map( + "map", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.Map( + "map", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.KeyValue{Key: "over"}, + ) + + got, changed := KeyValueLimitDepth(kv, 1) + if !changed { + t.Fatal("KeyValueLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueLimitDepth() 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, changed := KeyValuesDedup(kvs) + if changed { + t.Fatal("KeyValuesDedup() changed a no-op input") + } + if len(got) != len(kvs) { + t.Fatalf("KeyValuesDedup() length = %d, want %d", len(got), len(kvs)) + } + if &got[0] != &kvs[0] { + t.Fatal("KeyValuesDedup() copied a no-op input") + } +} + +func TestKeyValuesDedup(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, changed := KeyValuesDedup(kvs) + if !changed { + t.Fatal("KeyValuesDedup() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValuesWithDepthLimit(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + attribute.String("tail", "value"), + } + want := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ), + attribute.String("tail", "value"), + } + + got, changed := KeyValuesWithDepthLimit(kvs, 2) + if !changed { + t.Fatal("KeyValuesWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValuesDepthLimitNoopReturnsInput(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("one", "1"), + attribute.Map("two", attribute.String("nested", "value")), + } + tests := []struct { + name string + fn func([]attribute.KeyValue, int) ([]attribute.KeyValue, bool) + }{ + { + name: "KeyValuesWithDepthLimit", + fn: KeyValuesWithDepthLimit, + }, + { + name: "KeyValuesLimitDepth", + fn: KeyValuesLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(kvs, 2) + if changed { + t.Fatalf("%s() changed a no-op input", test.name) + } + if len(got) != len(kvs) { + t.Fatalf("%s() length = %d, want %d", test.name, len(got), len(kvs)) + } + if &got[0] != &kvs[0] { + t.Fatalf("%s() copied a no-op input", test.name) + } + }) + } +} + +func TestKeyValuesLimitDepth(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map("over", attribute.String("leaf", "value")), + ), + attribute.String("tail", "value"), + } + want := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.KeyValue{Key: "over"}, + ), + attribute.String("tail", "value"), + } + + got, changed := KeyValuesLimitDepth(kvs, 1) + if !changed { + t.Fatal("KeyValuesLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetDedup(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, changed := SetDedup(set) + if !changed { + t.Fatal("SetDedup() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetWithDepthLimit(t *testing.T) { + set := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + attribute.String("z-tail", "value"), + ) + want := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ), + attribute.String("z-tail", "value"), + ) + + got, changed := SetWithDepthLimit(set, 2) + if !changed { + t.Fatal("SetWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetLimitDepth(t *testing.T) { + set := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map("over", attribute.String("leaf", "value")), + ), + attribute.String("z-tail", "value"), + ) + want := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.KeyValue{Key: "over"}, + ), + attribute.String("z-tail", "value"), + ) + + got, changed := SetLimitDepth(set, 1) + if !changed { + t.Fatal("SetLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetDepthLimitNoop(t *testing.T) { + set := attribute.NewSet( + attribute.String("top", "value"), + attribute.Map("map", attribute.String("nested", "value")), + ) + tests := []struct { + name string + fn func(attribute.Set, int) (attribute.Set, bool) + }{ + { + name: "SetWithDepthLimit", + fn: SetWithDepthLimit, + }, + { + name: "SetLimitDepth", + fn: SetLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(set, 2) + if changed { + t.Fatalf("%s() changed a no-op input", test.name) + } + if !got.Equals(&set) { + t.Fatalf("%s() changed a no-op input", test.name) + } + }) + } +} + +func TestSetDedupNoop(t *testing.T) { + set := attribute.NewSet( + attribute.String("top", "value"), + attribute.Map("map", attribute.String("nested", "value")), + ) + + got, changed := SetDedup(set) + if changed { + t.Fatal("SetDedup() changed a no-op input") + } + if !got.Equals(&set) { + t.Fatal("SetDedup() changed a no-op input") + } +} + +func TestSetDedupEmpty(t *testing.T) { + set := attribute.Set{} + + got, changed := SetDedup(set) + if changed { + t.Fatal("SetDedup() changed an empty input") + } + if !got.Equals(&set) { + t.Fatal("SetDedup() changed an empty input") + } +} + +func TestSetDepthLimitEmpty(t *testing.T) { + set := attribute.Set{} + tests := []struct { + name string + fn func(attribute.Set, int) (attribute.Set, bool) + }{ + { + name: "SetWithDepthLimit", + fn: SetWithDepthLimit, + }, + { + name: "SetLimitDepth", + fn: SetLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(set, 1) + if changed { + t.Fatalf("%s() changed an empty input", test.name) + } + if !got.Equals(&set) { + t.Fatalf("%s() changed an empty input", test.name) + } + }) + } +} + +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) + } +} diff --git a/sdk/log/internal/gen.go b/sdk/log/internal/gen.go index 6cd5f3868a3..241d180770b 100644 --- a/sdk/log/internal/gen.go +++ b/sdk/log/internal/gen.go @@ -6,5 +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 +//go:generate gotmpl --body=../../../internal/shared/attrdedup/dedup.go.tmpl "--data={}" --out=attrnorm/dedup.go +//go:generate gotmpl --body=../../../internal/shared/attrdedup/dedup_test.go.tmpl "--data={}" --out=attrnorm/dedup_test.go diff --git a/sdk/log/logger.go b/sdk/log/logger.go index 50c3d97a1aa..38ae33643da 100644 --- a/sdk/log/logger.go +++ b/sdk/log/logger.go @@ -104,6 +104,7 @@ func (l *logger) newRecord(ctx context.Context, r log.Record) Record { resource: l.provider.resource, scope: &l.instrumentationScope, attributeValueLengthLimit: l.provider.attributeValueLengthLimit, + attributeValueDepthLimit: l.provider.attrValueDepthLimit(), attributeCountLimit: l.provider.attributeCountLimit, allowDupKeys: l.provider.allowDupKeys, } diff --git a/sdk/log/logger_test.go b/sdk/log/logger_test.go index 77feb9fa2cb..ab93911cf77 100644 --- a/sdk/log/logger_test.go +++ b/sdk/log/logger_test.go @@ -116,6 +116,7 @@ func TestLoggerEmit(t *testing.T) { observedTimestamp: r.ObservedTimestamp(), resource: resource.NewSchemaless(attribute.String("key", "value")), attributeValueLengthLimit: 3, + attributeValueDepthLimit: defaultAttrValDepthLim, attributeCountLimit: 2, scope: &instrumentation.Scope{Name: "scope"}, front: [attributesInlineCount]attribute.KeyValue{ @@ -157,6 +158,7 @@ func TestLoggerEmit(t *testing.T) { observedTimestamp: r.ObservedTimestamp(), resource: resource.NewSchemaless(attribute.String("key", "value")), attributeValueLengthLimit: 3, + attributeValueDepthLimit: defaultAttrValDepthLim, attributeCountLimit: 2, scope: &instrumentation.Scope{Name: "scope"}, front: [attributesInlineCount]attribute.KeyValue{ @@ -191,6 +193,7 @@ func TestLoggerEmit(t *testing.T) { observedTimestamp: r.ObservedTimestamp(), resource: resource.NewSchemaless(attribute.String("key", "value")), attributeValueLengthLimit: 3, + attributeValueDepthLimit: defaultAttrValDepthLim, attributeCountLimit: 2, scope: &instrumentation.Scope{Name: "scope"}, front: [attributesInlineCount]attribute.KeyValue{ @@ -222,6 +225,7 @@ func TestLoggerEmit(t *testing.T) { observedTimestamp: nowDate, resource: resource.NewSchemaless(attribute.String("key", "value")), attributeValueLengthLimit: 3, + attributeValueDepthLimit: defaultAttrValDepthLim, attributeCountLimit: 2, scope: &instrumentation.Scope{Name: "scope"}, front: [attributesInlineCount]attribute.KeyValue{ @@ -254,6 +258,7 @@ func TestLoggerEmit(t *testing.T) { observedTimestamp: rWithAllowKeyDuplication.ObservedTimestamp(), resource: resource.NewSchemaless(attribute.String("key", "value")), attributeValueLengthLimit: 5, + attributeValueDepthLimit: defaultAttrValDepthLim, attributeCountLimit: 5, scope: &instrumentation.Scope{Name: "scope"}, front: [attributesInlineCount]attribute.KeyValue{ @@ -290,6 +295,7 @@ func TestLoggerEmit(t *testing.T) { observedTimestamp: rWithDuplicatesInBody.ObservedTimestamp(), resource: resource.NewSchemaless(attribute.String("key", "value")), attributeValueLengthLimit: 5, + attributeValueDepthLimit: defaultAttrValDepthLim, attributeCountLimit: 5, scope: &instrumentation.Scope{Name: "scope"}, front: [attributesInlineCount]attribute.KeyValue{ diff --git a/sdk/log/logtest/factory.go b/sdk/log/logtest/factory.go index d93d3a66cae..0099c57b65b 100644 --- a/sdk/log/logtest/factory.go +++ b/sdk/log/logtest/factory.go @@ -50,6 +50,7 @@ func (f RecordFactory) NewRecord() sdklog.Record { // Set to unlimited so attributes are set exactly. set(r, "attributeCountLimit", -1) set(r, "attributeValueLengthLimit", -1) + set(r, "attributeValueDepthLimit", -1) r.SetEventName(f.EventName) r.SetTimestamp(f.Timestamp) diff --git a/sdk/log/logtest/factory_test.go b/sdk/log/logtest/factory_test.go index de9043e5340..d66824f08c9 100644 --- a/sdk/log/logtest/factory_test.go +++ b/sdk/log/logtest/factory_test.go @@ -18,7 +18,22 @@ import ( ) func TestRecordFactoryEmpty(t *testing.T) { - assert.Equal(t, sdklog.Record{}, RecordFactory{}.NewRecord()) + var zero sdklog.Record + got := RecordFactory{}.NewRecord() + + assert.Equal(t, zero.EventName(), got.EventName()) + assert.Equal(t, zero.Timestamp(), got.Timestamp()) + assert.Equal(t, zero.ObservedTimestamp(), got.ObservedTimestamp()) + assert.Equal(t, zero.Severity(), got.Severity()) + assert.Equal(t, zero.SeverityText(), got.SeverityText()) + assertBody(t, zero.Body(), got) + assertAttributes(t, nil, got) + assert.Equal(t, zero.DroppedAttributes(), got.DroppedAttributes()) + assert.Equal(t, zero.TraceID(), got.TraceID()) + assert.Equal(t, zero.SpanID(), got.SpanID()) + assert.Equal(t, zero.TraceFlags(), got.TraceFlags()) + assert.Equal(t, zero.InstrumentationScope(), got.InstrumentationScope()) + assert.Equal(t, zero.Resource(), got.Resource()) } func TestRecordFactory(t *testing.T) { @@ -109,6 +124,23 @@ func TestRecordFactoryMultiple(t *testing.T) { assert.Equal(t, scope, record1.InstrumentationScope()) } +func TestRecordFactoryDisablesAttributeValueDepthLimit(t *testing.T) { + attr := attribute.KeyValue{Key: "attr", Value: nestedValue(70)} + + r := RecordFactory{}.NewRecord() + r.AddAttributes(attr) + + assertAttributes(t, []attribute.KeyValue{attr}, r) +} + +func nestedValue(depth int) attribute.Value { + v := attribute.IntValue(1) + for range depth { + v = attribute.MapValue(attribute.KeyValue{Key: "level", Value: v}) + } + return v +} + func assertBody(t *testing.T, want attribute.Value, r sdklog.Record) { t.Helper() got := r.Body() diff --git a/sdk/log/provider.go b/sdk/log/provider.go index 5cc6ce54de8..2655196209e 100644 --- a/sdk/log/provider.go +++ b/sdk/log/provider.go @@ -15,24 +15,26 @@ 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/log/internal/attrnorm" "go.opentelemetry.io/otel/sdk/resource" ) const ( - defaultAttrCntLim = 128 - defaultAttrValLenLim = -1 + defaultAttrCntLim = 128 + defaultAttrValLenLim = -1 + defaultAttrValDepthLim = 64 envarAttrCntLim = "OTEL_LOGRECORD_ATTRIBUTE_COUNT_LIMIT" envarAttrValLenLim = "OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT" ) type providerConfig struct { - resource *resource.Resource - processors []Processor - attrCntLim setting[int] - attrValLenLim setting[int] - allowDupKeys setting[bool] + resource *resource.Resource + processors []Processor + attrCntLim setting[int] + attrValLenLim setting[int] + attrValDepthLim setting[int] + allowDupKeys setting[bool] } type experimentalOption interface { @@ -62,6 +64,10 @@ func newProviderConfig(opts []LoggerProviderOption) providerConfig { fallback[int](defaultAttrValLenLim), ) + c.attrValDepthLim = c.attrValDepthLim.Resolve( + fallback[int](defaultAttrValDepthLim), + ) + return c } @@ -74,6 +80,7 @@ type LoggerProvider struct { processors []Processor attributeCountLimit int attributeValueLengthLimit int + attributeValueDepthLimit int allowDupKeys bool loggersMu sync.Mutex @@ -100,6 +107,7 @@ func NewLoggerProvider(opts ...LoggerProviderOption) *LoggerProvider { processors: cfg.processors, attributeCountLimit: cfg.attrCntLim.Value, attributeValueLengthLimit: cfg.attrValLenLim.Value, + attributeValueDepthLimit: cfg.attrValDepthLim.Value, allowDupKeys: cfg.allowDupKeys.Value, } } @@ -120,8 +128,11 @@ func (p *LoggerProvider) Logger(name string, opts ...log.LoggerOption) log.Logge cfg := log.NewLoggerConfig(opts...) attrs := cfg.InstrumentationAttributes() + depthLimit := p.attrValueDepthLimit() if !p.allowDupKeys { - attrs, _ = attrdedup.Set(attrs) + attrs, _ = attrnorm.SetWithDepthLimit(attrs, depthLimit) + } else { + attrs, _ = attrnorm.SetLimitDepth(attrs, depthLimit) } scope := instrumentation.Scope{ Name: name, @@ -148,6 +159,13 @@ func (p *LoggerProvider) Logger(name string, opts ...log.LoggerOption) log.Logge return l } +func (p *LoggerProvider) attrValueDepthLimit() int { + if p.attributeValueDepthLimit != 0 { + return p.attributeValueDepthLimit + } + return defaultAttrValDepthLim +} + // Shutdown shuts down the provider and all processors. // // This method can be called concurrently. @@ -262,6 +280,28 @@ func WithAttributeValueLengthLimit(limit int) LoggerProviderOption { }) } +// WithAttributeValueDepthLimit sets the maximum allowed depth for nested +// attribute values. Any slice or map value beyond this depth will be +// replaced with an empty value. +// +// This limit applies to log record and instrumentation scope attributes +// processed by this LoggerProvider. +// +// Setting this to zero means the default limit is used. +// +// Setting this to a negative value means no limit is applied. +// +// By default, 64 will be used. +func WithAttributeValueDepthLimit(limit int) LoggerProviderOption { + return loggerProviderOptionFunc(func(cfg providerConfig) providerConfig { + if limit == 0 { + limit = defaultAttrValDepthLim + } + cfg.attrValDepthLim = newSetting(limit) + return cfg + }) +} + // WithAllowKeyDuplication sets whether deduplication is skipped for log record // and instrumentation scope key-value collections. // diff --git a/sdk/log/provider_test.go b/sdk/log/provider_test.go index c2e62d538bf..2041b27785a 100644 --- a/sdk/log/provider_test.go +++ b/sdk/log/provider_test.go @@ -94,6 +94,7 @@ func TestNewLoggerProviderConfiguration(t *testing.T) { p0, p1 := newProcessor("0"), newProcessor("1") attrCntLim := 12 attrValLenLim := 21 + attrValDepthLim := 5 testcases := []struct { name string @@ -107,6 +108,7 @@ func TestNewLoggerProviderConfiguration(t *testing.T) { resource: resource.Default(), attributeCountLimit: defaultAttrCntLim, attributeValueLengthLimit: defaultAttrValLenLim, + attributeValueDepthLimit: defaultAttrValDepthLim, }, }, { @@ -117,6 +119,7 @@ func TestNewLoggerProviderConfiguration(t *testing.T) { WithProcessor(p1), WithAttributeCountLimit(attrCntLim), WithAttributeValueLengthLimit(attrValLenLim), + WithAttributeValueDepthLimit(attrValDepthLim), WithAllowKeyDuplication(), }, want: &LoggerProvider{ @@ -124,9 +127,22 @@ func TestNewLoggerProviderConfiguration(t *testing.T) { processors: []Processor{p0, p1}, attributeCountLimit: attrCntLim, attributeValueLengthLimit: attrValLenLim, + attributeValueDepthLimit: attrValDepthLim, allowDupKeys: true, }, }, + { + name: "ZeroAttributeValueDepthLimitOption", + options: []LoggerProviderOption{ + WithAttributeValueDepthLimit(0), + }, + want: &LoggerProvider{ + resource: resource.Default(), + attributeCountLimit: defaultAttrCntLim, + attributeValueLengthLimit: defaultAttrValLenLim, + attributeValueDepthLimit: defaultAttrValDepthLim, + }, + }, { name: "Environment", envars: map[string]string{ @@ -137,6 +153,7 @@ func TestNewLoggerProviderConfiguration(t *testing.T) { resource: resource.Default(), attributeCountLimit: attrCntLim, attributeValueLengthLimit: attrValLenLim, + attributeValueDepthLimit: defaultAttrValDepthLim, }, }, { @@ -149,6 +166,7 @@ func TestNewLoggerProviderConfiguration(t *testing.T) { resource: resource.Default(), attributeCountLimit: defaultAttrCntLim, attributeValueLengthLimit: defaultAttrValLenLim, + attributeValueDepthLimit: defaultAttrValDepthLim, }, }, { @@ -166,6 +184,7 @@ func TestNewLoggerProviderConfiguration(t *testing.T) { resource: resource.Default(), attributeCountLimit: attrCntLim, attributeValueLengthLimit: attrValLenLim, + attributeValueDepthLimit: defaultAttrValDepthLim, }, }, } @@ -296,6 +315,68 @@ func TestMapDeduplication(t *testing.T) { }) } +func recordAttributes(r Record) []attribute.KeyValue { + var attrs []attribute.KeyValue + r.WalkAttributes(func(kv attribute.KeyValue) bool { + attrs = append(attrs, kv) + return true + }) + return attrs +} + +func TestLoggerProviderAttributeValueDepthLimit(t *testing.T) { + p := newProcessor("processor") + lp := NewLoggerProvider( + WithProcessor(p), + WithAttributeValueDepthLimit(2), + WithResource(resource.NewSchemaless(logDepthLimitInputAttr("resource"))), + ) + l := lp.Logger("scope", log.WithInstrumentationAttributes(logDepthLimitInputAttr("scope"))) + + var in log.Record + in.SetBody(logDepthLimitInputAttr("body").Value) + in.AddAttributes(logDepthLimitInputAttr("attr")) + l.Emit(t.Context(), in) + + require.Len(t, p.records, 1) + got := p.records[0] + assert.Equal(t, []attribute.KeyValue{logDepthLimitInputAttr("resource")}, got.Resource().Attributes()) + assert.Equal(t, attribute.NewSet(logDepthLimitWantAttr("scope")), got.InstrumentationScope().Attributes) + assert.Equal(t, []attribute.KeyValue{logDepthLimitWantAttr("attr")}, recordAttributes(got)) + assert.True(t, valueEqual(logDepthLimitInputAttr("body").Value, got.Body())) +} + +func TestLoggerProviderAttributeValueDepthLimitZeroDefault(t *testing.T) { + p := newProcessor("processor") + lp := NewLoggerProvider( + WithProcessor(p), + WithAttributeValueDepthLimit(0), + WithResource(resource.Empty()), + ) + l := lp.Logger("scope", log.WithInstrumentationAttributes(logDepthLimitInputAttr("scope"))) + + var in log.Record + in.SetBody(logDepthLimitInputAttr("body").Value) + in.AddAttributes(logDepthLimitInputAttr("attr"), attribute.String("scalar", "ok")) + l.Emit(t.Context(), in) + + require.Len(t, p.records, 1) + got := p.records[0] + assert.Equal(t, attribute.NewSet(logDepthLimitInputAttr("scope")), got.InstrumentationScope().Attributes) + assert.ElementsMatch(t, []attribute.KeyValue{ + logDepthLimitInputAttr("attr"), + attribute.String("scalar", "ok"), + }, recordAttributes(got)) + assert.True(t, valueEqual(logDepthLimitInputAttr("body").Value, got.Body())) +} + +func TestLoggerProviderAttributeValueDepthLimitOptionPrecedence(t *testing.T) { + assert.Equal(t, 3, newProviderConfig([]LoggerProviderOption{ + WithAttributeValueDepthLimit(7), + WithAttributeValueDepthLimit(3), + }).attrValDepthLim.Value) +} + func TestLoggerProviderConcurrentSafe(t *testing.T) { const goRoutineN = 10 diff --git a/sdk/log/record.go b/sdk/log/record.go index 2e30fb17771..b7fb2ffb44a 100644 --- a/sdk/log/record.go +++ b/sdk/log/record.go @@ -14,7 +14,7 @@ import ( "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/sdk/instrumentation" - "go.opentelemetry.io/otel/sdk/log/internal/attrdedup" + "go.opentelemetry.io/otel/sdk/log/internal/attrnorm" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/trace" ) @@ -118,6 +118,7 @@ type Record struct { scope *instrumentation.Scope attributeValueLengthLimit int + attributeValueDepthLimit int attributeCountLimit int // specifies whether we should deduplicate any key value collections or not @@ -195,7 +196,7 @@ func (r *Record) Body() attribute.Value { // SetBody sets the body of the log record. func (r *Record) SetBody(v attribute.Value) { if !r.allowDupKeys { - r.body, _ = attrdedup.Value(v) + r.body, _ = attrnorm.ValueDedup(v) } else { r.body = v } @@ -471,17 +472,28 @@ func (r *Record) Clone() Record { } func (r *Record) applyAttrLimitsAndDedup(attr attribute.KeyValue) attribute.KeyValue { + depthLimit := r.attrValueDepthLimit() if !r.allowDupKeys { var changed bool - attr, changed = attrdedup.KeyValue(attr) + attr, _ = attrnorm.KeyValueLimitDepth(attr, depthLimit) + attr, changed = attrnorm.KeyValueDedup(attr) if changed { logKeyValuePairDropped() } + } else { + attr, _ = attrnorm.KeyValueLimitDepth(attr, depthLimit) } attr.Value = truncateValue(r.attributeValueLengthLimit, attr.Value) return attr } +func (r *Record) attrValueDepthLimit() int { + if r.attributeValueDepthLimit != 0 { + return r.attributeValueDepthLimit + } + return defaultAttrValDepthLim +} + // truncateValue returns a truncated version of v. Only string, string slice, // byte slice, and (recursively) slice and map values are modified. // diff --git a/sdk/log/record_test.go b/sdk/log/record_test.go index 9d58b69ecf6..c9e21d9c61e 100644 --- a/sdk/log/record_test.go +++ b/sdk/log/record_test.go @@ -432,6 +432,134 @@ func TestRecordBody(t *testing.T) { } } +func logDepthLimitInputAttr(key string) attribute.KeyValue { + return attribute.Map( + key, + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ) +} + +func logDepthLimitWantAttr(key string) attribute.KeyValue { + return attribute.Map( + key, + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ) +} + +func TestRecordAttributeValueDepthLimit(t *testing.T) { + t.Run("AddAttributes", func(t *testing.T) { + r := Record{ + attributeValueLengthLimit: -1, + attributeValueDepthLimit: 2, + } + r.AddAttributes(logDepthLimitInputAttr("attr")) + assertKV(t, r, logDepthLimitWantAttr("attr")) + }) + + t.Run("SetAttributes", func(t *testing.T) { + r := Record{ + attributeValueLengthLimit: -1, + attributeValueDepthLimit: 2, + } + r.SetAttributes(logDepthLimitInputAttr("attr")) + assertKV(t, r, logDepthLimitWantAttr("attr")) + }) + + t.Run("Negative", func(t *testing.T) { + r := Record{ + attributeValueLengthLimit: -1, + attributeValueDepthLimit: -1, + } + r.AddAttributes(logDepthLimitInputAttr("attr")) + assertKV(t, r, logDepthLimitInputAttr("attr")) + }) + + t.Run("ZeroDefault", func(t *testing.T) { + r := Record{ + attributeValueLengthLimit: -1, + attributeValueDepthLimit: 0, + } + r.AddAttributes(logDepthLimitInputAttr("attr"), attribute.String("scalar", "ok")) + + var got []attribute.KeyValue + r.WalkAttributes(func(kv attribute.KeyValue) bool { + got = append(got, kv) + return true + }) + want := []attribute.KeyValue{ + logDepthLimitInputAttr("attr"), + attribute.String("scalar", "ok"), + } + assert.ElementsMatch(t, want, got) + }) + + t.Run("AllowKeyDuplication", func(t *testing.T) { + dup := attribute.Map( + "attr", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.Map( + "attr", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.KeyValue{Key: "over"}, + ) + r := Record{ + attributeValueLengthLimit: -1, + attributeValueDepthLimit: 1, + allowDupKeys: true, + } + r.AddAttributes(dup) + assertKV(t, r, want) + }) +} + +func TestRecordBodyAttributeValueDepthLimitNotApplied(t *testing.T) { + t.Run("DeduplicatesWithoutDepthLimit", func(t *testing.T) { + r := Record{ + attributeValueDepthLimit: 2, + } + r.SetBody(logDepthLimitInputAttr("body").Value) + assert.True(t, valueEqual(logDepthLimitInputAttr("body").Value, r.Body())) + }) + + t.Run("AllowKeyDuplication", func(t *testing.T) { + body := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + r := Record{ + attributeValueDepthLimit: 1, + allowDupKeys: true, + } + r.SetBody(body) + assert.True(t, valueEqual(want, r.Body())) + }) + + t.Run("Zero", func(t *testing.T) { + r := Record{ + attributeValueDepthLimit: 0, + } + body := attribute.SliceValue(attribute.StringValue("body")) + r.SetBody(body) + assert.True(t, valueEqual(body, r.Body())) + }) +} + func TestRecordAttributes(t *testing.T) { attrs := []attribute.KeyValue{ attribute.Bool("0", true), diff --git a/sdk/metric/instrument.go b/sdk/metric/instrument.go index ca32cb842c9..69926d064df 100644 --- a/sdk/metric/instrument.go +++ b/sdk/metric/instrument.go @@ -16,7 +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" + "go.opentelemetry.io/otel/sdk/metric/internal/attrnorm" ) var zeroScope instrumentation.Scope @@ -207,11 +207,11 @@ func extractRawKVs[T any](opts []T) []attribute.KeyValue { } func resolveAttributes(configAttrs attribute.Set, rawKVs []attribute.KeyValue) attribute.Set { - configAttrs, _ = attrdedup.Set(configAttrs) + configAttrs, _ = attrnorm.SetDedup(configAttrs) if len(rawKVs) == 0 { return configAttrs } - rawKVs, _ = attrdedup.KeyValues(rawKVs) + rawKVs, _ = attrnorm.KeyValuesDedup(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. diff --git a/sdk/metric/internal/attrdedup/dedup.go b/sdk/metric/internal/attrdedup/dedup.go deleted file mode 100644 index 547b602c5ef..00000000000 --- a/sdk/metric/internal/attrdedup/dedup.go +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// DO NOT MODIFY. Generated by gotmpl. -// source: internal/shared/attrdedup/dedup.go.tmpl - -// 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 and whether it changed. -// -// Duplicate map keys are resolved using last-value-wins semantics. -func Value(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 - } -} - -// KeyValue returns kv with all map values deduplicated and whether it changed. -func KeyValue(kv attribute.KeyValue) (attribute.KeyValue, bool) { - value, changed := Value(kv.Value) - if changed { - kv.Value = value - } - return kv, changed -} - -// KeyValues returns kvs with all map values deduplicated and whether they changed. -// -// 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, 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 := KeyValue(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 -} - -// Set returns set with all map values deduplicated and whether it changed. -// -// 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, bool) { - if set.Len() == 0 { - return set, false - } - - // 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 := range set.Len() { - kv, _ := set.Get(i) - kv, changed := KeyValue(kv) - if normalized != nil { - normalized = append(normalized, kv) - continue - } - if !changed { - continue - } - - normalized = make([]attribute.KeyValue, 0, set.Len()) - for j := range i { - prior, _ := set.Get(j) - normalized = append(normalized, prior) - } - normalized = append(normalized, kv) - } - if normalized == nil { - return set, false - } - - return attribute.NewSet(normalized...), true -} - -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 := range length { - elem := valueAt(storage, i) - elem, changed := Value(elem) - if normalized != nil { - normalized[i] = elem - continue - } - if !changed { - continue - } - - normalized = make([]attribute.Value, length) - for j := range i { - 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 := KeyValue(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 := KeyValue(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 := range i { - 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) -} diff --git a/sdk/metric/internal/attrdedup/dedup_test.go b/sdk/metric/internal/attrdedup/dedup_test.go deleted file mode 100644 index 82fab043ab4..00000000000 --- a/sdk/metric/internal/attrdedup/dedup_test.go +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -// DO NOT MODIFY. Generated by gotmpl. -// source: internal/shared/attrdedup/dedup_test.go.tmpl - -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 - wantChanged bool - }{ - { - 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"), - ), - wantChanged: false, - }, - { - 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"), - ), - wantChanged: true, - }, - { - 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"), - ), - wantChanged: true, - }, - { - 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"), - ), - ), - wantChanged: true, - }, - { - 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"), - ), - wantChanged: true, - }, - { - name: "unique slice", - value: attribute.SliceValue( - attribute.StringValue("one"), - attribute.IntValue(2), - ), - want: attribute.SliceValue( - attribute.StringValue("one"), - attribute.IntValue(2), - ), - wantChanged: false, - }, - { - 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"), - ), - wantChanged: true, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got, changed := Value(test.value) - if changed != test.wantChanged { - t.Fatalf("Value() changed = %v, want %v", changed, test.wantChanged) - } - 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 _, changed := Value(value); changed { - t.Fatal("Value() changed a no-op input") - } - 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, changed := Value(value) - if changed { - t.Fatal("Value() changed a no-op input") - } - 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, changed := Value(value) - if changed { - t.Fatal("Value() changed a no-op input") - } - 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, changed := KeyValue(kv) - if !changed { - t.Fatal("KeyValue() changed = false, want true") - } - 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, changed := KeyValues(kvs) - if changed { - t.Fatal("KeyValues() changed a no-op input") - } - 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, changed := KeyValues(kvs) - if !changed { - t.Fatal("KeyValues() changed = false, want true") - } - 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, changed := Set(set) - if !changed { - t.Fatal("Set() changed = false, want true") - } - 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, changed := Set(set) - if changed { - t.Fatal("Set() changed a no-op input") - } - if !got.Equals(&set) { - t.Fatal("Set() changed a no-op input") - } -} - -func TestSetEmpty(t *testing.T) { - set := attribute.Set{} - - got, changed := Set(set) - if changed { - t.Fatal("Set() changed an empty input") - } - 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) - } -} diff --git a/sdk/metric/internal/attrnorm/dedup.go b/sdk/metric/internal/attrnorm/dedup.go new file mode 100644 index 00000000000..c65381bd539 --- /dev/null +++ b/sdk/metric/internal/attrnorm/dedup.go @@ -0,0 +1,667 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/attrdedup/dedup.go.tmpl + +// Package attrnorm normalizes attribute values. +package attrnorm // import "go.opentelemetry.io/otel/sdk/metric/internal/attrnorm" + +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 +} + +// ValueDedup returns value with all map values deduplicated and whether it +// changed. +// +// Duplicate map keys are resolved using last-value-wins semantics. +func ValueDedup(value attribute.Value) (attribute.Value, bool) { + switch value.Type() { + case attribute.SLICE: + return sliceValueDedup(value) + case attribute.MAP: + return mapValueDedup(value) + default: + return value, false + } +} + +// ValueWithDepthLimit returns value with all map values deduplicated and all +// slice and map values limited to depth levels. +// +// Duplicate map keys are resolved using last-value-wins semantics. When a +// slice or map value would exceed a non-negative depth limit, that value is +// replaced by an empty value. A negative depth limit disables depth limiting. +func ValueWithDepthLimit(value attribute.Value, depthLimit int) (attribute.Value, bool) { + return valueDedupWithDepthLimit(value, depthLimit, 1) +} + +// ValueLimitDepth returns value with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +// +// When a slice or map value would exceed a non-negative depth limit, that +// value is replaced by an empty value. A negative depth limit disables depth +// limiting. +func ValueLimitDepth(value attribute.Value, depthLimit int) (attribute.Value, bool) { + return valueLimitDepth(value, depthLimit, 1) +} + +func valueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + switch value.Type() { + case attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE, attribute.STRINGSLICE, attribute.BYTESLICE: + return primitiveSliceValueLimitDepth(value, depthLimit, depth) + case attribute.SLICE: + return sliceValueDedupWithDepthLimit(value, depthLimit, depth) + case attribute.MAP: + return mapValueDedupWithDepthLimit(value, depthLimit, depth) + default: + return value, false + } +} + +func valueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + switch value.Type() { + case attribute.BOOLSLICE, attribute.INT64SLICE, attribute.FLOAT64SLICE, attribute.STRINGSLICE, attribute.BYTESLICE: + return primitiveSliceValueLimitDepth(value, depthLimit, depth) + case attribute.SLICE: + return sliceValueLimitDepth(value, depthLimit, depth) + case attribute.MAP: + return mapValueLimitDepth(value, depthLimit, depth) + default: + return value, false + } +} + +// KeyValueDedup returns kv with all map values deduplicated and whether it +// changed. +func KeyValueDedup(kv attribute.KeyValue) (attribute.KeyValue, bool) { + value, changed := ValueDedup(kv.Value) + if changed { + kv.Value = value + } + return kv, changed +} + +// KeyValueWithDepthLimit returns kv with all map values deduplicated and all +// slice and map values limited to depth levels. +func KeyValueWithDepthLimit(kv attribute.KeyValue, depthLimit int) (attribute.KeyValue, bool) { + return keyValueDedupWithDepthLimit(kv, depthLimit, 1) +} + +// KeyValueLimitDepth returns kv with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +func KeyValueLimitDepth(kv attribute.KeyValue, depthLimit int) (attribute.KeyValue, bool) { + return keyValueLimitDepth(kv, depthLimit, 1) +} + +func keyValueDedupWithDepthLimit(kv attribute.KeyValue, depthLimit, depth int) (attribute.KeyValue, bool) { + value, changed := valueDedupWithDepthLimit(kv.Value, depthLimit, depth) + if changed { + kv.Value = value + } + return kv, changed +} + +func keyValueLimitDepth(kv attribute.KeyValue, depthLimit, depth int) (attribute.KeyValue, bool) { + value, changed := valueLimitDepth(kv.Value, depthLimit, depth) + if changed { + kv.Value = value + } + return kv, changed +} + +// KeyValuesDedup returns kvs with all map values deduplicated and whether they +// changed. +// +// The returned slice is the original kvs slice if no value needs +// deduplication. Top-level keys in kvs are not deduplicated. +func KeyValuesDedup(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 := KeyValueDedup(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 +} + +// KeyValuesWithDepthLimit returns kvs with all map values deduplicated and all +// slice and map values limited to depth levels. +// +// The returned slice is the original kvs slice if no value needs +// normalization. Top-level keys in kvs are not deduplicated. +func KeyValuesWithDepthLimit(kvs []attribute.KeyValue, depthLimit int) ([]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 := keyValueDedupWithDepthLimit(kv, depthLimit, 1) + 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 +} + +// KeyValuesLimitDepth returns kvs with all slice and map values limited to +// depth levels. Map keys are not deduplicated. +func KeyValuesLimitDepth(kvs []attribute.KeyValue, depthLimit int) ([]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 := keyValueLimitDepth(kv, depthLimit, 1) + 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 +} + +// SetDedup returns set with all map values deduplicated and whether it changed. +// +// 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 SetDedup(set attribute.Set) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := KeyValueDedup(kv) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +// SetWithDepthLimit returns set with all map values deduplicated and all slice +// and map values limited to depth levels. +// +// The returned Set is the original set if no value needs normalization. +// Top-level key uniqueness remains attribute.Set's responsibility; this only +// normalizes attribute values. +func SetWithDepthLimit(set attribute.Set, depthLimit int) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := keyValueDedupWithDepthLimit(kv, depthLimit, 1) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +// SetLimitDepth returns set with all slice and map values limited to depth +// levels. Map keys are not deduplicated. +func SetLimitDepth(set attribute.Set, depthLimit int) (attribute.Set, bool) { + if set.Len() == 0 { + return set, false + } + + // 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 := range set.Len() { + kv, _ := set.Get(i) + kv, changed := keyValueLimitDepth(kv, depthLimit, 1) + if normalized != nil { + normalized = append(normalized, kv) + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, 0, set.Len()) + for j := range i { + prior, _ := set.Get(j) + normalized = append(normalized, prior) + } + normalized = append(normalized, kv) + } + if normalized == nil { + return set, false + } + + return attribute.NewSet(normalized...), true +} + +func sliceValueDedup(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 := range length { + elem := valueAt(storage, i) + elem, changed := ValueDedup(elem) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func primitiveSliceValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if !exceedsDepthLimit(depthLimit, depth) { + return value, false + } + return attribute.Value{}, true +} + +func sliceValueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + 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 := range length { + elem := valueAt(storage, i) + elem, changed := valueDedupWithDepthLimit(elem, depthLimit, depth+1) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func sliceValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + storage := valueStorage(value) + length := valueLen(storage) + + var normalized []attribute.Value + for i := range length { + elem := valueAt(storage, i) + elem, changed := valueLimitDepth(elem, depthLimit, depth+1) + if normalized != nil { + normalized[i] = elem + continue + } + if !changed { + continue + } + + normalized = make([]attribute.Value, length) + for j := range i { + normalized[j] = valueAt(storage, j) + } + normalized[i] = elem + } + if normalized == nil { + return value, false + } + return attribute.SliceValue(normalized...), true +} + +func mapValueDedup(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 := KeyValueDedup(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 := KeyValueDedup(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 := range i { + normalized = append(normalized, keyValueAt(storage, k)) + } + normalized = append(normalized, kv) + } + i = j + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func mapValueDedupWithDepthLimit(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + 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 := keyValueDedupWithDepthLimit(keyValueAt(storage, 0), depthLimit, depth+1) + 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 := keyValueDedupWithDepthLimit(keyValueAt(storage, j-1), depthLimit, depth+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 := range i { + normalized = append(normalized, keyValueAt(storage, k)) + } + normalized = append(normalized, kv) + } + i = j + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func mapValueLimitDepth(value attribute.Value, depthLimit, depth int) (attribute.Value, bool) { + if exceedsDepthLimit(depthLimit, depth) { + return attribute.Value{}, true + } + + storage := valueStorage(value) + length := keyValueLen(storage) + + var normalized []attribute.KeyValue + for i := range length { + kv := keyValueAt(storage, i) + kv, changed := keyValueLimitDepth(kv, depthLimit, depth+1) + if normalized != nil { + normalized[i] = kv + continue + } + if !changed { + continue + } + + normalized = make([]attribute.KeyValue, length) + for j := range i { + normalized[j] = keyValueAt(storage, j) + } + normalized[i] = kv + } + if normalized == nil { + return value, false + } + return attribute.MapValue(normalized...), true +} + +func exceedsDepthLimit(depthLimit, depth int) bool { + return depthLimit >= 0 && depth > depthLimit +} + +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) +} diff --git a/sdk/metric/internal/attrnorm/dedup_test.go b/sdk/metric/internal/attrnorm/dedup_test.go new file mode 100644 index 00000000000..038c90c5ad1 --- /dev/null +++ b/sdk/metric/internal/attrnorm/dedup_test.go @@ -0,0 +1,1178 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// DO NOT MODIFY. Generated by gotmpl. +// source: internal/shared/attrdedup/dedup_test.go.tmpl + +package attrnorm + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "go.opentelemetry.io/otel/attribute" +) + +var cmpValue = cmp.AllowUnexported(attribute.Value{}) + +func TestValueDedup(t *testing.T) { + tests := []struct { + name string + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + 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"), + ), + wantChanged: false, + }, + { + 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"), + ), + wantChanged: true, + }, + { + 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"), + ), + wantChanged: true, + }, + { + 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"), + ), + ), + wantChanged: true, + }, + { + 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"), + ), + wantChanged: true, + }, + { + name: "unique slice", + value: attribute.SliceValue( + attribute.StringValue("one"), + attribute.IntValue(2), + ), + want: attribute.SliceValue( + attribute.StringValue("one"), + attribute.IntValue(2), + ), + wantChanged: false, + }, + { + 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"), + ), + wantChanged: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueDedup(test.value) + if changed != test.wantChanged { + t.Fatalf("ValueDedup() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueWithDepthLimit(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "below limit", + limit: 3, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.String("leaf", "value"), + ), + ), + ), + }, + { + name: "at limit", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + }, + { + name: "multi-entry map below limit", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + }, + { + name: "deduplicate multi-entry map", + limit: 2, + value: attribute.MapValue( + attribute.String("duplicate", "first"), + attribute.String("duplicate", "second"), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("duplicate", "second"), + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "deduplicate multi-entry map after prior key", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.String("middle", "first"), + attribute.String("middle", "second"), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.String("middle", "second"), + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "slice below limit", + limit: 2, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + }, + { + name: "map over limit", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.KeyValue{Key: "level2"}, + ), + ), + wantChanged: true, + }, + { + name: "slice over limit", + limit: 1, + value: attribute.SliceValue( + attribute.SliceValue(attribute.StringValue("over")), + attribute.StringValue("leaf"), + ), + want: attribute.SliceValue( + attribute.Value{}, + attribute.StringValue("leaf"), + ), + wantChanged: true, + }, + { + name: "slice over limit after scalar", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("over")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.Value{}, + attribute.StringValue("tail"), + ), + wantChanged: true, + }, + { + name: "zero allows scalar", + limit: 0, + value: attribute.StringValue("leaf"), + want: attribute.StringValue("leaf"), + wantChanged: false, + }, + { + name: "zero replaces slice", + limit: 0, + value: attribute.SliceValue(attribute.StringValue("leaf")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "zero replaces map", + limit: 0, + value: attribute.MapValue(attribute.String("leaf", "value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "negative disables depth limit", + limit: -1, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map( + "level3", + attribute.String("leaf", "value"), + ), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Map( + "level2", + attribute.Map( + "level3", + attribute.String("leaf", "value"), + ), + ), + ), + ), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueWithDepthLimit(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("ValueWithDepthLimit() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueWithDepthLimit() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueDepthLimitSpecCases(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "raw limit zero preserves scalar", + limit: 0, + value: attribute.StringValue("value"), + want: attribute.StringValue("value"), + wantChanged: false, + }, + { + name: "raw limit zero replaces top-level heterogeneous array", + limit: 0, + value: attribute.SliceValue(attribute.StringValue("value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "raw limit zero replaces top-level map", + limit: 0, + value: attribute.MapValue(attribute.String("key", "value")), + want: attribute.Value{}, + wantChanged: true, + }, + { + name: "limit one preserves top-level heterogeneous array and replaces nested collections", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.MapValue(attribute.String("nested", "value")), + ), + want: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.Value{}, + attribute.Value{}, + ), + wantChanged: true, + }, + { + name: "limit one replaces nested homogeneous array in heterogeneous array", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.StringSliceValue([]string{"nested"}), + ), + want: attribute.SliceValue( + attribute.StringValue("scalar"), + attribute.Value{}, + ), + wantChanged: true, + }, + { + name: "limit one preserves top-level map and replaces nested collections", + limit: 1, + value: attribute.MapValue( + attribute.Slice("array", attribute.StringValue("nested")), + attribute.Map("map", attribute.String("nested", "value")), + attribute.StringSlice("primitive-array", []string{"nested"}), + attribute.String("scalar", "value"), + ), + want: attribute.MapValue( + attribute.KeyValue{Key: "array"}, + attribute.KeyValue{Key: "map"}, + attribute.KeyValue{Key: "primitive-array"}, + attribute.String("scalar", "value"), + ), + wantChanged: true, + }, + { + name: "nested heterogeneous array beyond limit replaced", + limit: 2, + value: attribute.SliceValue( + attribute.SliceValue( + attribute.SliceValue(attribute.StringValue("over")), + ), + ), + want: attribute.SliceValue( + attribute.SliceValue( + attribute.Value{}, + ), + ), + wantChanged: true, + }, + { + name: "negative limit preserves nested collections", + limit: -1, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.Slice( + "level2", + attribute.MapValue(attribute.String("leaf", "value")), + ), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.Slice( + "level2", + attribute.MapValue(attribute.String("leaf", "value")), + ), + ), + ), + wantChanged: false, + }, + { + name: "otherwise value is unchanged", + limit: 2, + value: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + want: attribute.MapValue( + attribute.Map( + "level1", + attribute.String("leaf", "value"), + ), + ), + wantChanged: false, + }, + } + + fns := []struct { + name string + fn func(attribute.Value, int) (attribute.Value, bool) + }{ + { + name: "ValueWithDepthLimit", + fn: ValueWithDepthLimit, + }, + { + name: "ValueLimitDepth", + fn: ValueLimitDepth, + }, + } + + for _, fn := range fns { + t.Run(fn.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := fn.fn(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("%s() changed = %v, want %v", fn.name, changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("%s() mismatch (-want +got):\n%s", fn.name, diff) + } + }) + } + }) + } +} + +func TestValueDepthLimitPrimitiveSliceTypes(t *testing.T) { + tests := []struct { + name string + value attribute.Value + want attribute.Value + }{ + { + name: "bool", + value: attribute.BoolSliceValue([]bool{true}), + want: attribute.Value{}, + }, + { + name: "int64", + value: attribute.Int64SliceValue([]int64{1}), + want: attribute.Value{}, + }, + { + name: "float64", + value: attribute.Float64SliceValue([]float64{1}), + want: attribute.Value{}, + }, + { + name: "string", + value: attribute.StringSliceValue([]string{"value"}), + want: attribute.Value{}, + }, + { + name: "bytes", + value: attribute.ByteSliceValue([]byte("value")), + want: attribute.Value{}, + }, + } + + fns := []struct { + name string + fn func(attribute.Value, int) (attribute.Value, bool) + }{ + { + name: "ValueWithDepthLimit", + fn: ValueWithDepthLimit, + }, + { + name: "ValueLimitDepth", + fn: ValueLimitDepth, + }, + } + + for _, fn := range fns { + t.Run(fn.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := fn.fn(test.value, 0) + if !changed { + t.Fatalf("%s() changed = false, want true", fn.name) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("%s() limit 0 mismatch (-want +got):\n%s", fn.name, diff) + } + + got, changed = fn.fn(test.value, 1) + if changed { + t.Fatalf("%s() changed = true, want false", fn.name) + } + if diff := cmp.Diff(test.value, got, cmpValue); diff != "" { + t.Fatalf("%s() limit 1 mismatch (-want +got):\n%s", fn.name, diff) + } + }) + } + }) + } +} + +func TestValueLimitDepth(t *testing.T) { + tests := []struct { + name string + limit int + value attribute.Value + want attribute.Value + wantChanged bool + }{ + { + name: "scalar", + limit: 1, + value: attribute.StringValue("value"), + want: attribute.StringValue("value"), + wantChanged: false, + }, + { + name: "slice below limit", + limit: 2, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + wantChanged: false, + }, + { + name: "map below limit", + limit: 2, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map( + "nested", + attribute.String("leaf", "value"), + ), + attribute.String("tail", "value"), + ), + wantChanged: false, + }, + { + name: "map over limit after prior key", + limit: 1, + value: attribute.MapValue( + attribute.String("first", "value"), + attribute.Map("middle", attribute.String("leaf", "value")), + attribute.String("tail", "value"), + ), + want: attribute.MapValue( + attribute.String("first", "value"), + attribute.KeyValue{Key: "middle"}, + attribute.String("tail", "value"), + ), + wantChanged: true, + }, + { + name: "slice over limit", + limit: 1, + value: attribute.SliceValue( + attribute.StringValue("first"), + attribute.SliceValue(attribute.StringValue("nested")), + attribute.StringValue("tail"), + ), + want: attribute.SliceValue( + attribute.StringValue("first"), + attribute.Value{}, + attribute.StringValue("tail"), + ), + wantChanged: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := ValueLimitDepth(test.value, test.limit) + if changed != test.wantChanged { + t.Fatalf("ValueLimitDepth() changed = %v, want %v", changed, test.wantChanged) + } + if diff := cmp.Diff(test.want, got, cmpValue); diff != "" { + t.Fatalf("ValueLimitDepth() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestValueLimitDepthPreservesDuplicateMapKeys(t *testing.T) { + value := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.MapValue( + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.KeyValue{Key: "over"}, + ) + + got, changed := ValueLimitDepth(value, 1) + if !changed { + t.Fatal("ValueLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("ValueLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestValueDedupNoopAllocationFree(t *testing.T) { + value := attribute.MapValue( + attribute.String("one", "1"), + attribute.String("two", "2"), + ) + var got attribute.Value + + allocs := testing.AllocsPerRun(1000, func() { + got, _ = ValueDedup(value) + }) + if allocs != 0 { + t.Fatalf("ValueDedup() allocations = %v, want 0", allocs) + } + if _, changed := ValueDedup(value); changed { + t.Fatal("ValueDedup() changed a no-op input") + } + if diff := cmp.Diff(value, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestValueDedupStorageShapes(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, changed := ValueDedup(value) + if changed { + t.Fatal("ValueDedup() changed a no-op input") + } + if diff := cmp.Diff(value, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() 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, changed := ValueDedup(value) + if changed { + t.Fatal("ValueDedup() changed a no-op input") + } + if diff := cmp.Diff(value, got, cmpValue); diff != "" { + t.Fatalf("ValueDedup() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestKeyValueDedup(t *testing.T) { + kv := attribute.Map( + "map", + attribute.String("nested", "first"), + attribute.String("nested", "second"), + ) + want := attribute.Map( + "map", + attribute.String("nested", "second"), + ) + + got, changed := KeyValueDedup(kv) + if !changed { + t.Fatal("KeyValueDedup() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValueWithDepthLimit(t *testing.T) { + kv := attribute.Map( + "map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ) + want := attribute.Map( + "map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ) + + got, changed := KeyValueWithDepthLimit(kv, 2) + if !changed { + t.Fatal("KeyValueWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValueLimitDepth(t *testing.T) { + kv := attribute.Map( + "map", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.Map("over", attribute.String("leaf", "value")), + ) + want := attribute.Map( + "map", + attribute.String("dup", "first"), + attribute.String("dup", "second"), + attribute.KeyValue{Key: "over"}, + ) + + got, changed := KeyValueLimitDepth(kv, 1) + if !changed { + t.Fatal("KeyValueLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValueLimitDepth() 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, changed := KeyValuesDedup(kvs) + if changed { + t.Fatal("KeyValuesDedup() changed a no-op input") + } + if len(got) != len(kvs) { + t.Fatalf("KeyValuesDedup() length = %d, want %d", len(got), len(kvs)) + } + if &got[0] != &kvs[0] { + t.Fatal("KeyValuesDedup() copied a no-op input") + } +} + +func TestKeyValuesDedup(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, changed := KeyValuesDedup(kvs) + if !changed { + t.Fatal("KeyValuesDedup() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValuesWithDepthLimit(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + attribute.String("tail", "value"), + } + want := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ), + attribute.String("tail", "value"), + } + + got, changed := KeyValuesWithDepthLimit(kvs, 2) + if !changed { + t.Fatal("KeyValuesWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestKeyValuesDepthLimitNoopReturnsInput(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("one", "1"), + attribute.Map("two", attribute.String("nested", "value")), + } + tests := []struct { + name string + fn func([]attribute.KeyValue, int) ([]attribute.KeyValue, bool) + }{ + { + name: "KeyValuesWithDepthLimit", + fn: KeyValuesWithDepthLimit, + }, + { + name: "KeyValuesLimitDepth", + fn: KeyValuesLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(kvs, 2) + if changed { + t.Fatalf("%s() changed a no-op input", test.name) + } + if len(got) != len(kvs) { + t.Fatalf("%s() length = %d, want %d", test.name, len(got), len(kvs)) + } + if &got[0] != &kvs[0] { + t.Fatalf("%s() copied a no-op input", test.name) + } + }) + } +} + +func TestKeyValuesLimitDepth(t *testing.T) { + kvs := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.Map("over", attribute.String("leaf", "value")), + ), + attribute.String("tail", "value"), + } + want := []attribute.KeyValue{ + attribute.String("top", "value"), + attribute.Map( + "map", + attribute.KeyValue{Key: "over"}, + ), + attribute.String("tail", "value"), + } + + got, changed := KeyValuesLimitDepth(kvs, 1) + if !changed { + t.Fatal("KeyValuesLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want, got, cmpValue); diff != "" { + t.Fatalf("KeyValuesLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetDedup(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, changed := SetDedup(set) + if !changed { + t.Fatal("SetDedup() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetDedup() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetWithDepthLimit(t *testing.T) { + set := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ), + attribute.String("z-tail", "value"), + ) + want := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ), + attribute.String("z-tail", "value"), + ) + + got, changed := SetWithDepthLimit(set, 2) + if !changed { + t.Fatal("SetWithDepthLimit() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetWithDepthLimit() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetLimitDepth(t *testing.T) { + set := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.Map("over", attribute.String("leaf", "value")), + ), + attribute.String("z-tail", "value"), + ) + want := attribute.NewSet( + attribute.String("a-top", "value"), + attribute.Map( + "m-map", + attribute.KeyValue{Key: "over"}, + ), + attribute.String("z-tail", "value"), + ) + + got, changed := SetLimitDepth(set, 1) + if !changed { + t.Fatal("SetLimitDepth() changed = false, want true") + } + if diff := cmp.Diff(want.ToSlice(), got.ToSlice(), cmpValue); diff != "" { + t.Fatalf("SetLimitDepth() mismatch (-want +got):\n%s", diff) + } +} + +func TestSetDepthLimitNoop(t *testing.T) { + set := attribute.NewSet( + attribute.String("top", "value"), + attribute.Map("map", attribute.String("nested", "value")), + ) + tests := []struct { + name string + fn func(attribute.Set, int) (attribute.Set, bool) + }{ + { + name: "SetWithDepthLimit", + fn: SetWithDepthLimit, + }, + { + name: "SetLimitDepth", + fn: SetLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(set, 2) + if changed { + t.Fatalf("%s() changed a no-op input", test.name) + } + if !got.Equals(&set) { + t.Fatalf("%s() changed a no-op input", test.name) + } + }) + } +} + +func TestSetDedupNoop(t *testing.T) { + set := attribute.NewSet( + attribute.String("top", "value"), + attribute.Map("map", attribute.String("nested", "value")), + ) + + got, changed := SetDedup(set) + if changed { + t.Fatal("SetDedup() changed a no-op input") + } + if !got.Equals(&set) { + t.Fatal("SetDedup() changed a no-op input") + } +} + +func TestSetDedupEmpty(t *testing.T) { + set := attribute.Set{} + + got, changed := SetDedup(set) + if changed { + t.Fatal("SetDedup() changed an empty input") + } + if !got.Equals(&set) { + t.Fatal("SetDedup() changed an empty input") + } +} + +func TestSetDepthLimitEmpty(t *testing.T) { + set := attribute.Set{} + tests := []struct { + name string + fn func(attribute.Set, int) (attribute.Set, bool) + }{ + { + name: "SetWithDepthLimit", + fn: SetWithDepthLimit, + }, + { + name: "SetLimitDepth", + fn: SetLimitDepth, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, changed := test.fn(set, 1) + if changed { + t.Fatalf("%s() changed an empty input", test.name) + } + if !got.Equals(&set) { + t.Fatalf("%s() changed an empty input", test.name) + } + }) + } +} + +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) + } +} diff --git a/sdk/metric/internal/gen.go b/sdk/metric/internal/gen.go index f79a8e5ccbe..f069192ade6 100644 --- a/sdk/metric/internal/gen.go +++ b/sdk/metric/internal/gen.go @@ -6,5 +6,5 @@ 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 +//go:generate gotmpl --body=../../../internal/shared/attrdedup/dedup.go.tmpl "--data={}" --out=attrnorm/dedup.go +//go:generate gotmpl --body=../../../internal/shared/attrdedup/dedup_test.go.tmpl "--data={}" --out=attrnorm/dedup_test.go diff --git a/sdk/metric/provider.go b/sdk/metric/provider.go index f01275507b7..bc2d7248f08 100644 --- a/sdk/metric/provider.go +++ b/sdk/metric/provider.go @@ -12,7 +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" + "go.opentelemetry.io/otel/sdk/metric/internal/attrnorm" ) // MeterProvider handles the creation and coordination of Meters. All Meters @@ -43,7 +43,13 @@ func NewMeterProvider(options ...Option) *MeterProvider { flush, sdown := conf.readerSignals() mp := &MeterProvider{ - pipes: newPipelines(conf.res, conf.readers, conf.views, conf.exemplarFilter, conf.cardinalityLimit), + pipes: newPipelines( + conf.res, + conf.readers, + conf.views, + conf.exemplarFilter, + conf.cardinalityLimit, + ), forceFlush: flush, shutdown: sdown, } @@ -77,7 +83,7 @@ func (mp *MeterProvider) Meter(name string, options ...metric.MeterOption) metri } c := metric.NewMeterConfig(options...) - attrs, _ := attrdedup.Set(c.InstrumentationAttributes()) + attrs, _ := attrnorm.SetDedup(c.InstrumentationAttributes()) s := instrumentation.Scope{ Name: name, Version: c.InstrumentationVersion(), diff --git a/sdk/metric/provider_test.go b/sdk/metric/provider_test.go index 7f64e1ad710..81f65145ddc 100644 --- a/sdk/metric/provider_test.go +++ b/sdk/metric/provider_test.go @@ -19,6 +19,7 @@ import ( api "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/sdk/metric/metricdata" + "go.opentelemetry.io/otel/sdk/resource" ) func TestMeterConcurrentSafe(*testing.T) { @@ -127,6 +128,47 @@ func TestMeterProviderReturnsNoopMeterAfterShutdown(t *testing.T) { assert.Truef(t, ok, "Meter from shutdown MeterProvider is not NoOp: %T", m) } +func metricDeeplyNestedMapAttr(key string) attribute.KeyValue { + const depthBeyondDefaultLimit = 65 + + value := attribute.StringValue("value") + for i := depthBeyondDefaultLimit - 1; i >= 0; i-- { + value = attribute.MapValue(attribute.KeyValue{ + Key: attribute.Key(fmt.Sprintf("level%d", i)), + Value: value, + }) + } + return attribute.KeyValue{Key: attribute.Key(key), Value: value} +} + +func TestMeterProviderAttributeLimitsExempt(t *testing.T) { + reader := NewManualReader() + resAttr := metricDeeplyNestedMapAttr("resource") + scopeAttr := metricDeeplyNestedMapAttr("scope") + measurementAttr := metricDeeplyNestedMapAttr("measurement") + mp := NewMeterProvider( + WithReader(reader), + WithResource(resource.NewSchemaless(resAttr)), + ) + meter := mp.Meter("scope", api.WithInstrumentationAttributes(scopeAttr)) + counter, err := meter.Int64Counter("counter") + require.NoError(t, err) + + counter.Add(t.Context(), 1, api.WithAttributes(measurementAttr)) + + var rm metricdata.ResourceMetrics + require.NoError(t, reader.Collect(t.Context(), &rm)) + + assert.Equal(t, []attribute.KeyValue{resAttr}, rm.Resource.Attributes()) + require.Len(t, rm.ScopeMetrics, 1) + assert.Equal(t, attribute.NewSet(scopeAttr), 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(measurementAttr), sum.DataPoints[0].Attributes) +} + func TestMeterProviderMixingOnRegisterErrors(t *testing.T) { otel.SetLogger(testr.New(t)) diff --git a/sdk/resource/resource.go b/sdk/resource/resource.go index 935d3b3c31e..a0ccfa4c88d 100644 --- a/sdk/resource/resource.go +++ b/sdk/resource/resource.go @@ -11,7 +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/attrnorm" "go.opentelemetry.io/otel/sdk/internal/x" ) @@ -90,7 +90,7 @@ func NewSchemaless(attrs ...attribute.KeyValue) *Resource { return &Resource{} } - attrs, _ = attrdedup.KeyValues(attrs) + attrs, _ = attrnorm.KeyValuesDedup(attrs) // Ensure attributes comply with the specification: // https://github.com/open-telemetry/opentelemetry-specification/blob/v1.20.0/specification/common/README.md#attribute diff --git a/sdk/trace/provider.go b/sdk/trace/provider.go index 6a328c5a1bf..643f9568553 100644 --- a/sdk/trace/provider.go +++ b/sdk/trace/provider.go @@ -13,7 +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/internal/attrnorm" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace/internal/observ" "go.opentelemetry.io/otel/trace" @@ -143,7 +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()) + attrs, _ := attrnorm.SetWithDepthLimit(c.InstrumentationAttributes(), p.spanLimits.AttributeValueDepthLimit) if name == "" { name = defaultTracerName } @@ -412,6 +412,28 @@ func WithSampler(s Sampler) TracerProviderOption { }) } +// WithAttributeValueDepthLimit sets the maximum allowed depth for nested +// attribute values. Any slice or map value beyond this depth will be +// replaced with an empty value. +// +// This limit applies to span, event, link, and instrumentation scope +// attributes processed by this TracerProvider. +// +// Setting this to zero means the default limit is used. +// +// Setting this to a negative value means no limit is applied. +// +// By default, 64 will be used. +func WithAttributeValueDepthLimit(limit int) TracerProviderOption { + return traceProviderOptionFunc(func(cfg tracerProviderConfig) tracerProviderConfig { + if limit == 0 { + limit = DefaultAttributeValueDepthLimit + } + cfg.spanLimits.AttributeValueDepthLimit = limit + return cfg + }) +} + // WithSpanLimits returns a TracerProviderOption that configures a // TracerProvider to use the SpanLimits sl. These SpanLimits bound any Span // created by a Tracer from the TracerProvider. @@ -425,12 +447,15 @@ func WithSampler(s Sampler) TracerProviderOption { // relationship. // // Deprecated: Use WithRawSpanLimits instead which allows setting unlimited -// and zero limits. This option will be kept until the next major version -// incremented release. +// limits. This option will be kept until the next major version incremented +// release. func WithSpanLimits(sl SpanLimits) TracerProviderOption { if sl.AttributeValueLengthLimit <= 0 { sl.AttributeValueLengthLimit = DefaultAttributeValueLengthLimit } + if sl.AttributeValueDepthLimit <= 0 { + sl.AttributeValueDepthLimit = DefaultAttributeValueDepthLimit + } if sl.AttributeCountLimit <= 0 { sl.AttributeCountLimit = DefaultAttributeCountLimit } @@ -456,13 +481,14 @@ func WithSpanLimits(sl SpanLimits) TracerProviderOption { // TracerProvider to use these limits. These limits bound any Span created by // a Tracer from the TracerProvider. // -// The limits will be used as-is. Zero or negative values will not be changed -// to the default value like WithSpanLimits does. Setting a limit to zero will -// effectively disable the related resource it limits and setting to a -// negative value will mean that resource is unlimited. Consequentially, this -// means that the zero-value SpanLimits will disable all span resources. -// Because of this, limits should be constructed using NewSpanLimits and -// updated accordingly. +// The limits will be used as-is, except AttributeValueDepthLimit where zero +// means the default limit is used. Other zero or negative values will not be +// changed to the default value like WithSpanLimits does. Setting a limit to +// zero will effectively disable the related resource it limits and setting to +// a negative value will mean that resource is unlimited. Consequentially, this +// means that the zero-value SpanLimits will disable all span resources except +// AttributeValueDepthLimit. Because of this, limits should be constructed using +// NewSpanLimits and updated accordingly. // // If this or WithSpanLimits are not provided, the TracerProvider will use the // limits defined by environment variables, or the defaults if unset. Refer to @@ -508,5 +534,8 @@ func ensureValidTracerProviderConfig(cfg tracerProviderConfig) tracerProviderCon if cfg.resource == nil { cfg.resource = resource.Default() } + if cfg.spanLimits.AttributeValueDepthLimit == 0 { + cfg.spanLimits.AttributeValueDepthLimit = DefaultAttributeValueDepthLimit + } return cfg } diff --git a/sdk/trace/span.go b/sdk/trace/span.go index cab9523fc55..ff5b1f80d91 100644 --- a/sdk/trace/span.go +++ b/sdk/trace/span.go @@ -19,7 +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/internal/attrnorm" "go.opentelemetry.io/otel/sdk/resource" semconv "go.opentelemetry.io/otel/semconv/v1.42.0" "go.opentelemetry.io/otel/trace" @@ -270,7 +270,7 @@ func (s *recordingSpan) SetAttributes(attributes ...attribute.KeyValue) { s.addDroppedAttr(1) continue } - a = dedupAttr(a) + a = dedupAttr(a, s.tracer.provider.spanLimits.AttributeValueDepthLimit) a = truncateAttr(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a) s.attributes = append(s.attributes, a) } @@ -331,7 +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 = dedupAttr(a, s.tracer.provider.spanLimits.AttributeValueDepthLimit) a = truncateAttr(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a) s.attributes[idx] = a continue @@ -342,7 +342,7 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) { // updates are checked and performed. s.addDroppedAttr(1) } else { - a = dedupAttr(a) + a = dedupAttr(a, s.tracer.provider.spanLimits.AttributeValueDepthLimit) a = truncateAttr(s.tracer.provider.spanLimits.AttributeValueLengthLimit, a) s.attributes = append(s.attributes, a) exists[a.Key] = len(s.attributes) - 1 @@ -350,10 +350,10 @@ func (s *recordingSpan) addOverCapAttrs(limit int, attrs []attribute.KeyValue) { } } -func dedupAttr(attr attribute.KeyValue) attribute.KeyValue { +func dedupAttr(attr attribute.KeyValue, depthLimit int) attribute.KeyValue { switch attr.Value.Type() { case attribute.SLICE, attribute.MAP: - attr, _ = attrdedup.KeyValue(attr) + attr, _ = attrnorm.KeyValueWithDepthLimit(attr, depthLimit) return attr default: return attr @@ -731,7 +731,7 @@ 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...) - attrs, _ := attrdedup.KeyValues(c.Attributes()) + attrs, _ := attrnorm.KeyValuesWithDepthLimit(c.Attributes(), s.tracer.provider.spanLimits.AttributeValueDepthLimit) e := Event{Name: name, Attributes: attrs, Time: c.Timestamp()} // Discard attributes over limit. @@ -905,7 +905,7 @@ func (s *recordingSpan) AddLink(link trace.Link) { return } - attrs, _ := attrdedup.KeyValues(link.Attributes) + attrs, _ := attrnorm.KeyValuesWithDepthLimit(link.Attributes, s.tracer.provider.spanLimits.AttributeValueDepthLimit) l := Link{SpanContext: link.SpanContext, Attributes: attrs} // Discard attributes over limit. diff --git a/sdk/trace/span_limits.go b/sdk/trace/span_limits.go index 2fe891bd492..1834b396bc1 100644 --- a/sdk/trace/span_limits.go +++ b/sdk/trace/span_limits.go @@ -10,6 +10,10 @@ const ( // attribute value length, unlimited. DefaultAttributeValueLengthLimit = -1 + // DefaultAttributeValueDepthLimit is the default maximum allowed depth for + // nested attribute values. + DefaultAttributeValueDepthLimit = 64 + // DefaultAttributeCountLimit is the default maximum number of attributes // a span can have. DefaultAttributeCountLimit = 128 @@ -43,6 +47,15 @@ type SpanLimits struct { // Setting this to a negative value means no limit is applied. AttributeValueLengthLimit int + // AttributeValueDepthLimit is the maximum allowed depth for nested + // attribute values. Any slice or map value beyond this depth will be + // replaced with an empty value. + // + // Setting this to zero means the default limit is used. + // + // Setting this to a negative value means no limit is applied. + AttributeValueDepthLimit int + // AttributeCountLimit is the maximum allowed span attribute count. Any // attribute added to a span once this limit is reached will be dropped. // @@ -94,6 +107,8 @@ type SpanLimits struct { // • AttributeValueLengthLimit: OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT // (default: unlimited) // +// • AttributeValueDepthLimit: (default: 64) +// // • AttributeCountLimit: OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT (default: 128) // // • EventCountLimit: OTEL_SPAN_EVENT_COUNT_LIMIT (default: 128) @@ -107,6 +122,7 @@ type SpanLimits struct { func NewSpanLimits() SpanLimits { return SpanLimits{ AttributeValueLengthLimit: env.SpanAttributeValueLength(DefaultAttributeValueLengthLimit), + AttributeValueDepthLimit: DefaultAttributeValueDepthLimit, AttributeCountLimit: env.SpanAttributeCount(DefaultAttributeCountLimit), EventCountLimit: env.SpanEventCount(DefaultEventCountLimit), LinkCountLimit: env.SpanLinkCount(DefaultLinkCountLimit), diff --git a/sdk/trace/span_limits_test.go b/sdk/trace/span_limits_test.go index 6cae8259dca..80bcaeeb4ee 100644 --- a/sdk/trace/span_limits_test.go +++ b/sdk/trace/span_limits_test.go @@ -30,6 +30,7 @@ func TestSettingSpanLimits(t *testing.T) { limits := func(n int) *SpanLimits { lims := NewSpanLimits() lims.AttributeValueLengthLimit = n + lims.AttributeValueDepthLimit = n lims.AttributeCountLimit = n lims.EventCountLimit = n lims.LinkCountLimit = n @@ -37,6 +38,11 @@ func TestSettingSpanLimits(t *testing.T) { lims.AttributePerLinkCountLimit = n return &lims } + envWant := func(n int) SpanLimits { + lims := *limits(n) + lims.AttributeValueDepthLimit = DefaultAttributeValueDepthLimit + return lims + } tests := []struct { name string @@ -52,7 +58,7 @@ func TestSettingSpanLimits(t *testing.T) { { name: "env", env: envLimits("42"), - want: *limits(42), + want: envWant(42), }, { name: "opt", @@ -64,6 +70,13 @@ func TestSettingSpanLimits(t *testing.T) { rawOpt: limits(42), want: *limits(42), }, + { + name: "raw-opt-zero-depth", + rawOpt: limits(0), + want: SpanLimits{ + AttributeValueDepthLimit: DefaultAttributeValueDepthLimit, + }, + }, { name: "opt-override", env: envLimits("-2"), @@ -91,7 +104,7 @@ func TestSettingSpanLimits(t *testing.T) { // negative values to signal this than this value is expected to // pass through. env: envLimits("-1"), - want: *limits(-1), + want: envWant(-1), }, { name: "opt(unlimited)", @@ -127,6 +140,30 @@ func TestSettingSpanLimits(t *testing.T) { } } +func TestAttributeValueDepthLimitOptionPrecedence(t *testing.T) { + limits := NewSpanLimits() + limits.AttributeValueDepthLimit = 7 + + assert.Equal(t, 3, NewTracerProvider( + WithRawSpanLimits(limits), + WithAttributeValueDepthLimit(3), + ).spanLimits.AttributeValueDepthLimit) + + assert.Equal(t, 7, NewTracerProvider( + WithAttributeValueDepthLimit(3), + WithRawSpanLimits(limits), + ).spanLimits.AttributeValueDepthLimit) + + assert.Equal(t, DefaultAttributeValueDepthLimit, NewTracerProvider( + WithAttributeValueDepthLimit(0), + ).spanLimits.AttributeValueDepthLimit) + + limits.AttributeValueDepthLimit = 0 + assert.Equal(t, DefaultAttributeValueDepthLimit, NewTracerProvider( + WithSpanLimits(limits), + ).spanLimits.AttributeValueDepthLimit) +} + type recorder []ReadOnlySpan func (*recorder) OnStart(context.Context, ReadWriteSpan) {} diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index 16e30640b76..c9761291065 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -166,6 +166,19 @@ func (testSampler) Description() string { return "testSampler" } +type attributeSampler []attribute.KeyValue + +func (s attributeSampler) ShouldSample(SamplingParameters) SamplingResult { + return SamplingResult{ + Decision: RecordAndSample, + Attributes: []attribute.KeyValue(s), + } +} + +func (attributeSampler) Description() string { + return "attributeSampler" +} + func TestSetName(t *testing.T) { tp := NewTracerProvider() @@ -1562,6 +1575,108 @@ func TestMapDeduplication(t *testing.T) { assert.Equal(t, attribute.NewSet(dedup), got.InstrumentationScope().Attributes) } +func depthLimitInputAttr(key string) attribute.KeyValue { + return attribute.Map( + key, + attribute.Map( + "level1", + attribute.Map("over", attribute.String("leaf", "value")), + ), + ) +} + +func depthLimitWantAttr(key string) attribute.KeyValue { + return attribute.Map( + key, + attribute.Map( + "level1", + attribute.KeyValue{Key: "over"}, + ), + ) +} + +func TestAttributeValueDepthLimit(t *testing.T) { + linkSC := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: trace.TraceID{1}, + SpanID: trace.SpanID{1}, + }) + + te := NewTestExporter() + limits := NewSpanLimits() + limits.AttributeValueDepthLimit = 2 + tp := NewTracerProvider( + WithSyncer(te), + WithSampler(attributeSampler{depthLimitInputAttr("sampler")}), + WithRawSpanLimits(limits), + WithResource(resource.NewSchemaless(depthLimitInputAttr("resource"))), + ) + + _, span := tp.Tracer( + "scope", + trace.WithInstrumentationAttributes(depthLimitInputAttr("scope")), + ).Start( + t.Context(), + "span0", + trace.WithAttributes(depthLimitInputAttr("start")), + ) + span.SetAttributes(depthLimitInputAttr("span")) + span.AddEvent("event", trace.WithAttributes(depthLimitInputAttr("event"))) + span.AddLink(trace.Link{ + SpanContext: linkSC, + Attributes: []attribute.KeyValue{depthLimitInputAttr("link")}, + }) + + got, err := endSpan(te, span) + require.NoError(t, err) + + assert.ElementsMatch(t, []attribute.KeyValue{ + depthLimitWantAttr("sampler"), + depthLimitWantAttr("start"), + depthLimitWantAttr("span"), + }, got.Attributes()) + require.Len(t, got.Events(), 1) + assert.Equal(t, []attribute.KeyValue{depthLimitWantAttr("event")}, got.Events()[0].Attributes) + require.Len(t, got.Links(), 1) + assert.Equal(t, []attribute.KeyValue{depthLimitWantAttr("link")}, got.Links()[0].Attributes) + assert.Equal(t, []attribute.KeyValue{depthLimitInputAttr("resource")}, got.Resource().Attributes()) + assert.Equal(t, attribute.NewSet(depthLimitWantAttr("scope")), got.InstrumentationScope().Attributes) +} + +func TestAttributeValueDepthLimitNegativeUnlimited(t *testing.T) { + te := NewTestExporter() + tp := NewTracerProvider( + WithSyncer(te), + WithAttributeValueDepthLimit(-1), + WithResource(resource.Empty()), + ) + + _, span := tp.Tracer("scope").Start(t.Context(), "span0") + span.SetAttributes(depthLimitInputAttr("span")) + + got, err := endSpan(te, span) + require.NoError(t, err) + assert.Equal(t, []attribute.KeyValue{depthLimitInputAttr("span")}, got.Attributes()) +} + +func TestAttributeValueDepthLimitZeroDefault(t *testing.T) { + te := NewTestExporter() + tp := NewTracerProvider( + WithSyncer(te), + WithAttributeValueDepthLimit(0), + WithResource(resource.Empty()), + ) + + _, span := tp.Tracer("scope").Start(t.Context(), "span0") + span.SetAttributes(depthLimitInputAttr("span"), attribute.String("scalar", "ok")) + + got, err := endSpan(te, span) + require.NoError(t, err) + assert.ElementsMatch(t, []attribute.KeyValue{ + depthLimitInputAttr("span"), + attribute.String("scalar", "ok"), + }, got.Attributes()) +} + func TestWithInstrumentationVersionAndSchema(t *testing.T) { te := NewTestExporter() tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty()))