Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
85060b2
attribute: add SLICE type support
pellared Apr 9, 2026
67829fb
add changelog entry
pellared Apr 9, 2026
3eebedd
Merge branch 'main' into add-slice
pellared Apr 9, 2026
4988313
Update auto.go
pellared Apr 10, 2026
ace4df3
Merge branch 'main' into add-slice
pellared Apr 10, 2026
e1452f2
test: enhance SliceValue tests with various scenarios
pellared Apr 10, 2026
3becce5
reduce SliceValue.String allocations
pellared Apr 10, 2026
6bf6db4
fmt
pellared Apr 10, 2026
80e24fd
test: enhance SliceValue tests with additional scenarios and edge cases
pellared Apr 10, 2026
3c4f3f6
test: expand SliceValue tests with additional nested slice scenarios
pellared Apr 10, 2026
6190393
comment appendBase64
pellared Apr 10, 2026
e073aea
Merge branch 'main' into add-slice
pellared Apr 10, 2026
22f4b51
simplify equalKeyValue function
pellared Apr 10, 2026
8fa7953
Merge branch 'main' into add-slice
pellared Apr 13, 2026
fd9c40a
change Slice and SliceValue functions to accept variadic arguments
pellared Apr 13, 2026
e1f21e0
improve performance of hashValue for slice values
pellared Apr 14, 2026
4db42b5
add test cases to cover hashValue
pellared Apr 14, 2026
061ecd8
reorder asValueSliceReflect
pellared Apr 14, 2026
4b5d8ee
Merge branch 'main' into add-slice
pellared Apr 14, 2026
b8d196c
Merge branch 'main' into add-slice
pellared Apr 15, 2026
56b58d4
inline formatArrayFloat64
pellared Apr 15, 2026
2c9ad70
Merge branch 'main' into add-slice
pellared Apr 15, 2026
d6906e5
Merge branch 'main' into add-slice
pellared Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric`. (#8153)
- Support `BYTESLICE` attributes in `go.opentelemetry.io/otel/exporters/zipkin`. (#8153)
- Add `String` method for `Value` type in `go.opentelemetry.io/otel/attribute`. (#8142)
- Add `Slice` and `SliceValue` functions for new `SLICE` attribute type in `go.opentelemetry.io/otel/attribute`. (#8166)
- Add `Error` field on `Record` type in `go.opentelemetry.io/otel/log/logtest`. (#8148)
- Add experimental support for splitting metric data across multiple batches in `go.opentelemetry.io/otel/sdk/metric`.
Set `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=<max_size>` to enable for all periodic readers.
Expand Down
54 changes: 54 additions & 0 deletions attribute/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package attribute_test

import (
"math"
"testing"

"go.opentelemetry.io/otel/attribute"
Expand All @@ -23,6 +24,7 @@ var (
outFloat64Slice []float64
outStr string
outStrSlice []string
outValueSlice []attribute.Value
)

func benchmarkEmit(kv attribute.KeyValue) func(*testing.B) {
Expand Down Expand Up @@ -341,6 +343,58 @@ func BenchmarkStringSlice(b *testing.B) {
}
}

func BenchmarkSlice(b *testing.B) {
for _, bench := range []struct {
name string
v []attribute.Value
}{
{
name: "Len3",
v: []attribute.Value{
attribute.BoolValue(true),
attribute.IntValue(42),
attribute.StringValue("test"),
},
},
{
name: "Len5Nested",
v: []attribute.Value{
attribute.StringValue("quote\""),
attribute.Float64Value(math.Inf(1)),
attribute.ByteSliceValue([]byte("bin")),
attribute.SliceValue(attribute.StringValue("nested"), attribute.Value{}),
attribute.BoolValue(false),
},
},
} {
b.Run(bench.name, func(b *testing.B) {
k, v := "slice", bench.v
kv := attribute.Slice(k, v...)

b.Run("Value", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
outV = attribute.SliceValue(v...)
}
})
b.Run("KeyValue", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
outKV = attribute.Slice(k, v...)
}
})
b.Run("AsSlice", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
outValueSlice = kv.Value.AsSlice()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
})
}
}

func BenchmarkByteSlice(b *testing.B) {
k, v := "bytes", []byte("forty-two")
kv := attribute.ByteSlice(k, v)
Expand Down
34 changes: 22 additions & 12 deletions attribute/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
float64SliceID uint64 = 7308324551835016539 // "[]double" (little endian)
stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian)
byteSliceID uint64 = 6874028470941080415 // "_[]byte_" (little endian)
sliceID uint64 = 7883494272577650031 // "__slice_" (little endian)
emptyID uint64 = 7305809155345288421 // "__empty_" (little endian)
)

Expand All @@ -43,55 +44,64 @@ func hashKVs(kvs []KeyValue) uint64 {
// hashKV returns the xxHash64 hash of kv with h as the base.
func hashKV(h xxhash.Hash, kv KeyValue) xxhash.Hash {
h = h.String(string(kv.Key))
return hashValue(h, kv.Value)
}

switch kv.Value.Type() {
func hashValue(h xxhash.Hash, v Value) xxhash.Hash {
switch v.Type() {
case BOOL:
h = h.Uint64(boolID)
h = h.Uint64(kv.Value.numeric)
h = h.Uint64(v.numeric)
case INT64:
h = h.Uint64(int64ID)
h = h.Uint64(kv.Value.numeric)
h = h.Uint64(v.numeric)
case FLOAT64:
h = h.Uint64(float64ID)
// Assumes numeric stored with math.Float64bits.
h = h.Uint64(kv.Value.numeric)
h = h.Uint64(v.numeric)
case STRING:
h = h.Uint64(stringID)
h = h.String(kv.Value.stringly)
h = h.String(v.stringly)
case BOOLSLICE:
h = h.Uint64(boolSliceID)
rv := reflect.ValueOf(kv.Value.slice)
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = h.Bool(rv.Index(i).Bool())
}
case INT64SLICE:
h = h.Uint64(int64SliceID)
rv := reflect.ValueOf(kv.Value.slice)
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = h.Int64(rv.Index(i).Int())
}
case FLOAT64SLICE:
h = h.Uint64(float64SliceID)
rv := reflect.ValueOf(kv.Value.slice)
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = h.Float64(rv.Index(i).Float())
}
case STRINGSLICE:
h = h.Uint64(stringSliceID)
rv := reflect.ValueOf(kv.Value.slice)
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = h.String(rv.Index(i).String())
}
case BYTESLICE:
h = h.Uint64(byteSliceID)
h = h.String(kv.Value.stringly)
h = h.String(v.stringly)
case SLICE:
Comment thread
dashpole marked this conversation as resolved.
h = h.Uint64(sliceID)
rv := reflect.ValueOf(v.slice)
for i := 0; i < rv.Len(); i++ {
h = hashValue(h, rv.Index(i).Interface().(Value))
}
Comment thread
pellared marked this conversation as resolved.
case EMPTY:
h = h.Uint64(emptyID)
default:
// Logging is an alternative, but using the internal logger here
// causes an import cycle so it is not done.
v := kv.Value.AsInterface()
msg := fmt.Sprintf("unknown value type: %[1]v (%[1]T)", v)
val := v.AsInterface()
msg := fmt.Sprintf("unknown value type: %[1]v (%[1]T)", val)
panic(msg)
}
return h
Expand Down
42 changes: 40 additions & 2 deletions attribute/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ var keyVals = []func(string) KeyValue{
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, BoolValue(true), IntValue(42)) },
func(k string) KeyValue {
return Slice(k,
Comment thread
MrAlias marked this conversation as resolved.
StringValue("nested"),
SliceValue(Float64Value(math.Inf(1)), ByteSliceValue([]byte("bin"))),
)
},
func(k string) KeyValue { return KeyValue{Key: Key(k)} }, // Empty value.
}

Expand Down Expand Up @@ -188,9 +195,9 @@ func FuzzHashKVs(f *testing.F) {
kvs = append(kvs, Bool(k5, b))
}

// Add slice types based on sliceType parameter
// Add slice types based on sliceType parameter.
if numAttrs > 5 {
switch sliceType % 5 {
switch sliceType % 6 {
case 0:
// Test BoolSlice with variable length.
bools := make([]bool, len(s)%5) // 0-4 elements
Expand Down Expand Up @@ -235,6 +242,21 @@ func FuzzHashKVs(f *testing.F) {
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...))
}
}

Expand Down Expand Up @@ -316,6 +338,22 @@ func FuzzHashKVs(f *testing.F) {
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")
}
Expand Down
11 changes: 11 additions & 0 deletions attribute/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ func (k Key) ByteSlice(v []byte) KeyValue {
}
}

// Slice creates a KeyValue instance with a SLICE Value.
//
// If creating both a key and value at the same time, use the provided
// convenience function instead -- Slice(name, values...).
func (k Key) Slice(v ...Value) KeyValue {
return KeyValue{
Key: k,
Value: SliceValue(v...),
}
}

// Defined reports whether the key is not empty.
func (k Key) Defined() bool {
return len(k) != 0
Expand Down
24 changes: 24 additions & 0 deletions attribute/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package attribute_test

import (
"encoding/json"
"math"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -113,6 +114,16 @@ func TestEmit(t *testing.T) {
v: attribute.ByteSliceValue([]byte("foo")),
want: "Zm9v",
},
{
name: `test Key.Emit() can emit a string representing self.SLICE`,
v: attribute.SliceValue(
attribute.BoolValue(true),
attribute.StringValue("foo\"bar"),
attribute.Float64Value(math.Inf(1)),
attribute.ByteSliceValue([]byte("bin")),
),
want: `[true,"foo\"bar","Infinity","Ymlu"]`,
},
{
name: `test Key.Emit() can emit a string representing self.EMPTY`,
v: attribute.Value{},
Expand All @@ -128,3 +139,16 @@ func TestEmit(t *testing.T) {
})
}
}

func TestString(t *testing.T) {
v := attribute.SliceValue(
attribute.StringValue("foo\nbar"),
attribute.Float64Value(math.NaN()),
attribute.SliceValue(
attribute.ByteSliceValue([]byte("bin")),
attribute.Value{},
),
)

require.Equal(t, `["foo\nbar","NaN",["Ymlu",null]]`, v.String())
}
5 changes: 5 additions & 0 deletions attribute/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ func ByteSlice(k string, v []byte) KeyValue {
return Key(k).ByteSlice(v)
}

// Slice creates a KeyValue with a SLICE Value type.
func Slice(k string, v ...Value) KeyValue {
return Key(k).Slice(v...)
}

// Stringer creates a new key-value pair with a passed name and a string
// value generated by the passed Stringer interface.
func Stringer(k string, v fmt.Stringer) KeyValue {
Expand Down
18 changes: 18 additions & 0 deletions attribute/kv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ func TestKeyValueConstructors(t *testing.T) {
Value: attribute.ByteSliceValue([]byte{123}),
},
},
{
name: "Slice",
actual: attribute.Slice("k1", attribute.BoolValue(true), attribute.IntValue(42)),
expected: attribute.KeyValue{
Key: "k1",
Value: attribute.SliceValue(attribute.BoolValue(true), attribute.IntValue(42)),
},
},
}

for _, test := range tt {
Expand Down Expand Up @@ -127,6 +135,11 @@ func TestKeyValueValid(t *testing.T) {
valid: true,
kv: attribute.ByteSlice("bytes", []byte{}),
},
{
desc: "non-empty key with SLICE type Value should be valid",
valid: true,
kv: attribute.Slice("slice", attribute.StringValue("value")),
},
}

for _, test := range tests {
Expand Down Expand Up @@ -173,6 +186,10 @@ func TestIncorrectCast(t *testing.T) {
name: "Empty",
val: attribute.Value{},
},
{
name: "Slice",
val: attribute.SliceValue(attribute.StringValue("value")),
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -184,6 +201,7 @@ func TestIncorrectCast(t *testing.T) {
tt.val.AsInt64()
tt.val.AsInt64Slice()
tt.val.AsInterface()
tt.val.AsSlice()
tt.val.AsString()
tt.val.AsStringSlice()
tt.val.AsByteSlice()
Expand Down
5 changes: 3 additions & 2 deletions attribute/type_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading