You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-03 18:35:08 +02:00
d13f8ecb2d
Fixes https://github.com/open-telemetry/opentelemetry-go/issues/7934 ``` $ go test -run=^$ -bench=BenchmarkSlice goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/attribute cpu: 13th Gen Intel(R) Core(TM) i7-13800H BenchmarkSlice/Len3/Value-20 25297926 52.56 ns/op 144 B/op 1 allocs/op BenchmarkSlice/Len3/KeyValue-20 21315132 55.97 ns/op 144 B/op 1 allocs/op BenchmarkSlice/Len3/AsSlice-20 24214248 50.03 ns/op 144 B/op 1 allocs/op BenchmarkSlice/Len3/String-20 14148270 86.48 ns/op 48 B/op 1 allocs/op BenchmarkSlice/Len3/Emit-20 13605388 85.18 ns/op 48 B/op 1 allocs/op BenchmarkSlice/Len5Nested/Value-20 16086171 71.30 ns/op 240 B/op 1 allocs/op BenchmarkSlice/Len5Nested/KeyValue-20 15547844 75.81 ns/op 240 B/op 1 allocs/op BenchmarkSlice/Len5Nested/AsSlice-20 17806996 66.16 ns/op 240 B/op 1 allocs/op BenchmarkSlice/Len5Nested/String-20 7409064 165.2 ns/op 64 B/op 1 allocs/op BenchmarkSlice/Len5Nested/Emit-20 7666302 161.0 ns/op 64 B/op 1 allocs/op PASS ok go.opentelemetry.io/otel/attribute 12.980s ``` ``` $ go test -run=^$ -bench=BenchmarkHashKVs goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/attribute cpu: 13th Gen Intel(R) Core(TM) i7-13800H BenchmarkHashKVs-20 1268742 940.5 ns/op 0 B/op 0 allocs/op PASS ok go.opentelemetry.io/otel/attribute 1.198s ```
429 lines
12 KiB
Go
429 lines
12 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package attribute // import "go.opentelemetry.io/otel/attribute"
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"math"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"go.opentelemetry.io/otel/attribute/internal/xxhash"
|
|
)
|
|
|
|
// keyVals is all the KeyValue generators that are used for testing. This is
|
|
// not []KeyValue so different keys can be used with the test Values.
|
|
var keyVals = []func(string) KeyValue{
|
|
func(k string) KeyValue { return Bool(k, true) },
|
|
func(k string) KeyValue { return Bool(k, false) },
|
|
func(k string) KeyValue { return BoolSlice(k, []bool{false, true}) },
|
|
func(k string) KeyValue { return BoolSlice(k, []bool{true, true, false}) },
|
|
func(k string) KeyValue { return Int(k, -1278) },
|
|
func(k string) KeyValue { return Int(k, 0) }, // Should be different than false above.
|
|
func(k string) KeyValue { return IntSlice(k, []int{3, 23, 21, -8, 0}) },
|
|
func(k string) KeyValue { return IntSlice(k, []int{1}) },
|
|
func(k string) KeyValue { return Int64(k, 1) }, // Should be different from true and []int{1}.
|
|
func(k string) KeyValue { return Int64(k, 29369) },
|
|
func(k string) KeyValue { return Int64Slice(k, []int64{3826, -38, -29, -1}) },
|
|
func(k string) KeyValue { return Int64Slice(k, []int64{8, -328, 29, 0}) },
|
|
func(k string) KeyValue { return Float64(k, -0.3812381) },
|
|
func(k string) KeyValue { return Float64(k, 1e32) },
|
|
func(k string) KeyValue { return Float64Slice(k, []float64{0.1, -3.8, -29., 0.3321}) },
|
|
func(k string) KeyValue { return Float64Slice(k, []float64{-13e8, -32.8, 4., 1e28}) },
|
|
func(k string) KeyValue { return String(k, "foo") },
|
|
func(k string) KeyValue { return String(k, "bar") },
|
|
func(k string) KeyValue { return StringSlice(k, []string{"foo", "bar", "baz"}) },
|
|
func(k string) KeyValue { return StringSlice(k, []string{"[]i1"}) },
|
|
func(k string) KeyValue { return ByteSlice(k, []byte("foo")) },
|
|
func(k string) KeyValue { return ByteSlice(k, []byte("[]i1")) },
|
|
func(k string) KeyValue { return Slice(k) },
|
|
func(k string) KeyValue { return Slice(k, BoolValue(true)) },
|
|
func(k string) KeyValue { return Slice(k, BoolValue(true), IntValue(42)) },
|
|
func(k string) KeyValue {
|
|
return Slice(k,
|
|
StringValue("triad"),
|
|
IntValue(3),
|
|
BoolValue(false),
|
|
)
|
|
},
|
|
func(k string) KeyValue {
|
|
return Slice(k,
|
|
StringValue("quad"),
|
|
IntValue(4),
|
|
BoolValue(false),
|
|
Float64Value(4.25),
|
|
)
|
|
},
|
|
func(k string) KeyValue {
|
|
return Slice(k,
|
|
StringValue("penta"),
|
|
IntValue(5),
|
|
BoolValue(true),
|
|
Float64Value(5.5),
|
|
ByteSliceValue([]byte("five")),
|
|
)
|
|
},
|
|
func(k string) KeyValue {
|
|
return Slice(k,
|
|
StringValue("nested"),
|
|
SliceValue(Float64Value(math.Inf(1)), ByteSliceValue([]byte("bin"))),
|
|
BoolValue(true),
|
|
IntValue(6),
|
|
StringValue("tail"),
|
|
StringSliceValue([]string{"fallback"}),
|
|
)
|
|
},
|
|
func(k string) KeyValue { return KeyValue{Key: Key(k)} }, // Empty value.
|
|
}
|
|
|
|
func TestHashKVs(t *testing.T) {
|
|
type testcase struct {
|
|
num int
|
|
hash uint64
|
|
kvs []KeyValue
|
|
}
|
|
|
|
keys := []string{"k0", "k1"}
|
|
|
|
// Track hashes as we generate them so collision detection stays linear.
|
|
i := 0
|
|
seen := make(map[uint64]testcase)
|
|
assertUniqueHash := func(kvs []KeyValue) {
|
|
hash := hashKVs(kvs)
|
|
tc := testcase{num: i, hash: hash, kvs: kvs}
|
|
i++
|
|
|
|
if prev, ok := seen[hash]; ok {
|
|
t.Errorf("hashes equal: (%d: %d)%s == (%d: %d)%s",
|
|
prev.num, prev.hash, slice(prev.kvs), tc.num, tc.hash, slice(tc.kvs))
|
|
return
|
|
}
|
|
|
|
seen[hash] = tc
|
|
}
|
|
|
|
// Test empty slice.
|
|
assertUniqueHash(nil)
|
|
|
|
// Test all combinations of 1, 2, and 3 attributes with different keys and values.
|
|
for _, key := range keys {
|
|
for i := range keyVals {
|
|
kvs := []KeyValue{keyVals[i](key)}
|
|
assertUniqueHash(kvs)
|
|
|
|
for j := range keyVals {
|
|
kvs := []KeyValue{
|
|
keyVals[i](key),
|
|
keyVals[j](key),
|
|
}
|
|
assertUniqueHash(kvs)
|
|
|
|
for k := range keyVals {
|
|
kvs := []KeyValue{
|
|
keyVals[i](key),
|
|
keyVals[j](key),
|
|
keyVals[k](key),
|
|
}
|
|
assertUniqueHash(kvs)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func slice(kvs []KeyValue) string {
|
|
if len(kvs) == 0 {
|
|
return "[]"
|
|
}
|
|
|
|
var b strings.Builder
|
|
_, _ = b.WriteRune('[')
|
|
_, _ = b.WriteString(string(kvs[0].Key))
|
|
_, _ = b.WriteRune(':')
|
|
_, _ = b.WriteString(kvs[0].Value.String())
|
|
for _, kv := range kvs[1:] {
|
|
_, _ = b.WriteRune(',')
|
|
_, _ = b.WriteString(string(kv.Key))
|
|
_, _ = b.WriteRune(':')
|
|
_, _ = b.WriteString(kv.Value.String())
|
|
}
|
|
_, _ = b.WriteRune(']')
|
|
return b.String()
|
|
}
|
|
|
|
func BenchmarkHashKVs(b *testing.B) {
|
|
attrs := make([]KeyValue, len(keyVals))
|
|
for i := range keyVals {
|
|
attrs[i] = keyVals[i]("k")
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for b.Loop() {
|
|
hashKVs(attrs)
|
|
}
|
|
}
|
|
|
|
func BenchmarkHashValueSlice(b *testing.B) {
|
|
benches := []struct {
|
|
name string
|
|
v Value
|
|
}{
|
|
{
|
|
name: "Len2",
|
|
v: SliceValue(
|
|
BoolValue(true),
|
|
StringValue("two"),
|
|
),
|
|
},
|
|
{
|
|
name: "Len5",
|
|
v: SliceValue(
|
|
BoolValue(true),
|
|
IntValue(2),
|
|
StringValue("three"),
|
|
Float64Value(4.5),
|
|
ByteSliceValue([]byte("five")),
|
|
),
|
|
},
|
|
{
|
|
name: "Len8Nested",
|
|
v: SliceValue(
|
|
BoolValue(true),
|
|
IntValue(2),
|
|
StringValue("three"),
|
|
Float64Value(4.5),
|
|
ByteSliceValue([]byte("five")),
|
|
SliceValue(StringValue("nested"), Int64Value(6)),
|
|
BoolSliceValue([]bool{true, false, true}),
|
|
StringSliceValue([]string{"seven", "eight"}),
|
|
),
|
|
},
|
|
}
|
|
|
|
for _, bench := range benches {
|
|
b.Run(bench.name, func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for b.Loop() {
|
|
hashValue(xxhash.New(), bench.v).Sum64()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func FuzzHashKVs(f *testing.F) {
|
|
// Add seed inputs to ensure coverage of edge cases.
|
|
f.Add("", "", "", "", "", "", 0, int64(0), 0.0, false, uint8(0))
|
|
f.Add("key", "value", "🌍", "test", "bool", "float", -1, int64(-1), -1.0, true, uint8(1))
|
|
f.Add("duplicate", "duplicate", "duplicate", "duplicate", "duplicate", "NaN",
|
|
0, int64(0), math.Inf(1), false, uint8(2))
|
|
|
|
f.Fuzz(func(t *testing.T, k1, k2, k3, k4, k5, s string, i int, i64 int64, fVal float64, b bool, sliceType uint8) {
|
|
// Test variable number of attributes (0-11).
|
|
numAttrs := len(k1) % 12 // Use key length to determine number of attributes.
|
|
if numAttrs == 0 && k1 == "" {
|
|
// Test empty set.
|
|
h := hashKVs(nil)
|
|
if h == 0 {
|
|
t.Error("hash of empty slice should not be zero")
|
|
}
|
|
return
|
|
}
|
|
|
|
var kvs []KeyValue
|
|
|
|
// Add basic types.
|
|
if numAttrs > 0 {
|
|
kvs = append(kvs, String(k1, s))
|
|
}
|
|
if numAttrs > 1 {
|
|
kvs = append(kvs, Int(k2, i))
|
|
}
|
|
if numAttrs > 2 {
|
|
kvs = append(kvs, Int64(k3, i64))
|
|
}
|
|
if numAttrs > 3 {
|
|
kvs = append(kvs, Float64(k4, fVal))
|
|
}
|
|
if numAttrs > 4 {
|
|
kvs = append(kvs, Bool(k5, b))
|
|
}
|
|
|
|
// Add slice types based on sliceType parameter.
|
|
if numAttrs > 5 {
|
|
switch sliceType % 6 {
|
|
case 0:
|
|
// Test BoolSlice with variable length.
|
|
bools := make([]bool, len(s)%5) // 0-4 elements
|
|
for i := range bools {
|
|
bools[i] = (i+len(k1))%2 == 0
|
|
}
|
|
kvs = append(kvs, BoolSlice("boolslice", bools))
|
|
case 1:
|
|
// Test IntSlice with variable length.
|
|
ints := make([]int, len(s)%6) // 0-5 elements
|
|
for i := range ints {
|
|
ints[i] = i + len(k2)
|
|
}
|
|
kvs = append(kvs, IntSlice("intslice", ints))
|
|
case 2:
|
|
// Test Int64Slice with variable length.
|
|
int64s := make([]int64, len(s)%4) // 0-3 elements
|
|
for i := range int64s {
|
|
int64s[i] = int64(i) + i64
|
|
}
|
|
kvs = append(kvs, Int64Slice("int64slice", int64s))
|
|
case 3:
|
|
// Test Float64Slice with variable length and special values.
|
|
float64s := make([]float64, len(s)%5) // 0-4 elements
|
|
for i := range float64s {
|
|
switch i % 4 {
|
|
case 0:
|
|
float64s[i] = fVal
|
|
case 1:
|
|
float64s[i] = math.Inf(1) // +Inf
|
|
case 2:
|
|
float64s[i] = math.Inf(-1) // -Inf
|
|
case 3:
|
|
float64s[i] = math.NaN() // NaN
|
|
}
|
|
}
|
|
kvs = append(kvs, Float64Slice("float64slice", float64s))
|
|
case 4:
|
|
// Test ByteSlice with variable length.
|
|
bytes := make([]byte, len(s)%5)
|
|
for i := range bytes {
|
|
bytes[i] = byte(i + len(k1))
|
|
}
|
|
kvs = append(kvs, ByteSlice("bytes", bytes))
|
|
case 5:
|
|
values := make([]Value, len(s)%4) // 0-3 elements
|
|
for i := range values {
|
|
switch i % 4 {
|
|
case 0:
|
|
values[i] = BoolValue((i+len(k1))%2 == 0)
|
|
case 1:
|
|
values[i] = IntValue(i + len(k2))
|
|
case 2:
|
|
values[i] = StringValue(fmt.Sprintf("item_%d", i))
|
|
case 3:
|
|
values[i] = SliceValue(Float64Value(fVal), ByteSliceValue([]byte("bin")))
|
|
}
|
|
}
|
|
kvs = append(kvs, Slice("slice", values...))
|
|
}
|
|
}
|
|
|
|
// Add StringSlice.
|
|
if numAttrs > 6 {
|
|
strings := make([]string, len(k1)%4) // 0-3 elements
|
|
for i := range strings {
|
|
strings[i] = fmt.Sprintf("%s_%d", s, i)
|
|
}
|
|
kvs = append(kvs, StringSlice("stringslice", strings))
|
|
}
|
|
|
|
// Test duplicate keys (should be handled by Set construction).
|
|
if numAttrs > 7 && k1 != "" {
|
|
kvs = append(kvs, String(k1, "duplicate_key_value"))
|
|
}
|
|
|
|
// Add more attributes with Unicode keys.
|
|
if numAttrs > 8 {
|
|
kvs = append(kvs, String("🔑", "unicode_key"))
|
|
}
|
|
if numAttrs > 9 {
|
|
kvs = append(kvs, String("empty", ""))
|
|
}
|
|
|
|
// Add empty value.
|
|
if numAttrs > 10 {
|
|
kvs = append(kvs, KeyValue{Key: Key("empty_value")})
|
|
}
|
|
|
|
// Sort to ensure consistent ordering (as Set would do).
|
|
slices.SortFunc(kvs, func(a, b KeyValue) int {
|
|
return cmp.Compare(string(a.Key), string(b.Key))
|
|
})
|
|
|
|
// Remove duplicates (as Set will do).
|
|
if len(kvs) > 1 {
|
|
j := 0
|
|
for i := 1; i < len(kvs); i++ {
|
|
if kvs[j].Key != kvs[i].Key {
|
|
j++
|
|
kvs[j] = kvs[i]
|
|
} else {
|
|
// Keep the later value for duplicate keys.
|
|
kvs[j] = kvs[i]
|
|
}
|
|
}
|
|
kvs = kvs[:j+1]
|
|
}
|
|
|
|
// Hash the key-value pairs.
|
|
h1 := hashKVs(kvs)
|
|
h2 := hashKVs(kvs) // Should be deterministic
|
|
|
|
if h1 != h2 {
|
|
t.Errorf("hash is not deterministic: %d != %d for kvs=%v", h1, h2, kvs)
|
|
}
|
|
|
|
if h1 == 0 && len(kvs) > 0 {
|
|
t.Errorf("hash should not be zero for non-empty input: kvs=%v", kvs)
|
|
}
|
|
|
|
// Test that different inputs produce different hashes (most of the time).
|
|
// This is a probabilistic test - collisions are possible but rare.
|
|
if len(kvs) > 0 {
|
|
// Modify one value slightly.
|
|
modifiedKvs := make([]KeyValue, len(kvs))
|
|
copy(modifiedKvs, kvs)
|
|
if len(modifiedKvs) > 0 {
|
|
switch modifiedKvs[0].Value.Type() {
|
|
case STRING:
|
|
modifiedKvs[0] = String(string(modifiedKvs[0].Key), modifiedKvs[0].Value.AsString()+"_modified")
|
|
case INT64:
|
|
modifiedKvs[0] = Int64(string(modifiedKvs[0].Key), modifiedKvs[0].Value.AsInt64()+1)
|
|
case BOOL:
|
|
modifiedKvs[0] = Bool(string(modifiedKvs[0].Key), !modifiedKvs[0].Value.AsBool())
|
|
case FLOAT64:
|
|
val := modifiedKvs[0].Value.AsFloat64()
|
|
if !math.IsNaN(val) && !math.IsInf(val, 0) {
|
|
modifiedKvs[0] = Float64(string(modifiedKvs[0].Key), val+1.0)
|
|
}
|
|
case SLICE:
|
|
origSlice := modifiedKvs[0].Value.AsSlice()
|
|
if len(origSlice) > 0 {
|
|
newSlice := slices.Clone(origSlice)
|
|
switch newSlice[0].Type() {
|
|
case INT64:
|
|
newSlice[0] = Int64Value(newSlice[0].AsInt64() + 1)
|
|
case BOOL:
|
|
newSlice[0] = BoolValue(!newSlice[0].AsBool())
|
|
case STRING:
|
|
newSlice[0] = StringValue(newSlice[0].AsString() + "_mod")
|
|
default:
|
|
newSlice[0] = StringValue("modified")
|
|
}
|
|
modifiedKvs[0] = Slice(string(modifiedKvs[0].Key), newSlice...)
|
|
}
|
|
case EMPTY:
|
|
modifiedKvs[0] = String(string(modifiedKvs[0].Key), "not_empty")
|
|
}
|
|
|
|
h3 := hashKVs(modifiedKvs)
|
|
// Note: We don't assert h1 != h3 because hash collisions are theoretically possible
|
|
// but we can log suspicious cases for manual review.
|
|
if h1 == h3 && !reflect.DeepEqual(kvs, modifiedKvs) {
|
|
t.Logf("Potential hash collision detected: original=%v, modified=%v, hash=%d", kvs, modifiedKvs, h1)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|