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
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:
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user