1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-06-03 18:35:08 +02:00

attribute: add BYTESLICE type support (#7948)

Fixes #7933 

Add BYTES type to https://pkg.go.dev/go.opentelemetry.io/otel/attribute

- Introduces BYTES type and byte 
- Adds Bytes / BytesValue constructors
- Implements hashing support
- Adds base64 representation in Emit()
- Adds test coverage for constructors, hashing, and set equality

```
$ go test -run=^$ -bench=BenchmarkByteSlice
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/attribute
cpu: 13th Gen Intel(R) Core(TM) i7-13800H
BenchmarkByteSlice/Value-20             149529567                7.993 ns/op           0 B/op          0 allocs/op
BenchmarkByteSlice/KeyValue-20          136973736                8.768 ns/op           0 B/op          0 allocs/op
BenchmarkByteSlice/AsByteSlice-20       562915658                2.120 ns/op           0 B/op          0 allocs/op
BenchmarkByteSlice/Emit-20              29149410                40.26 ns/op           16 B/op          1 allocs/op
PASS
```

---------

Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
Co-authored-by: Robert Pająk <pellared@hotmail.com>
This commit is contained in:
Nesterov Yehor
2026-04-07 09:39:32 +02:00
committed by GitHub
parent a00b377f31
commit 5e9a80b3ce
12 changed files with 138 additions and 3 deletions
+4
View File
@@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]
### Added
- Add `ByteSlice` and `ByteSliceValue` functions for new `BYTESLICE` attribute type in `go.opentelemetry.io/otel/attribute`. (#7948)
<!-- Released section -->
<!-- Don't change this section unless doing release -->
+28
View File
@@ -322,6 +322,34 @@ func BenchmarkStringSlice(b *testing.B) {
}
}
func BenchmarkByteSlice(b *testing.B) {
k, v := "bytes", []byte("forty-two")
kv := attribute.ByteSlice(k, v)
b.Run("Value", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
attribute.ByteSliceValue(v)
}
})
b.Run("KeyValue", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
attribute.ByteSlice(k, v)
}
})
b.Run("AsByteSlice", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
kv.Value.AsByteSlice()
}
})
b.Run("Emit", benchmarkEmit(kv))
}
func BenchmarkSetEquals(b *testing.B) {
b.Run("Empty", func(b *testing.B) {
benchmarkSetEquals(b, attribute.EmptySet())
+4
View File
@@ -27,6 +27,7 @@ const (
int64SliceID uint64 = 3762322556277578591 // "_[]int64" (little endian)
float64SliceID uint64 = 7308324551835016539 // "[]double" (little endian)
stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian)
byteSliceID uint64 = 6874028470941080415 // "_[]byte_" (little endian)
emptyID uint64 = 7305809155345288421 // "__empty_" (little endian)
)
@@ -81,6 +82,9 @@ func hashKV(h xxhash.Hash, kv KeyValue) xxhash.Hash {
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)
case EMPTY:
h = h.Uint64(emptyID)
default:
+10 -1
View File
@@ -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 ByteSlice(k, []byte("foo")) },
func(k string) KeyValue { return ByteSlice(k, []byte("[]i1")) },
func(k string) KeyValue { return KeyValue{Key: Key(k)} }, // Empty value.
}
@@ -188,7 +190,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
@@ -226,6 +228,13 @@ func FuzzHashKVs(f *testing.F) {
}
}
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))
}
}
+11
View File
@@ -117,6 +117,17 @@ func (k Key) StringSlice(v []string) KeyValue {
}
}
// ByteSlice creates a KeyValue instance with a BYTESLICE Value.
//
// If creating both a key and value at the same time, use the provided
// convenience function instead -- ByteSlice(name, value).
func (k Key) ByteSlice(v []byte) KeyValue {
return KeyValue{
Key: k,
Value: ByteSliceValue(v),
}
}
// Defined reports whether the key is not empty.
func (k Key) Defined() bool {
return len(k) != 0
+5
View File
@@ -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.BYTESLICE`,
v: attribute.ByteSliceValue([]byte("foo")),
want: "Zm9v",
},
{
name: `test Key.Emit() can emit a string representing self.EMPTY`,
v: attribute.Value{},
+5
View File
@@ -68,6 +68,11 @@ func StringSlice(k string, v []string) KeyValue {
return Key(k).StringSlice(v)
}
// ByteSlice creates a KeyValue with a BYTESLICE Value type.
func ByteSlice(k string, v []byte) KeyValue {
return Key(k).ByteSlice(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 {
+18
View File
@@ -58,6 +58,14 @@ func TestKeyValueConstructors(t *testing.T) {
Value: attribute.IntValue(123),
},
},
{
name: "ByteSlice",
actual: attribute.ByteSlice("k1", []byte{123}),
expected: attribute.KeyValue{
Key: "k1",
Value: attribute.ByteSliceValue([]byte{123}),
},
},
}
for _, test := range tt {
@@ -114,6 +122,11 @@ func TestKeyValueValid(t *testing.T) {
valid: true,
kv: attribute.String("string", ""),
},
{
desc: "non-empty key with BYTESLICE type Value should be valid",
valid: true,
kv: attribute.ByteSlice("bytes", []byte{}),
},
}
for _, test := range tests {
@@ -152,6 +165,10 @@ func TestIncorrectCast(t *testing.T) {
name: "StringSlice",
val: attribute.BoolSliceValue([]bool{true}),
},
{
name: "ByteSlice",
val: attribute.ByteSliceValue([]byte{123}),
},
{
name: "Empty",
val: attribute.Value{},
@@ -169,6 +186,7 @@ func TestIncorrectCast(t *testing.T) {
tt.val.AsInterface()
tt.val.AsString()
tt.val.AsStringSlice()
tt.val.AsByteSlice()
})
})
}
+3 -2
View File
@@ -17,11 +17,12 @@ func _() {
_ = x[INT64SLICE-6]
_ = x[FLOAT64SLICE-7]
_ = x[STRINGSLICE-8]
_ = x[BYTESLICE-9]
}
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICE"
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICEBYTESLICE"
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69}
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69, 78}
func (i Type) String() string {
idx := int(i) - 0
+28
View File
@@ -4,6 +4,7 @@
package attribute // import "go.opentelemetry.io/otel/attribute"
import (
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
@@ -45,6 +46,8 @@ const (
FLOAT64SLICE
// STRINGSLICE is a slice of strings Type Value.
STRINGSLICE
// BYTESLICE is a slice of bytes Type Value.
BYTESLICE
// INVALID is used for a Value with no value set.
//
// Deprecated: Use EMPTY instead as an empty value is a valid value.
@@ -134,6 +137,14 @@ func StringSliceValue(v []string) Value {
return Value{vtype: STRINGSLICE, slice: attribute.SliceValue(v)}
}
// ByteSliceValue creates a BYTESLICE Value.
func ByteSliceValue(v []byte) Value {
return Value{
vtype: BYTESLICE,
stringly: string(v),
}
}
// Type returns a type of the Value.
func (v Value) Type() Type {
return v.vtype
@@ -215,6 +226,19 @@ func (v Value) asStringSlice() []string {
return attribute.AsSlice[string](v.slice)
}
// AsByteSlice returns the bytes value. Make sure that the Value's type
// is BYTESLICE.
func (v Value) AsByteSlice() []byte {
if v.vtype != BYTESLICE {
return nil
}
return v.asByteSlice()
}
func (v Value) asByteSlice() []byte {
return []byte(v.stringly)
}
type unknownValueType struct{}
// AsInterface returns Value's data as any.
@@ -236,6 +260,8 @@ func (v Value) AsInterface() any {
return v.stringly
case STRINGSLICE:
return v.asStringSlice()
case BYTESLICE:
return v.asByteSlice()
case EMPTY:
return nil
}
@@ -273,6 +299,8 @@ func (v Value) Emit() string {
return string(j)
case STRING:
return v.stringly
case BYTESLICE:
return base64.StdEncoding.EncodeToString(v.asByteSlice())
case EMPTY:
return ""
default:
+19
View File
@@ -86,6 +86,12 @@ func TestValue(t *testing.T) {
wantType: attribute.STRINGSLICE,
wantValue: []string{"forty-two", "negative three", "twelve"},
},
{
name: "Key.ByteSlice() correctly returns keys's internal []byte value",
value: k.ByteSlice([]byte("hello world")).Value,
wantType: attribute.BYTESLICE,
wantValue: []byte("hello world"),
},
{
name: "empty value",
value: attribute.Value{},
@@ -146,6 +152,10 @@ func TestEquivalence(t *testing.T) {
attribute.StringSlice("StringSlice", []string{"one", "two", "three"}),
attribute.StringSlice("StringSlice", []string{"one", "two", "three"}),
},
{
attribute.ByteSlice("ByteSlice", []byte("one")),
attribute.ByteSlice("ByteSlice", []byte("one")),
},
{
attribute.KeyValue{Key: "Empty"},
attribute.KeyValue{Key: "Empty"},
@@ -229,6 +239,10 @@ func TestNotEquivalence(t *testing.T) {
attribute.Float64("Float64", 19.09),
attribute.Float64("Float64", 22.09),
},
{
attribute.ByteSlice("ByteSlice", []byte("bytes value")),
attribute.ByteSlice("ByteSlice", []byte("another value")),
},
{
attribute.Float64Slice("Float64Slice", []float64{12398.1, -37.1713873737, 3}),
attribute.Float64Slice("Float64Slice", []float64{12398.1, -37.1713873737, 5}),
@@ -315,4 +329,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.ByteSlice("ByteSlice", b1)
b2 := kv.Value.AsByteSlice()
assert.Equal(t, b1, b2)
}
+3
View File
@@ -428,6 +428,9 @@ func ValueFromAttribute(value attribute.Value) Value {
res = append(res, StringValue(v))
}
return SliceValue(res...)
case attribute.BYTESLICE:
val := value.AsByteSlice()
return BytesValue(val)
}
// This code should never be reached
// as log attributes are a superset of standard attributes.