Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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 @@ -14,6 +14,7 @@ The next release will require at least [Go 1.25].
### Added

- Support testing of [Go 1.26]. (#7902)
- Add `Bytes` and `BytesValue` functions for new `BYTES Type` in `go.opentelemetry.io/otel/attribute`. (#7948)

<!-- Released section -->
<!-- Don't change this section unless doing release -->
Expand Down
26 changes: 26 additions & 0 deletions attribute/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var (
outFloat64Slice []float64
outStr string
outStrSlice []string
outBytes []byte
)

func benchmarkEmit(kv attribute.KeyValue) func(*testing.B) {
Expand Down Expand Up @@ -272,6 +273,31 @@ func BenchmarkStringSlice(b *testing.B) {
b.Run("Emit", benchmarkEmit(kv))
}

func BenchmarkBytes(b *testing.B) {
k, v := "bytes", []byte("forty-two")
kv := attribute.Bytes(k, v)

b.Run("Value", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
outV = attribute.BytesValue(v)
}
})
b.Run("KeyValue", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
outKV = attribute.Bytes(k, v)
}
})
b.Run("AsBytes", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
outBytes = kv.Value.AsBytes()
}
})
b.Run("Emit", benchmarkEmit(kv))
}

func BenchmarkSetEquals(b *testing.B) {
b.Run("Empty", func(b *testing.B) {
benchmarkSetEquals(b, attribute.EmptySet())
Expand Down
10 changes: 10 additions & 0 deletions attribute/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
int64SliceID uint64 = 3762322556277578591 // "_[]int64" (little endian)
float64SliceID uint64 = 7308324551835016539 // "[]double" (little endian)
stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian)
bytesID uint64 = 6874028470941080415 // "_[]byte_" (little endian)
)

// hashKVs returns a new xxHash64 hash of kvs.
Expand Down Expand Up @@ -80,6 +81,15 @@ func hashKV(h xxhash.Hash, kv KeyValue) xxhash.Hash {
for i := 0; i < rv.Len(); i++ {
h = h.String(rv.Index(i).String())
}
case BYTES:
h = h.Uint64(bytesID)
if kv.Value.slice == nil {
break
}
rv := reflect.ValueOf(kv.Value.slice)
for i := 0; i < rv.Len(); i++ {
h = h.Uint64(rv.Index(i).Uint())
}
case INVALID:
default:
// Logging is an alternative, but using the internal logger here
Expand Down
11 changes: 10 additions & 1 deletion attribute/hash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ var keyVals = []func(string) KeyValue{
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 Bytes(k, []byte("foo")) },
func(k string) KeyValue { return Bytes(k, []byte("[]i1")) },
}

func TestHashKVsEquality(t *testing.T) {
Expand Down Expand Up @@ -187,7 +189,7 @@ func FuzzHashKVs(f *testing.F) {

// Add slice types based on sliceType parameter
if numAttrs > 5 {
switch sliceType % 4 {
switch sliceType % 5 {
case 0:
// Test BoolSlice with variable length.
bools := make([]bool, len(s)%5) // 0-4 elements
Expand Down Expand Up @@ -225,6 +227,13 @@ func FuzzHashKVs(f *testing.F) {
}
}
kvs = append(kvs, Float64Slice("float64slice", float64s))
case 4:
// Test Bytes with variable length.
bytes := make([]byte, len(s)%5)
for i := range bytes {
bytes[i] = byte(i + len(k1))
}
kvs = append(kvs, Bytes("bytes", bytes))
}
}

Expand Down
20 changes: 20 additions & 0 deletions attribute/internal/attribute.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ func StringSliceValue(v []string) any {
return cp.Interface()
}

// BytesValue converts a bytes slice into an array with same elements as slice.
func BytesValue(v []byte) any {
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[byte]())).Elem()
reflect.Copy(cp, reflect.ValueOf(v))
return cp.Interface()
}

// AsBoolSlice converts a bool array into a slice into with same elements as array.
func AsBoolSlice(v any) []bool {
rv := reflect.ValueOf(v)
Expand Down Expand Up @@ -90,3 +97,16 @@ func AsStringSlice(v any) []string {
}
return cpy
}

// AsBytes converts a bytes array into a slice into with same elements as array.
func AsBytes(v any) []byte {
rv := reflect.ValueOf(v)
if rv.Type().Kind() != reflect.Array {
return nil
}
cpy := make([]byte, rv.Len())
if len(cpy) > 0 {
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
}
return cpy
}
11 changes: 11 additions & 0 deletions attribute/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ func (k Key) StringSlice(v []string) KeyValue {
}
}

// Bytes creates a KeyValue instance with a BYTES Value.
//
// If creating both a key and value at the same time, use the provided
// convenience function instead -- Bytes(name, value).
func (k Key) Bytes(v []byte) KeyValue {
return KeyValue{
Key: k,
Value: BytesValue(v),
}
}

