You've already forked opentelemetry-go
mirror of
https://github.com/open-telemetry/opentelemetry-go.git
synced 2026-06-19 21:45:50 +02:00
attribute: add MAP type support (#8445)
Fixes https://github.com/open-telemetry/opentelemetry-go/issues/7935 ``` $ go test -run=^$ -bench=BenchmarkMap goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/attribute cpu: 13th Gen Intel(R) Core(TM) i7-13800H BenchmarkMap/Len3/Value-20 12423774 143.1 ns/op 192 B/op 1 allocs/op BenchmarkMap/Len3/KeyValue-20 8123966 170.4 ns/op 192 B/op 1 allocs/op BenchmarkMap/Len3/AsMap-20 12685639 111.0 ns/op 192 B/op 1 allocs/op BenchmarkMap/Len3/String-20 4482690 250.1 ns/op 128 B/op 1 allocs/op BenchmarkMap/Len3/Emit-20 5927258 205.5 ns/op 128 B/op 1 allocs/op BenchmarkMap/Len5Nested/Value-20 2773639 387.1 ns/op 320 B/op 1 allocs/op BenchmarkMap/Len5Nested/KeyValue-20 3093283 404.7 ns/op 320 B/op 1 allocs/op BenchmarkMap/Len5Nested/AsMap-20 8646662 161.1 ns/op 320 B/op 1 allocs/op BenchmarkMap/Len5Nested/String-20 2785362 499.1 ns/op 208 B/op 1 allocs/op BenchmarkMap/Len5Nested/Emit-20 2882395 375.2 ns/op 208 B/op 1 allocs/op ``` ``` $ go test -run=^$ -bench=BenchmarkHashValueMap goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/attribute cpu: 13th Gen Intel(R) Core(TM) i7-13800H BenchmarkHashValueMap/Len2-20 7589696 133.0 ns/op 80 B/op 1 allocs/op BenchmarkHashValueMap/Len5-20 6254871 205.7 ns/op 80 B/op 1 allocs/op BenchmarkHashValueMap/Len8Nested-20 2726431 424.1 ns/op 80 B/op 1 allocs/op ```
This commit is contained in:
@@ -10,6 +10,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
|
||||
### Added
|
||||
|
||||
- Add `Map` and `MapValue` functions for new `MAP` attribute type in `go.opentelemetry.io/otel/attribute`. (#8445)
|
||||
- Add `WithUnsafeAttributes` to `go.opentelemetry.io/otel/metric/x` as an experimental no-copy attribute option intended for future performance work. This is a work in progress. (#8251)
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -25,6 +25,7 @@ var (
|
||||
outStr string
|
||||
outStrSlice []string
|
||||
outValueSlice []attribute.Value
|
||||
outMap []attribute.KeyValue
|
||||
)
|
||||
|
||||
func benchmarkEmit(kv attribute.KeyValue) func(*testing.B) {
|
||||
@@ -395,6 +396,58 @@ func BenchmarkSlice(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMap(b *testing.B) {
|
||||
for _, bench := range []struct {
|
||||
name string
|
||||
v []attribute.KeyValue
|
||||
}{
|
||||
{
|
||||
name: "Len3",
|
||||
v: []attribute.KeyValue{
|
||||
attribute.Bool("enabled", true),
|
||||
attribute.Int("count", 42),
|
||||
attribute.String("name", "test"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Len5Nested",
|
||||
v: []attribute.KeyValue{
|
||||
attribute.String("quote", "quote\""),
|
||||
attribute.Float64("inf", math.Inf(1)),
|
||||
attribute.ByteSlice("bin", []byte("bin")),
|
||||
attribute.Key("nested").Map(attribute.String("key", "value"), attribute.Key("empty").Slice(attribute.Value{})),
|
||||
attribute.Bool("enabled", false),
|
||||
},
|
||||
},
|
||||
} {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
k, v := "map", bench.v
|
||||
kv := attribute.Map(k, v...)
|
||||
|
||||
b.Run("Value", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
outV = attribute.MapValue(v...)
|
||||
}
|
||||
})
|
||||
b.Run("KeyValue", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
outKV = attribute.Map(k, v...)
|
||||
}
|
||||
})
|
||||
b.Run("AsMap", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
outMap = kv.Value.AsMap()
|
||||
}
|
||||
})
|
||||
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)
|
||||
|
||||
@@ -29,6 +29,7 @@ const (
|
||||
stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian)
|
||||
byteSliceID uint64 = 6874028470941080415 // "_[]byte_" (little endian)
|
||||
sliceID uint64 = 7883494272577650031 // "__slice_" (little endian)
|
||||
mapID uint64 = 6872316492666199903 // "__map___" (little endian)
|
||||
emptyID uint64 = 7305809155345288421 // "__empty_" (little endian)
|
||||
)
|
||||
|
||||
@@ -116,6 +117,29 @@ func hashValue(h xxhash.Hash, v Value) xxhash.Hash {
|
||||
h = hashValue(h, rv.Index(i).Interface().(Value))
|
||||
}
|
||||
}
|
||||
case MAP:
|
||||
h = h.Uint64(mapID)
|
||||
switch vals := v.slice.(type) {
|
||||
case [0]KeyValue:
|
||||
// No values to hash, but the type identifier is still hashed above.
|
||||
case [1]KeyValue:
|
||||
h = hashMap(h, vals[:])
|
||||
case [2]KeyValue:
|
||||
h = hashMap(h, vals[:])
|
||||
case [3]KeyValue:
|
||||
h = hashMap(h, vals[:])
|
||||
case [4]KeyValue:
|
||||
h = hashMap(h, vals[:])
|
||||
case [5]KeyValue:
|
||||
h = hashMap(h, vals[:])
|
||||
default:
|
||||
rv := reflect.ValueOf(v.slice)
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
kv := rv.Index(i).Interface().(KeyValue)
|
||||
h = h.String(string(kv.Key))
|
||||
h = hashValue(h, kv.Value)
|
||||
}
|
||||
}
|
||||
case EMPTY:
|
||||
h = h.Uint64(emptyID)
|
||||
default:
|
||||
@@ -134,3 +158,11 @@ func hashValueSlice(h xxhash.Hash, vals []Value) xxhash.Hash {
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func hashMap(h xxhash.Hash, vals []KeyValue) xxhash.Hash {
|
||||
for _, kv := range vals {
|
||||
h = h.String(string(kv.Key))
|
||||
h = hashValue(h, kv.Value)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
+139
-2
@@ -95,6 +95,22 @@ var keyVals = []keyVal{
|
||||
StringSliceValue([]string{"fallback"}),
|
||||
)
|
||||
}},
|
||||
{name: "MapLen0", kv: func(k string) KeyValue { return Map(k) }},
|
||||
{name: "MapLen2", kv: func(k string) KeyValue {
|
||||
return Map(
|
||||
k,
|
||||
String("b", "two"),
|
||||
Int("a", 1),
|
||||
)
|
||||
}},
|
||||
{name: "MapNested", kv: func(k string) KeyValue {
|
||||
return Map(
|
||||
k,
|
||||
String("nested", "map"),
|
||||
Map("child", Float64("f", math.Inf(1)), ByteSlice("bin", []byte("bin"))),
|
||||
Slice("slice", BoolValue(true), StringValue("tail")),
|
||||
)
|
||||
}},
|
||||
{name: "EmptyValue", kv: func(k string) KeyValue { return KeyValue{Key: Key(k)} }},
|
||||
}
|
||||
|
||||
@@ -153,6 +169,57 @@ func TestHashKVs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashValueMapOrdering(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
kvs []KeyValue
|
||||
}{
|
||||
{
|
||||
name: "Len4",
|
||||
kvs: []KeyValue{
|
||||
String("d", "4"),
|
||||
String("a", "1"),
|
||||
String("c", "3"),
|
||||
String("b", "2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Len5",
|
||||
kvs: []KeyValue{
|
||||
String("e", "5"),
|
||||
String("a", "1"),
|
||||
String("d", "4"),
|
||||
String("b", "2"),
|
||||
String("c", "3"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Reflect",
|
||||
kvs: []KeyValue{
|
||||
String("f", "6"),
|
||||
String("a", "1"),
|
||||
String("e", "5"),
|
||||
String("b", "2"),
|
||||
String("d", "4"),
|
||||
String("c", "3"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reversed := slices.Clone(tt.kvs)
|
||||
slices.Reverse(reversed)
|
||||
|
||||
got := hashValue(xxhash.New(), MapValue(tt.kvs...)).Sum64()
|
||||
want := hashValue(xxhash.New(), MapValue(reversed...)).Sum64()
|
||||
if got != want {
|
||||
t.Fatalf("hashValue(MapValue(%v)) = %d, want %d", tt.kvs, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func slice(kvs []KeyValue) string {
|
||||
if len(kvs) == 0 {
|
||||
return "[]"
|
||||
@@ -255,6 +322,53 @@ func BenchmarkHashValueSlice(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHashValueMap(b *testing.B) {
|
||||
benches := []struct {
|
||||
name string
|
||||
v Value
|
||||
}{
|
||||
{
|
||||
name: "Len2",
|
||||
v: MapValue(
|
||||
Bool("two", true),
|
||||
String("one", "one"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Len5",
|
||||
v: MapValue(
|
||||
Bool("one", true),
|
||||
Int("two", 2),
|
||||
String("three", "three"),
|
||||
Float64("four", 4.5),
|
||||
ByteSlice("five", []byte("five")),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Len8Nested",
|
||||
v: MapValue(
|
||||
Bool("one", true),
|
||||
Int("two", 2),
|
||||
String("three", "three"),
|
||||
Float64("four", 4.5),
|
||||
ByteSlice("five", []byte("five")),
|
||||
Map("six", String("nested", "map"), Int64("value", 6)),
|
||||
Slice("seven", StringValue("nested"), BoolValue(true)),
|
||||
StringSlice("eight", []string{"seven", "eight"}),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, bench := range benches {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for b.Loop() {
|
||||
hashValue(xxhash.New(), bench.v).Sum64()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzHashKVs(f *testing.F) {
|
||||
// Add seed inputs to ensure coverage of edge cases.
|
||||
f.Add("", "", "", "", "", "", 0, int64(0), 0.0, false, uint8(0))
|
||||
@@ -293,9 +407,9 @@ func FuzzHashKVs(f *testing.F) {
|
||||
kvs = append(kvs, Bool(k5, b))
|
||||
}
|
||||
|
||||
// Add slice types based on sliceType parameter.
|
||||
// Add slice and composite types based on sliceType parameter.
|
||||
if numAttrs > 5 {
|
||||
switch sliceType % 6 {
|
||||
switch sliceType % 7 {
|
||||
case 0:
|
||||
// Test BoolSlice with variable length.
|
||||
bools := make([]bool, len(s)%5) // 0-4 elements
|
||||
@@ -355,6 +469,22 @@ func FuzzHashKVs(f *testing.F) {
|
||||
}
|
||||
}
|
||||
kvs = append(kvs, Slice("slice", values...))
|
||||
case 6:
|
||||
values := make([]KeyValue, len(s)%4) // 0-3 elements
|
||||
for i := range values {
|
||||
key := fmt.Sprintf("item_%d", i)
|
||||
switch i % 4 {
|
||||
case 0:
|
||||
values[i] = Bool(key, (i+len(k1))%2 == 0)
|
||||
case 1:
|
||||
values[i] = Int(key, i+len(k2))
|
||||
case 2:
|
||||
values[i] = String(key, fmt.Sprintf("item_%d", i))
|
||||
case 3:
|
||||
values[i] = Map(key, Float64("float", fVal), ByteSlice("bin", []byte("bin")))
|
||||
}
|
||||
}
|
||||
kvs = append(kvs, Map("map", values...))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,6 +582,13 @@ func FuzzHashKVs(f *testing.F) {
|
||||
}
|
||||
modifiedKvs[0] = Slice(string(modifiedKvs[0].Key), newSlice...)
|
||||
}
|
||||
case MAP:
|
||||
origMap := modifiedKvs[0].Value.AsMap()
|
||||
if len(origMap) > 0 {
|
||||
newMap := slices.Clone(origMap)
|
||||
newMap[0] = String(string(newMap[0].Key), "modified")
|
||||
modifiedKvs[0] = Map(string(modifiedKvs[0].Key), newMap...)
|
||||
}
|
||||
case EMPTY:
|
||||
modifiedKvs[0] = String(string(modifiedKvs[0].Key), "not_empty")
|
||||
}
|
||||
|
||||
@@ -139,6 +139,20 @@ func (k Key) Slice(v ...Value) KeyValue {
|
||||
}
|
||||
}
|
||||
|
||||
// Map creates a KeyValue instance with a MAP Value.
|
||||
//
|
||||
// Users should avoid providing duplicate keys; many receivers handle maps
|
||||
// containing duplicate keys unpredictably.
|
||||
//
|
||||
// If creating both a key and value at the same time, use the provided
|
||||
// convenience function instead -- Map(name, values...).
|
||||
func (k Key) Map(v ...KeyValue) KeyValue {
|
||||
return KeyValue{
|
||||
Key: k,
|
||||
Value: MapValue(v...),
|
||||
}
|
||||
}
|
||||
|
||||
// Defined reports whether the key is not empty.
|
||||
func (k Key) Defined() bool {
|
||||
return len(k) != 0
|
||||
|
||||
@@ -124,6 +124,15 @@ func TestEmit(t *testing.T) {
|
||||
),
|
||||
want: `[true,"foo\"bar","Infinity","Ymlu"]`,
|
||||
},
|
||||
{
|
||||
name: `test Key.Emit() can emit a string representing self.MAP`,
|
||||
v: attribute.MapValue(
|
||||
attribute.String("b", "foo\"bar"),
|
||||
attribute.Float64("a", math.Inf(1)),
|
||||
attribute.Key("bytes").ByteSlice([]byte("bin")),
|
||||
),
|
||||
want: `{"a":"Infinity","b":"foo\"bar","bytes":"Ymlu"}`,
|
||||
},
|
||||
{
|
||||
name: `test Key.Emit() can emit a string representing self.EMPTY`,
|
||||
v: attribute.Value{},
|
||||
@@ -151,4 +160,15 @@ func TestString(t *testing.T) {
|
||||
)
|
||||
|
||||
require.Equal(t, `["foo\nbar","NaN",["Ymlu",null]]`, v.String())
|
||||
|
||||
m := attribute.MapValue(
|
||||
attribute.String("foo\nbar", "value"),
|
||||
attribute.Float64("nan", math.NaN()),
|
||||
attribute.Key("nested").Map(
|
||||
attribute.Key("bytes").ByteSlice([]byte("bin")),
|
||||
attribute.Key("empty").Slice(attribute.Value{}),
|
||||
),
|
||||
)
|
||||
|
||||
require.JSONEq(t, `{"foo\nbar":"value","nan":"NaN","nested":{"bytes":"Ymlu","empty":[null]}}`, m.String())
|
||||
}
|
||||
|
||||
@@ -78,6 +78,14 @@ func Slice(k string, v ...Value) KeyValue {
|
||||
return Key(k).Slice(v...)
|
||||
}
|
||||
|
||||
// Map creates a KeyValue with a MAP Value type.
|
||||
//
|
||||
// Users should avoid providing duplicate keys; many receivers handle maps
|
||||
// containing duplicate keys unpredictably.
|
||||
func Map(k string, v ...KeyValue) KeyValue {
|
||||
return Key(k).Map(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 {
|
||||
|
||||
@@ -74,6 +74,17 @@ func TestKeyValueConstructors(t *testing.T) {
|
||||
Value: attribute.SliceValue(attribute.BoolValue(true), attribute.IntValue(42)),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Map",
|
||||
actual: attribute.Map("k1", attribute.String("b", "two"), attribute.Int("a", 1)),
|
||||
expected: attribute.KeyValue{
|
||||
Key: "k1",
|
||||
Value: attribute.MapValue(
|
||||
attribute.Int("a", 1),
|
||||
attribute.String("b", "two"),
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tt {
|
||||
@@ -140,6 +151,11 @@ func TestKeyValueValid(t *testing.T) {
|
||||
valid: true,
|
||||
kv: attribute.Slice("slice", attribute.StringValue("value")),
|
||||
},
|
||||
{
|
||||
desc: "non-empty key with MAP type Value should be valid",
|
||||
valid: true,
|
||||
kv: attribute.Map("map", attribute.String("key", "value")),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -190,6 +206,10 @@ func TestIncorrectCast(t *testing.T) {
|
||||
name: "Slice",
|
||||
val: attribute.SliceValue(attribute.StringValue("value")),
|
||||
},
|
||||
{
|
||||
name: "Map",
|
||||
val: attribute.MapValue(attribute.String("key", "value")),
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -201,6 +221,7 @@ func TestIncorrectCast(t *testing.T) {
|
||||
tt.val.AsInt64()
|
||||
tt.val.AsInt64Slice()
|
||||
tt.val.AsInterface()
|
||||
tt.val.AsMap()
|
||||
tt.val.AsSlice()
|
||||
tt.val.AsString()
|
||||
tt.val.AsStringSlice()
|
||||
|
||||
@@ -19,11 +19,12 @@ func _() {
|
||||
_ = x[STRINGSLICE-8]
|
||||
_ = x[BYTESLICE-9]
|
||||
_ = x[SLICE-10]
|
||||
_ = x[MAP-11]
|
||||
}
|
||||
|
||||
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICEBYTESLICESLICE"
|
||||
const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICEBYTESLICESLICEMAP"
|
||||
|
||||
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69, 78, 83}
|
||||
var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69, 78, 83, 86}
|
||||
|
||||
func (i Type) String() string {
|
||||
idx := int(i) - 0
|
||||
|
||||
+208
-4
@@ -4,11 +4,13 @@
|
||||
package attribute // import "go.opentelemetry.io/otel/attribute"
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -54,6 +56,11 @@ const (
|
||||
BYTESLICE
|
||||
// SLICE is a slice of Value Type values.
|
||||
SLICE
|
||||
// MAP is a slice of KeyValue values representing a map of heterogeneous values.
|
||||
//
|
||||
// Note that MAP values may contain duplicate keys if duplicate keys are
|
||||
// provided when creating the value.
|
||||
MAP
|
||||
// INVALID is used for a Value with no value set.
|
||||
//
|
||||
// Deprecated: Use EMPTY instead as an empty value is a valid value.
|
||||
@@ -156,6 +163,14 @@ func SliceValue(v ...Value) Value {
|
||||
return Value{vtype: SLICE, slice: sliceValue(v)}
|
||||
}
|
||||
|
||||
// MapValue creates a MAP Value.
|
||||
//
|
||||
// Users should avoid providing duplicate keys; many receivers handle maps
|
||||
// containing duplicate keys unpredictably.
|
||||
func MapValue(v ...KeyValue) Value {
|
||||
return Value{vtype: MAP, slice: mapValue(v)}
|
||||
}
|
||||
|
||||
// Type returns a type of the Value.
|
||||
func (v Value) Type() Type {
|
||||
return v.vtype
|
||||
@@ -277,6 +292,53 @@ func asValueSliceReflect(v any) []Value {
|
||||
return cpy
|
||||
}
|
||||
|
||||
// AsMap returns the []KeyValue value. Make sure that the Value's type is
|
||||
// MAP.
|
||||
//
|
||||
// The returned slice is sorted by key and may differ from the order
|
||||
// provided when creating the map value.
|
||||
//
|
||||
// The returned slice may contain duplicate keys if duplicate keys were
|
||||
// provided when creating the map value. Callers should not assume the returned
|
||||
// keys are unique.
|
||||
func (v Value) AsMap() []KeyValue {
|
||||
if v.vtype != MAP {
|
||||
return nil
|
||||
}
|
||||
return v.asMap()
|
||||
}
|
||||
|
||||
func (v Value) asMap() []KeyValue {
|
||||
switch vals := v.slice.(type) {
|
||||
case [0]KeyValue:
|
||||
return []KeyValue{}
|
||||
case [1]KeyValue:
|
||||
return []KeyValue{vals[0]}
|
||||
case [2]KeyValue:
|
||||
return []KeyValue{vals[0], vals[1]}
|
||||
case [3]KeyValue:
|
||||
return []KeyValue{vals[0], vals[1], vals[2]}
|
||||
case [4]KeyValue:
|
||||
return []KeyValue{vals[0], vals[1], vals[2], vals[3]}
|
||||
case [5]KeyValue:
|
||||
return []KeyValue{vals[0], vals[1], vals[2], vals[3], vals[4]}
|
||||
default:
|
||||
return asKeyValueSliceReflect(v.slice)
|
||||
}
|
||||
}
|
||||
|
||||
func asKeyValueSliceReflect(v any) []KeyValue {
|
||||
rv := reflect.ValueOf(v)
|
||||
if !rv.IsValid() || rv.Kind() != reflect.Array || rv.Type().Elem() != reflect.TypeFor[KeyValue]() {
|
||||
return nil
|
||||
}
|
||||
cpy := make([]KeyValue, rv.Len())
|
||||
if len(cpy) > 0 {
|
||||
_ = reflect.Copy(reflect.ValueOf(cpy), rv)
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
// AsByteSlice returns the bytes value. Make sure that the Value's type
|
||||
// is BYTESLICE.
|
||||
func (v Value) AsByteSlice() []byte {
|
||||
@@ -315,6 +377,8 @@ func (v Value) AsInterface() any {
|
||||
return v.asByteSlice()
|
||||
case SLICE:
|
||||
return v.asSlice()
|
||||
case MAP:
|
||||
return v.asMap()
|
||||
case EMPTY:
|
||||
return nil
|
||||
}
|
||||
@@ -327,10 +391,10 @@ func (v Value) AsInterface() any {
|
||||
// Strings are returned as-is without JSON quoting, booleans and integers use
|
||||
// JSON literals, floating-point values use JSON numbers except that NaN and
|
||||
// ±Inf are rendered as NaN, Infinity, and -Infinity, byte slices are
|
||||
// base64-encoded, empty values are the empty string, and slices are encoded as
|
||||
// JSON arrays. String, byte, and special floating-point values inside arrays
|
||||
// are encoded as JSON strings, and empty values inside arrays are encoded as
|
||||
// null.
|
||||
// base64-encoded, empty values are the empty string, slices are encoded as JSON
|
||||
// arrays, and maps are encoded as JSON objects. String, byte, and special
|
||||
// floating-point values inside arrays and maps are encoded as JSON strings, and
|
||||
// empty values inside arrays and maps are encoded as null.
|
||||
//
|
||||
// [OpenTelemetry AnyValue representation for non-OTLP protocols]: https://opentelemetry.io/docs/specs/otel/common/#anyvalue-representation-for-non-otlp-protocols
|
||||
func (v Value) String() string {
|
||||
@@ -355,6 +419,8 @@ func (v Value) String() string {
|
||||
return formatByteSlice(v.stringly)
|
||||
case SLICE:
|
||||
return formatValueSliceValue(v.slice)
|
||||
case MAP:
|
||||
return formatMapValue(v.slice)
|
||||
case EMPTY:
|
||||
return ""
|
||||
default:
|
||||
@@ -399,6 +465,8 @@ func (v Value) Emit() string {
|
||||
return formatByteSlice(v.stringly)
|
||||
case SLICE:
|
||||
return formatValueSliceValue(v.slice)
|
||||
case MAP:
|
||||
return formatMapValue(v.slice)
|
||||
case EMPTY:
|
||||
return ""
|
||||
default:
|
||||
@@ -439,6 +507,47 @@ func sliceValueReflect(v []Value) any {
|
||||
return cp.Interface()
|
||||
}
|
||||
|
||||
func mapValue(v []KeyValue) any {
|
||||
switch len(v) {
|
||||
case 0:
|
||||
return [0]KeyValue{}
|
||||
case 1:
|
||||
return [1]KeyValue{v[0]}
|
||||
case 2:
|
||||
vals := [2]KeyValue{v[0], v[1]}
|
||||
sortKeyValues(vals[:])
|
||||
return vals
|
||||
case 3:
|
||||
vals := [3]KeyValue{v[0], v[1], v[2]}
|
||||
sortKeyValues(vals[:])
|
||||
return vals
|
||||
case 4:
|
||||
vals := [4]KeyValue{v[0], v[1], v[2], v[3]}
|
||||
sortKeyValues(vals[:])
|
||||
return vals
|
||||
case 5:
|
||||
vals := [5]KeyValue{v[0], v[1], v[2], v[3], v[4]}
|
||||
sortKeyValues(vals[:])
|
||||
return vals
|
||||
default:
|
||||
return mapValueReflect(v)
|
||||
}
|
||||
}
|
||||
|
||||
func mapValueReflect(v []KeyValue) any {
|
||||
cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[KeyValue]())).Elem()
|
||||
reflect.Copy(cp, reflect.ValueOf(v))
|
||||
vals := cp.Slice(0, len(v)).Interface().([]KeyValue)
|
||||
sortKeyValues(vals)
|
||||
return cp.Interface()
|
||||
}
|
||||
|
||||
func sortKeyValues(vals []KeyValue) {
|
||||
slices.SortStableFunc(vals, func(a, b KeyValue) int {
|
||||
return cmp.Compare(a.Key, b.Key)
|
||||
})
|
||||
}
|
||||
|
||||
func formatBoolSliceValue(v any) string {
|
||||
switch vals := v.(type) {
|
||||
case [0]bool:
|
||||
@@ -807,6 +916,37 @@ func formatValueSliceReflect(v any) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatMapValue(v any) string {
|
||||
switch vals := v.(type) {
|
||||
case [0]KeyValue:
|
||||
return "{}"
|
||||
case [1]KeyValue:
|
||||
return formatMap(vals[:])
|
||||
case [2]KeyValue:
|
||||
return formatMap(vals[:])
|
||||
case [3]KeyValue:
|
||||
return formatMap(vals[:])
|
||||
case [4]KeyValue:
|
||||
return formatMap(vals[:])
|
||||
case [5]KeyValue:
|
||||
return formatMap(vals[:])
|
||||
default:
|
||||
return formatMapReflect(v)
|
||||
}
|
||||
}
|
||||
|
||||
func formatMap(vals []KeyValue) string {
|
||||
var b strings.Builder
|
||||
appendMap(&b, vals)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatMapReflect(v any) string {
|
||||
var b strings.Builder
|
||||
appendMapReflect(&b, reflect.ValueOf(v))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func appendValueSliceValue(dst *strings.Builder, v any) {
|
||||
switch vals := v.(type) {
|
||||
case [0]Value:
|
||||
@@ -852,6 +992,68 @@ func appendValueSliceReflect(dst *strings.Builder, rv reflect.Value) {
|
||||
_ = dst.WriteByte(']')
|
||||
}
|
||||
|
||||
func appendMapValue(dst *strings.Builder, v any) {
|
||||
switch vals := v.(type) {
|
||||
case [0]KeyValue:
|
||||
_, _ = dst.WriteString("{}")
|
||||
case [1]KeyValue:
|
||||
appendMap(dst, vals[:])
|
||||
case [2]KeyValue:
|
||||
appendMap(dst, vals[:])
|
||||
case [3]KeyValue:
|
||||
appendMap(dst, vals[:])
|
||||
case [4]KeyValue:
|
||||
appendMap(dst, vals[:])
|
||||
case [5]KeyValue:
|
||||
appendMap(dst, vals[:])
|
||||
default:
|
||||
appendMapReflect(dst, reflect.ValueOf(v))
|
||||
}
|
||||
}
|
||||
|
||||
func appendMap(dst *strings.Builder, vals []KeyValue) {
|
||||
// Estimate 32 bytes per value for small values, plus key quotes, colon,
|
||||
// and commas. Escaped keys and larger values grow the builder as needed.
|
||||
size := len("{}") + len(vals)*commaLen + len(vals)*32
|
||||
for _, val := range vals {
|
||||
size += len(val.Key) + len(`"":`)
|
||||
}
|
||||
|
||||
dst.Grow(size)
|
||||
_ = dst.WriteByte('{')
|
||||
for i, val := range vals {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
appendJSONString(dst, string(val.Key))
|
||||
_ = dst.WriteByte(':')
|
||||
appendJSONValue(dst, val.Value)
|
||||
}
|
||||
_ = dst.WriteByte('}')
|
||||
}
|
||||
|
||||
func appendMapReflect(dst *strings.Builder, rv reflect.Value) {
|
||||
// Estimate 32 bytes per value for small values, plus key quotes, colon,
|
||||
// and commas. Escaped keys and larger values grow the builder as needed.
|
||||
size := len("{}") + rv.Len()*commaLen + rv.Len()*32
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
size += len(rv.Index(i).Field(0).String()) + len(`"":`)
|
||||
}
|
||||
|
||||
dst.Grow(size)
|
||||
_ = dst.WriteByte('{')
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
if i > 0 {
|
||||
_ = dst.WriteByte(',')
|
||||
}
|
||||
val := rv.Index(i).Interface().(KeyValue)
|
||||
appendJSONString(dst, string(val.Key))
|
||||
_ = dst.WriteByte(':')
|
||||
appendJSONValue(dst, val.Value)
|
||||
}
|
||||
_ = dst.WriteByte('}')
|
||||
}
|
||||
|
||||
func appendJSONValue(dst *strings.Builder, v Value) {
|
||||
switch v.Type() {
|
||||
case BOOL:
|
||||
@@ -894,6 +1096,8 @@ func appendJSONValue(dst *strings.Builder, v Value) {
|
||||
_ = dst.WriteByte('"')
|
||||
case SLICE:
|
||||
appendValueSliceValue(dst, v.slice)
|
||||
case MAP:
|
||||
appendMapValue(dst, v.slice)
|
||||
case EMPTY:
|
||||
_, _ = dst.WriteString("null")
|
||||
default:
|
||||
|
||||
@@ -99,6 +99,15 @@ func TestValue(t *testing.T) {
|
||||
wantType: attribute.SLICE,
|
||||
wantValue: []attribute.Value{attribute.BoolValue(true), attribute.IntValue(42), attribute.StringValue("foo")},
|
||||
},
|
||||
{
|
||||
name: "Key.Map() correctly returns keys's internal []KeyValue value",
|
||||
value: k.Map(attribute.String("b", "two"), attribute.Int("a", 1)).Value,
|
||||
wantType: attribute.MAP,
|
||||
wantValue: []attribute.KeyValue{
|
||||
attribute.Int("a", 1),
|
||||
attribute.String("b", "two"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
value: attribute.Value{},
|
||||
@@ -178,6 +187,18 @@ func TestEquivalence(t *testing.T) {
|
||||
attribute.SliceValue(attribute.StringValue("nested")),
|
||||
),
|
||||
},
|
||||
{
|
||||
attribute.Map(
|
||||
"Map",
|
||||
attribute.Bool("b", true),
|
||||
attribute.Map("a", attribute.String("nested", "value")),
|
||||
),
|
||||
attribute.Map(
|
||||
"Map",
|
||||
attribute.Map("a", attribute.String("nested", "value")),
|
||||
attribute.Bool("b", true),
|
||||
),
|
||||
},
|
||||
{
|
||||
attribute.KeyValue{Key: "Empty"},
|
||||
attribute.KeyValue{Key: "Empty"},
|
||||
@@ -281,6 +302,14 @@ func TestNotEquivalence(t *testing.T) {
|
||||
attribute.Slice("Slice", attribute.BoolValue(true), attribute.IntValue(42)),
|
||||
attribute.Slice("Slice", attribute.BoolValue(true), attribute.IntValue(43)),
|
||||
},
|
||||
{
|
||||
attribute.Map("Map", attribute.String("key", "value")),
|
||||
attribute.Map("Map", attribute.String("key", "other")),
|
||||
},
|
||||
{
|
||||
attribute.Map("Map", attribute.String("key", "value")),
|
||||
attribute.Map("Map", attribute.String("other", "value")),
|
||||
},
|
||||
{
|
||||
attribute.KeyValue{Key: "Empty"},
|
||||
attribute.String("Empty", ""),
|
||||
@@ -419,6 +448,131 @@ func TestAsSlice(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsMap(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in []attribute.KeyValue
|
||||
want []attribute.KeyValue
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "len1",
|
||||
in: []attribute.KeyValue{attribute.Bool("a", true)},
|
||||
want: []attribute.KeyValue{attribute.Bool("a", true)},
|
||||
},
|
||||
{
|
||||
name: "len2 sorted",
|
||||
in: []attribute.KeyValue{
|
||||
attribute.Int("b", 2),
|
||||
attribute.Int("a", 1),
|
||||
},
|
||||
want: []attribute.KeyValue{
|
||||
attribute.Int("a", 1),
|
||||
attribute.Int("b", 2),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "len4 sorted",
|
||||
in: []attribute.KeyValue{
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("c", "3"),
|
||||
attribute.String("b", "2"),
|
||||
},
|
||||
want: []attribute.KeyValue{
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("c", "3"),
|
||||
attribute.String("d", "4"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "len5 sorted",
|
||||
in: []attribute.KeyValue{
|
||||
attribute.String("e", "5"),
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("c", "3"),
|
||||
},
|
||||
want: []attribute.KeyValue{
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("c", "3"),
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("e", "5"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reflect path sorted",
|
||||
in: []attribute.KeyValue{
|
||||
attribute.String("f", "6"),
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("e", "5"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("c", "3"),
|
||||
},
|
||||
want: []attribute.KeyValue{
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("c", "3"),
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("e", "5"),
|
||||
attribute.String("f", "6"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "duplicate keys keep stable order",
|
||||
in: []attribute.KeyValue{
|
||||
attribute.String("dup", "first"),
|
||||
attribute.String("a", "before"),
|
||||
attribute.String("dup", "second"),
|
||||
},
|
||||
want: []attribute.KeyValue{
|
||||
attribute.String("a", "before"),
|
||||
attribute.String("dup", "first"),
|
||||
attribute.String("dup", "second"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty keys and values",
|
||||
in: []attribute.KeyValue{
|
||||
attribute.String("z", "last"),
|
||||
{Key: "empty-value"},
|
||||
attribute.String("", "empty-key"),
|
||||
{},
|
||||
},
|
||||
want: []attribute.KeyValue{
|
||||
attribute.String("", "empty-key"),
|
||||
{},
|
||||
{Key: "empty-value"},
|
||||
attribute.String("z", "last"),
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.want == nil {
|
||||
tc.want = []attribute.KeyValue{}
|
||||
}
|
||||
v := attribute.MapValue(tc.in...)
|
||||
if len(tc.in) > 0 {
|
||||
tc.in[0] = attribute.String("mutated", "input")
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.want, v.AsMap())
|
||||
|
||||
got := v.AsMap()
|
||||
if len(got) > 0 {
|
||||
got[0] = attribute.String("mutated", "output")
|
||||
}
|
||||
assert.Equal(t, tc.want, v.AsMap())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueString(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
@@ -751,6 +905,108 @@ func TestValueString(t *testing.T) {
|
||||
`,["tab\treturn\rformfeed\fbackslash\\quote\"backspace\b","\u0001\u2029","<tag>&","a\ufffdb"]` +
|
||||
`,[],[true],[true,2],[true,2,"x"],[true,2,"x","Infinity"],[true,2,"x","Infinity","Ymlu"],[true,2,"x","Infinity","Ymlu",null]]`,
|
||||
},
|
||||
{
|
||||
name: "empty map",
|
||||
v: attribute.MapValue(),
|
||||
want: "{}",
|
||||
},
|
||||
{
|
||||
name: "map with empty key",
|
||||
v: attribute.MapValue(attribute.String("", "value")),
|
||||
want: `{"":"value"}`,
|
||||
},
|
||||
{
|
||||
name: "map with empty value",
|
||||
v: attribute.MapValue(attribute.KeyValue{Key: "empty"}),
|
||||
want: `{"empty":null}`,
|
||||
},
|
||||
{
|
||||
name: "map with empty key and value",
|
||||
v: attribute.MapValue(attribute.KeyValue{}),
|
||||
want: `{"":null}`,
|
||||
},
|
||||
{
|
||||
name: "map len2 sorted",
|
||||
v: attribute.MapValue(
|
||||
attribute.Int("b", 2),
|
||||
attribute.String("a", `hello "world"`),
|
||||
),
|
||||
want: `{"a":"hello \"world\"","b":2}`,
|
||||
},
|
||||
{
|
||||
name: "map len4 fast path",
|
||||
v: attribute.MapValue(
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("c", "3"),
|
||||
attribute.String("b", "2"),
|
||||
),
|
||||
want: `{"a":"1","b":"2","c":"3","d":"4"}`,
|
||||
},
|
||||
{
|
||||
name: "map len5 fast path",
|
||||
v: attribute.MapValue(
|
||||
attribute.String("e", "5"),
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("c", "3"),
|
||||
),
|
||||
want: `{"a":"1","b":"2","c":"3","d":"4","e":"5"}`,
|
||||
},
|
||||
{
|
||||
name: "map escapes keys",
|
||||
v: attribute.MapValue(
|
||||
attribute.String("line\nkey", "value"),
|
||||
attribute.Bool("<tag>&", true),
|
||||
),
|
||||
want: `{"<tag>&":true,"line\nkey":"value"}`,
|
||||
},
|
||||
{
|
||||
name: "map reflect path nested values",
|
||||
v: attribute.MapValue(
|
||||
attribute.String("z", "last"),
|
||||
attribute.Key("bytes").ByteSlice([]byte("bin")),
|
||||
attribute.Key("empty").Slice(attribute.Value{}),
|
||||
attribute.Key("float").Float64(math.Inf(1)),
|
||||
attribute.Key("map").Map(attribute.String("nested", "value")),
|
||||
attribute.Key("slice").Slice(attribute.IntValue(1), attribute.StringValue("two")),
|
||||
),
|
||||
want: `{"bytes":"Ymlu","empty":[null],"float":"Infinity","map":{"nested":"value"},"slice":[1,"two"],"z":"last"}`,
|
||||
},
|
||||
{
|
||||
name: "slice nested map storage paths",
|
||||
v: attribute.SliceValue(
|
||||
attribute.MapValue(),
|
||||
attribute.MapValue(
|
||||
attribute.String("c", "3"),
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("b", "2"),
|
||||
),
|
||||
attribute.MapValue(
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("c", "3"),
|
||||
attribute.String("b", "2"),
|
||||
),
|
||||
attribute.MapValue(
|
||||
attribute.String("e", "5"),
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("c", "3"),
|
||||
),
|
||||
attribute.MapValue(
|
||||
attribute.String("f", "6"),
|
||||
attribute.String("a", "1"),
|
||||
attribute.String("e", "5"),
|
||||
attribute.String("b", "2"),
|
||||
attribute.String("d", "4"),
|
||||
attribute.String("c", "3"),
|
||||
),
|
||||
),
|
||||
want: `[{},{"a":"1","b":"2","c":"3"},{"a":"1","b":"2","c":"3","d":"4"},{"a":"1","b":"2","c":"3","d":"4","e":"5"},{"a":"1","b":"2","c":"3","d":"4","e":"5","f":"6"}]`,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
v: attribute.Value{},
|
||||
|
||||
@@ -438,6 +438,13 @@ func ValueFromAttribute(value attribute.Value) Value {
|
||||
res = append(res, ValueFromAttribute(v))
|
||||
}
|
||||
return SliceValue(res...)
|
||||
case attribute.MAP:
|
||||
val := value.AsMap()
|
||||
res := make([]KeyValue, 0, len(val))
|
||||
for _, kv := range val {
|
||||
res = append(res, KeyValueFromAttribute(kv))
|
||||
}
|
||||
return MapValue(res...)
|
||||
}
|
||||
// This code should never be reached
|
||||
// as log attributes are a superset of standard attributes.
|
||||
|
||||
@@ -284,6 +284,15 @@ func BenchmarkKeyValueFromAttribute(b *testing.B) {
|
||||
attribute.SliceValue(attribute.IntValue(7)),
|
||||
),
|
||||
},
|
||||
{
|
||||
desc: "Map",
|
||||
kv: attribute.Map(
|
||||
"k",
|
||||
attribute.String("b", "two"),
|
||||
attribute.Key("a").Map(attribute.Int("nested", 1)),
|
||||
attribute.Key("slice").Slice(attribute.BoolValue(true)),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.desc, func(b *testing.B) {
|
||||
|
||||
@@ -379,6 +379,19 @@ func TestValueFromAttribute(t *testing.T) {
|
||||
log.SliceValue(log.Int64Value(7)),
|
||||
),
|
||||
},
|
||||
{
|
||||
desc: "Map",
|
||||
v: attribute.MapValue(
|
||||
attribute.String("b", "two"),
|
||||
attribute.Key("a").Map(attribute.Int("nested", 1)),
|
||||
attribute.Key("slice").Slice(attribute.BoolValue(true)),
|
||||
),
|
||||
want: log.MapValue(
|
||||
log.Map("a", log.Int("nested", 1)),
|
||||
log.String("b", "two"),
|
||||
log.Slice("slice", log.BoolValue(true)),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
@@ -456,6 +469,21 @@ func TestKeyValueFromAttribute(t *testing.T) {
|
||||
log.SliceValue(log.Int64Value(7)),
|
||||
),
|
||||
},
|
||||
{
|
||||
desc: "Map",
|
||||
kv: attribute.Map(
|
||||
"k",
|
||||
attribute.String("b", "two"),
|
||||
attribute.Key("a").Map(attribute.Int("nested", 1)),
|
||||
attribute.Key("slice").Slice(attribute.BoolValue(true)),
|
||||
),
|
||||
want: log.Map(
|
||||
"k",
|
||||
log.Map("a", log.Int("nested", 1)),
|
||||
log.String("b", "two"),
|
||||
log.Slice("slice", log.BoolValue(true)),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
|
||||
@@ -851,6 +851,34 @@ func TestEqualKeyValue(t *testing.T) {
|
||||
b: attribute.ByteSlice("bytes", []byte{1, 2, 4}),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "map equal",
|
||||
a: attribute.Map(
|
||||
"map",
|
||||
attribute.String("b", "two"),
|
||||
attribute.Key("a").Map(attribute.Int("nested", 1)),
|
||||
),
|
||||
b: attribute.Map(
|
||||
"map",
|
||||
attribute.Key("a").Map(attribute.Int("nested", 1)),
|
||||
attribute.String("b", "two"),
|
||||
),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "map different value",
|
||||
a: attribute.Map(
|
||||
"map",
|
||||
attribute.String("b", "two"),
|
||||
attribute.Key("a").Map(attribute.Int("nested", 1)),
|
||||
),
|
||||
b: attribute.Map(
|
||||
"map",
|
||||
attribute.Key("a").Map(attribute.Int("nested", 2)),
|
||||
attribute.String("b", "two"),
|
||||
),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
a: attribute.KeyValue{},
|
||||
|
||||
@@ -589,6 +589,10 @@ func equalKeyValue(a, b attribute.KeyValue) bool {
|
||||
if ok := slices.Equal(a.Value.AsSlice(), b.Value.AsSlice()); !ok {
|
||||
return false
|
||||
}
|
||||
case attribute.MAP:
|
||||
if ok := slices.EqualFunc(a.Value.AsMap(), b.Value.AsMap(), equalKeyValue); !ok {
|
||||
return false
|
||||
}
|
||||
case attribute.EMPTY:
|
||||
default:
|
||||
// We control all types passed to this, panic to signal developers
|
||||
|
||||
@@ -358,6 +358,16 @@ func convAttrValue(value attribute.Value) telemetry.Value {
|
||||
out = append(out, convAttrValue(v))
|
||||
}
|
||||
return telemetry.SliceValue(out...)
|
||||
case attribute.MAP:
|
||||
kvs := value.AsMap()
|
||||
out := make([]telemetry.Attr, 0, len(kvs))
|
||||
for _, kv := range kvs {
|
||||
out = append(out, telemetry.Attr{
|
||||
Key: string(kv.Key),
|
||||
Value: convAttrValue(kv.Value),
|
||||
})
|
||||
}
|
||||
return telemetry.MapValue(out...)
|
||||
}
|
||||
return telemetry.Value{}
|
||||
}
|
||||
|
||||
@@ -243,6 +243,42 @@ func TestConvAttrValueSlice(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvAttrValueMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
val := convAttrValue(attribute.MapValue(
|
||||
attribute.String("b", "two"),
|
||||
attribute.Key("a").Map(attribute.Int("nested", 1)),
|
||||
attribute.Key("slice").Slice(attribute.BoolValue(true)),
|
||||
))
|
||||
|
||||
assert.Equal(t, telemetry.ValueKindMap, val.Kind())
|
||||
|
||||
kvs := val.AsMap()
|
||||
if assert.Len(t, kvs, 3) {
|
||||
assert.Equal(t, "a", kvs[0].Key)
|
||||
assert.Equal(t, telemetry.ValueKindMap, kvs[0].Value.Kind())
|
||||
nested := kvs[0].Value.AsMap()
|
||||
if assert.Len(t, nested, 1) {
|
||||
assert.Equal(t, "nested", nested[0].Key)
|
||||
assert.Equal(t, telemetry.ValueKindInt64, nested[0].Value.Kind())
|
||||
assert.EqualValues(t, 1, nested[0].Value.AsInt64())
|
||||
}
|
||||
|
||||
assert.Equal(t, "b", kvs[1].Key)
|
||||
assert.Equal(t, telemetry.ValueKindString, kvs[1].Value.Kind())
|
||||
assert.Equal(t, "two", kvs[1].Value.AsString())
|
||||
|
||||
assert.Equal(t, "slice", kvs[2].Key)
|
||||
assert.Equal(t, telemetry.ValueKindSlice, kvs[2].Value.Kind())
|
||||
slice := kvs[2].Value.AsSlice()
|
||||
if assert.Len(t, slice, 1) {
|
||||
assert.Equal(t, telemetry.ValueKindBool, slice[0].Kind())
|
||||
assert.True(t, slice[0].AsBool())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTracerStartPropagatesOrigCtx(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user