Skip to content

Commit d13f8ec

Browse files
authored
attribute: add SLICE type support (#8166)
Fixes #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 ```
1 parent bb6804e commit d13f8ec

16 files changed

Lines changed: 937 additions & 102 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1717
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric`. (#8153)
1818
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/zipkin`. (#8153)
1919
- Add `String` method for `Value` type in `go.opentelemetry.io/otel/attribute`. (#8142)
20+
- Add `Slice` and `SliceValue` functions for new `SLICE` attribute type in `go.opentelemetry.io/otel/attribute`. (#8166)
2021
- Add `Error` field on `Record` type in `go.opentelemetry.io/otel/log/logtest`. (#8148)
2122
- Add experimental support for splitting metric data across multiple batches in `go.opentelemetry.io/otel/sdk/metric`.
2223
Set `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=<max_size>` to enable for all periodic readers.

attribute/benchmark_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package attribute_test
55

66
import (
7+
"math"
78
"testing"
89

910
"go.opentelemetry.io/otel/attribute"
@@ -23,6 +24,7 @@ var (
2324
outFloat64Slice []float64
2425
outStr string
2526
outStrSlice []string
27+
outValueSlice []attribute.Value
2628
)
2729

2830
func benchmarkEmit(kv attribute.KeyValue) func(*testing.B) {
@@ -341,6 +343,58 @@ func BenchmarkStringSlice(b *testing.B) {
341343
}
342344
}
343345

346+
func BenchmarkSlice(b *testing.B) {
347+
for _, bench := range []struct {
348+
name string
349+
v []attribute.Value
350+
}{
351+
{
352+
name: "Len3",
353+
v: []attribute.Value{
354+
attribute.BoolValue(true),
355+
attribute.IntValue(42),
356+
attribute.StringValue("test"),
357+
},
358+
},
359+
{
360+
name: "Len5Nested",
361+
v: []attribute.Value{
362+
attribute.StringValue("quote\""),
363+
attribute.Float64Value(math.Inf(1)),
364+
attribute.ByteSliceValue([]byte("bin")),
365+
attribute.SliceValue(attribute.StringValue("nested"), attribute.Value{}),
366+
attribute.BoolValue(false),
367+
},
368+
},
369+
} {
370+
b.Run(bench.name, func(b *testing.B) {
371+
k, v := "slice", bench.v
372+
kv := attribute.Slice(k, v...)
373+
374+
b.Run("Value", func(b *testing.B) {
375+
b.ReportAllocs()
376+
for i := 0; i < b.N; i++ {
377+
outV = attribute.SliceValue(v...)
378+
}
379+
})
380+
b.Run("KeyValue", func(b *testing.B) {
381+
b.ReportAllocs()
382+
for i := 0; i < b.N; i++ {
383+
outKV = attribute.Slice(k, v...)
384+
}
385+
})
386+
b.Run("AsSlice", func(b *testing.B) {
387+
b.ReportAllocs()
388+
for i := 0; i < b.N; i++ {
389+
outValueSlice = kv.Value.AsSlice()
390+
}
391+
})
392+
b.Run("String", benchmarkString(kv))
393+
b.Run("Emit", benchmarkEmit(kv))
394+
})
395+
}
396+
}
397+
344398
func BenchmarkByteSlice(b *testing.B) {
345399
k, v := "bytes", []byte("forty-two")
346400
kv := attribute.ByteSlice(k, v)

attribute/hash.go

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const (
2828
float64SliceID uint64 = 7308324551835016539 // "[]double" (little endian)
2929
stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian)
3030
byteSliceID uint64 = 6874028470941080415 // "_[]byte_" (little endian)
31+
sliceID uint64 = 7883494272577650031 // "__slice_" (little endian)
3132
emptyID uint64 = 7305809155345288421 // "__empty_" (little endian)
3233
)
3334

@@ -43,56 +44,87 @@ func hashKVs(kvs []KeyValue) uint64 {
4344
// hashKV returns the xxHash64 hash of kv with h as the base.
4445
func hashKV(h xxhash.Hash, kv KeyValue) xxhash.Hash {
4546
h = h.String(string(kv.Key))
47+
return hashValue(h, kv.Value)
48+
}
4649

47-
switch kv.Value.Type() {
50+
func hashValue(h xxhash.Hash, v Value) xxhash.Hash {
51+
switch v.Type() {
4852
case BOOL:
4953
h = h.Uint64(boolID)
50-
h = h.Uint64(kv.Value.numeric)
54+
h = h.Uint64(v.numeric)
5155
case INT64:
5256
h = h.Uint64(int64ID)
53-
h = h.Uint64(kv.Value.numeric)
57+
h = h.Uint64(v.numeric)
5458
case FLOAT64:
5559
h = h.Uint64(float64ID)
5660
// Assumes numeric stored with math.Float64bits.
57-
h = h.Uint64(kv.Value.numeric)
61+
h = h.Uint64(v.numeric)
5862
case STRING:
5963
h = h.Uint64(stringID)
60-
h = h.String(kv.Value.stringly)
64+
h = h.String(v.stringly)
6165
case BOOLSLICE:
6266
h = h.Uint64(boolSliceID)
63-
rv := reflect.ValueOf(kv.Value.slice)
67+
rv := reflect.ValueOf(v.slice)
6468
for i := 0; i < rv.Len(); i++ {
6569
h = h.Bool(rv.Index(i).Bool())
6670
}
6771
case INT64SLICE:
6872
h = h.Uint64(int64SliceID)
69-
rv := reflect.ValueOf(kv.Value.slice)
73+
rv := reflect.ValueOf(v.slice)
7074
for i := 0; i < rv.Len(); i++ {
7175
h = h.Int64(rv.Index(i).Int())
7276
}
7377
case FLOAT64SLICE:
7478
h = h.Uint64(float64SliceID)
75-
rv := reflect.ValueOf(kv.Value.slice)
79+
rv := reflect.ValueOf(v.slice)
7680
for i := 0; i < rv.Len(); i++ {
7781
h = h.Float64(rv.Index(i).Float())
7882
}
7983
case STRINGSLICE:
8084
h = h.Uint64(stringSliceID)
81-
rv := reflect.ValueOf(kv.Value.slice)
85+
rv := reflect.ValueOf(v.slice)
8286
for i := 0; i < rv.Len(); i++ {
8387
h = h.String(rv.Index(i).String())
8488
}
8589
case BYTESLICE:
8690
h = h.Uint64(byteSliceID)
87-
h = h.String(kv.Value.stringly)
91+
h = h.String(v.stringly)
92+
case SLICE:
93+
h = h.Uint64(sliceID)
94+
switch vals := v.slice.(type) {
95+
case [0]Value:
96+
// No values to hash, but the type identifier is still hashed above.
97+
case [1]Value:
98+
h = hashValueSlice(h, vals[:])
99+
case [2]Value:
100+
h = hashValueSlice(h, vals[:])
101+
case [3]Value:
102+
h = hashValueSlice(h, vals[:])
103+
case [4]Value:
104+
h = hashValueSlice(h, vals[:])
105+
case [5]Value:
106+
h = hashValueSlice(h, vals[:])
107+
default:
108+
rv := reflect.ValueOf(v.slice)
109+
for i := 0; i < rv.Len(); i++ {
110+
h = hashValue(h, rv.Index(i).Interface().(Value))
111+
}
112+
}
88113
case EMPTY:
89114
h = h.Uint64(emptyID)
90115
default:
91116
// Logging is an alternative, but using the internal logger here
92117
// causes an import cycle so it is not done.
93-
v := kv.Value.AsInterface()
94-
msg := fmt.Sprintf("unknown value type: %[1]v (%[1]T)", v)
118+
val := v.AsInterface()
119+
msg := fmt.Sprintf("unknown value type: %[1]v (%[1]T)", val)
95120
panic(msg)
96121
}
97122
return h
98123
}
124+
125+
func hashValueSlice(h xxhash.Hash, vals []Value) xxhash.Hash {
126+
for _, v := range vals {
127+
h = hashValue(h, v)
128+
}
129+
return h
130+
}

attribute/hash_test.go

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"slices"
1212
"strings"
1313
"testing"
14+
15+
"go.opentelemetry.io/otel/attribute/internal/xxhash"
1416
)
1517

1618
// keyVals is all the KeyValue generators that are used for testing. This is
@@ -38,6 +40,43 @@ var keyVals = []func(string) KeyValue{
3840
func(k string) KeyValue { return StringSlice(k, []string{"[]i1"}) },
3941
func(k string) KeyValue { return ByteSlice(k, []byte("foo")) },
4042
func(k string) KeyValue { return ByteSlice(k, []byte("[]i1")) },
43+
func(k string) KeyValue { return Slice(k) },
44+
func(k string) KeyValue { return Slice(k, BoolValue(true)) },
45+
func(k string) KeyValue { return Slice(k, BoolValue(true), IntValue(42)) },
46+
func(k string) KeyValue {
47+
return Slice(k,
48+
StringValue("triad"),
49+
IntValue(3),
50+
BoolValue(false),
51+
)
52+
},
53+
func(k string) KeyValue {
54+
return Slice(k,
55+
StringValue("quad"),
56+
IntValue(4),
57+
BoolValue(false),
58+
Float64Value(4.25),
59+
)
60+
},
61+
func(k string) KeyValue {
62+
return Slice(k,
63+
StringValue("penta"),
64+
IntValue(5),
65+
BoolValue(true),
66+
Float64Value(5.5),
67+
ByteSliceValue([]byte("five")),
68+
)
69+
},
70+
func(k string) KeyValue {
71+
return Slice(k,
72+
StringValue("nested"),
73+
SliceValue(Float64Value(math.Inf(1)), ByteSliceValue([]byte("bin"))),
74+
BoolValue(true),
75+
IntValue(6),
76+
StringValue("tail"),
77+
StringSliceValue([]string{"fallback"}),
78+
)
79+
},
4180
func(k string) KeyValue { return KeyValue{Key: Key(k)} }, // Empty value.
4281
}
4382

@@ -129,6 +168,53 @@ func BenchmarkHashKVs(b *testing.B) {
129168
}
130169
}
131170

171+
func BenchmarkHashValueSlice(b *testing.B) {
172+
benches := []struct {
173+
name string
174+
v Value
175+
}{
176+
{
177+
name: "Len2",
178+
v: SliceValue(
179+
BoolValue(true),
180+
StringValue("two"),
181+
),
182+
},
183+
{
184+
name: "Len5",
185+
v: SliceValue(
186+
BoolValue(true),
187+
IntValue(2),
188+
StringValue("three"),
189+
Float64Value(4.5),
190+
ByteSliceValue([]byte("five")),
191+
),
192+
},
193+
{
194+
name: "Len8Nested",
195+
v: SliceValue(
196+
BoolValue(true),
197+
IntValue(2),
198+
StringValue("three"),
199+
Float64Value(4.5),
200+
ByteSliceValue([]byte("five")),
201+
SliceValue(StringValue("nested"), Int64Value(6)),
202+
BoolSliceValue([]bool{true, false, true}),
203+
StringSliceValue([]string{"seven", "eight"}),
204+
),
205+
},
206+
}
207+
208+
for _, bench := range benches {
209+
b.Run(bench.name, func(b *testing.B) {
210+
b.ReportAllocs()
211+
for b.Loop() {
212+
hashValue(xxhash.New(), bench.v).Sum64()
213+
}
214+
})
215+
}
216+
}
217+
132218
func FuzzHashKVs(f *testing.F) {
133219
// Add seed inputs to ensure coverage of edge cases.
134220
f.Add("", "", "", "", "", "", 0, int64(0), 0.0, false, uint8(0))
@@ -167,9 +253,9 @@ func FuzzHashKVs(f *testing.F) {
167253
kvs = append(kvs, Bool(k5, b))
168254
}
169255

170-
// Add slice types based on sliceType parameter
256+
// Add slice types based on sliceType parameter.
171257
if numAttrs > 5 {
172-
switch sliceType % 5 {
258+
switch sliceType % 6 {
173259
case 0:
174260
// Test BoolSlice with variable length.
175261
bools := make([]bool, len(s)%5) // 0-4 elements
@@ -214,6 +300,21 @@ func FuzzHashKVs(f *testing.F) {
214300
bytes[i] = byte(i + len(k1))
215301
}
216302
kvs = append(kvs, ByteSlice("bytes", bytes))
303+
case 5:
304+
values := make([]Value, len(s)%4) // 0-3 elements
305+
for i := range values {
306+
switch i % 4 {
307+
case 0:
308+
values[i] = BoolValue((i+len(k1))%2 == 0)
309+
case 1:
310+
values[i] = IntValue(i + len(k2))
311+
case 2:
312+
values[i] = StringValue(fmt.Sprintf("item_%d", i))
313+
case 3:
314+
values[i] = SliceValue(Float64Value(fVal), ByteSliceValue([]byte("bin")))
315+
}
316+
}
317+
kvs = append(kvs, Slice("slice", values...))
217318
}
218319
}
219320

@@ -295,6 +396,22 @@ func FuzzHashKVs(f *testing.F) {
295396
if !math.IsNaN(val) && !math.IsInf(val, 0) {
296397
modifiedKvs[0] = Float64(string(modifiedKvs[0].Key), val+1.0)
297398
}
399+
case SLICE:
400+
origSlice := modifiedKvs[0].Value.AsSlice()
401+
if len(origSlice) > 0 {
402+
newSlice := slices.Clone(origSlice)
403+
switch newSlice[0].Type() {
404+
case INT64:
405+
newSlice[0] = Int64Value(newSlice[0].AsInt64() + 1)
406+
case BOOL:
407+
newSlice[0] = BoolValue(!newSlice[0].AsBool())
408+
case STRING:
409+
newSlice[0] = StringValue(newSlice[0].AsString() + "_mod")
410+
default:
411+
newSlice[0] = StringValue("modified")
412+
}
413+
modifiedKvs[0] = Slice(string(modifiedKvs[0].Key), newSlice...)
414+
}
298415
case EMPTY:
299416
modifiedKvs[0] = String(string(modifiedKvs[0].Key), "not_empty")
300417
}

attribute/key.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,17 @@ func (k Key) ByteSlice(v []byte) KeyValue {
128128
}
129129
}
130130

131+
// Slice creates a KeyValue instance with a SLICE Value.
132+
//
133+
// If creating both a key and value at the same time, use the provided
134+
// convenience function instead -- Slice(name, values...).
135+
func (k Key) Slice(v ...Value) KeyValue {
136+
return KeyValue{
137+
Key: k,
138+
Value: SliceValue(v...),
139+
}
140+
}
141+
131142
// Defined reports whether the key is not empty.
132143
func (k Key) Defined() bool {
133144
return len(k) != 0

0 commit comments

Comments
 (0)