// Defined reports whether the key is not empty.
func (k Key) Defined() bool {
return len(k) != 0
Expand Down
5 changes: 5 additions & 0 deletions attribute/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ func TestEmit(t *testing.T) {
v: attribute.StringSliceValue([]string{"foo", "bar"}),
want: `["foo","bar"]`,
},
{
name: `test Key.Emit() can emit a string representing self.BYTES`,
v: attribute.BytesValue([]byte("foo")),
want: `Zm9v`,
},
} {
t.Run(testcase.name, func(t *testing.T) {
// proto: func (v attribute.Value) Emit() string {
Expand Down
5 changes: 5 additions & 0 deletions attribute/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ func StringSlice(k string, v []string) KeyValue {
return Key(k).StringSlice(v)
}

// Bytes creates a KeyValue with a BYTES Value type.
func Bytes(k string, v []byte) KeyValue {
return Key(k).Bytes(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 @@ -58,6 +58,14 @@ func TestKeyValueConstructors(t *testing.T) {
Value: attribute.IntValue(123),
},
},
{
name: "Bytes",
actual: attribute.Bytes("k1", []byte{123}),
expected: attribute.KeyValue{
Key: "k1",
Value: attribute.BytesValue([]byte{123}),
},
},
}

for _, test := range tt {
Expand Down Expand Up @@ -114,6 +122,11 @@ func TestKeyValueValid(t *testing.T) {
valid: true,
kv: attribute.String("string", ""),
},
{
desc: "non-empty key with BYTE type Value should be valid",
valid: true,
kv: attribute.Bytes("string", []byte{}),
},
}

for _, test := range tests {
Expand Down Expand Up @@ -152,6 +165,10 @@ func TestIncorrectCast(t *testing.T) {
name: "StringSlice",
val: attribute.BoolSliceValue([]bool{true}),
},
{
name: "Bytes",
val: attribute.BytesValue([]byte{123}),
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -165,6 +182,7 @@ func TestIncorrectCast(t *testing.T) {
tt.val.AsInterface()
tt.val.AsString()
tt.val.AsStringSlice()
tt.val.AsBytes()
})
})
}
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.

24 changes: 24 additions & 0 deletions attribute/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package attribute // import "go.opentelemetry.io/otel/attribute"

import (
"encoding/base64"
"encoding/json"
"fmt"
"reflect"
Expand Down Expand Up @@ -44,6 +45,8 @@ const (
FLOAT64SLICE
// STRINGSLICE is a slice of strings Type Value.
STRINGSLICE
// BYTES is a slice of bytes Type Value.
BYTES
)

// BoolValue creates a BOOL Value.
Expand Down Expand Up @@ -115,6 +118,14 @@ func StringSliceValue(v []string) Value {
return Value{vtype: STRINGSLICE, slice: attribute.StringSliceValue(v)}
}

// BytesValue creates a BYTES Value.
func BytesValue(v []byte) Value {
return Value{
vtype: BYTES,
slice: attribute.BytesValue(v),
}
}

// Type returns a type of the Value.
func (v Value) Type() Type {
return v.vtype
Expand Down Expand Up @@ -196,6 +207,15 @@ func (v Value) asStringSlice() []string {
return attribute.AsStringSlice(v.slice)
}

// AsBytes returns the bytes value. Make sure that the Value's type
// is BYTES.
func (v Value) AsBytes() []byte {
if v.vtype != BYTES {
return nil
}
return attribute.AsBytes(v.slice)
}

type unknownValueType struct{}

// AsInterface returns Value's data as any.
Expand All @@ -217,6 +237,8 @@ func (v Value) AsInterface() any {
return v.stringly
case STRINGSLICE:
return v.asStringSlice()
case BYTES:
return v.AsBytes()
}
return unknownValueType{}
}
Expand Down Expand Up @@ -252,6 +274,8 @@ func (v Value) Emit() string {
return string(j)
case STRING:
return v.stringly
case BYTES:
return base64.StdEncoding.EncodeToString(v.AsBytes())
default:
return "unknown"
}
Expand Down
15 changes: 15 additions & 0 deletions attribute/value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ func TestValue(t *testing.T) {
wantType: attribute.STRINGSLICE,
wantValue: []string{"forty-two", "negative three", "twelve"},
},
{
name: "Key.Bytes() correctly returns keys's internal []byte value",
value: k.Bytes([]byte("hello world")).Value,
wantType: attribute.BYTES,
wantValue: []byte("hello world"),
},
} {
t.Logf("Running test case %s", testcase.name)
if testcase.value.Type() != testcase.wantType {
Expand Down Expand Up @@ -143,6 +149,10 @@ func TestEquivalence(t *testing.T) {
attribute.StringSlice("StringSlice", []string{"one", "two", "three"}),
attribute.StringSlice("StringSlice", []string{"one", "two", "three"}),
},
{
attribute.Bytes("Bytes", []byte("one")),
attribute.Bytes("Bytes", []byte("one")),
},
}

t.Run("Distinct", func(t *testing.T) {
Expand Down Expand Up @@ -205,4 +215,9 @@ func TestAsSlice(t *testing.T) {
kv = attribute.StringSlice("StringSlice", ss1)
ss2 := kv.Value.AsStringSlice()
assert.Equal(t, ss1, ss2)

b1 := []byte("one")
kv = attribute.Bytes("Bytes", b1)
b2 := kv.Value.AsBytes()
assert.Equal(t, b1, b2)
}
3 changes: 3 additions & 0 deletions log/keyvalue.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@ func ValueFromAttribute(value attribute.Value) Value {
res = append(res, StringValue(v))
}
return SliceValue(res...)
case attribute.BYTES:
val := value.AsBytes()
return BytesValue(val)
}
// This code should never be reached
// as log attributes are a superset of standard attributes.
Expand Down