Skip to content
Draft
Show file tree
Hide file tree
Changes from 15 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 @@ -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 `String` method for `KeyValue` type in `go.opentelemetry.io/otel/attribute`. (#8205)
- Add `Slice` and `SliceValue` functions for new `SLICE` attribute type in `go.opentelemetry.io/otel/attribute`. (#8166)
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlptrace`. (#8216)
- Support `SLICE` attributes in `go.opentelemetry.io/otel/exporters/otlp/otlplog`. (#8216)
Expand Down
55 changes: 29 additions & 26 deletions attribute/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,30 @@ func benchmarkEmit(kv attribute.KeyValue) func(*testing.B) {
}
}

func benchmarkString(kv attribute.KeyValue) func(*testing.B) {
func benchmarkValueString(v attribute.Value) func(*testing.B) {
return func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
outStr = kv.Value.String()
outStr = v.String()
}
}
}

func benchmarkKeyValueString(kv attribute.KeyValue) func(*testing.B) {
return func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
outStr = kv.String()
}
}
}

func runStringBenchmarks(b *testing.B, kv attribute.KeyValue) {
b.Run("String", benchmarkValueString(kv.Value))
b.Run("KeyValueString", benchmarkKeyValueString(kv))
b.Run("Emit", benchmarkEmit(kv))
}

func BenchmarkBool(b *testing.B) {
k, v := "bool", true
kv := attribute.Bool(k, v)
Expand All @@ -67,8 +82,7 @@ func BenchmarkBool(b *testing.B) {
outBool = kv.Value.AsBool()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
}

func BenchmarkBoolSlice(b *testing.B) {
Expand Down Expand Up @@ -101,8 +115,7 @@ func BenchmarkBoolSlice(b *testing.B) {
outBoolSlice = kv.Value.AsBoolSlice()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
})
}
}
Expand All @@ -123,8 +136,7 @@ func BenchmarkInt(b *testing.B) {
outKV = attribute.Int(k, v)
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
}

func BenchmarkIntSlice(b *testing.B) {
Expand All @@ -151,8 +163,7 @@ func BenchmarkIntSlice(b *testing.B) {
outKV = attribute.IntSlice(k, v)
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
})
}
}
Expand All @@ -179,8 +190,7 @@ func BenchmarkInt64(b *testing.B) {
outInt64 = kv.Value.AsInt64()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
}

func BenchmarkInt64Slice(b *testing.B) {
Expand Down Expand Up @@ -213,8 +223,7 @@ func BenchmarkInt64Slice(b *testing.B) {
outInt64Slice = kv.Value.AsInt64Slice()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
})
}
}
Expand All @@ -241,8 +250,7 @@ func BenchmarkFloat64(b *testing.B) {
outFloat64 = kv.Value.AsFloat64()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
}

func BenchmarkFloat64Slice(b *testing.B) {
Expand Down Expand Up @@ -275,8 +283,7 @@ func BenchmarkFloat64Slice(b *testing.B) {
outFloat64Slice = kv.Value.AsFloat64Slice()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
})
}
}
Expand All @@ -303,8 +310,7 @@ func BenchmarkString(b *testing.B) {
outStr = kv.Value.AsString()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
}

func BenchmarkStringSlice(b *testing.B) {
Expand Down Expand Up @@ -337,8 +343,7 @@ func BenchmarkStringSlice(b *testing.B) {
outStrSlice = kv.Value.AsStringSlice()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
})
}
}
Expand Down Expand Up @@ -389,8 +394,7 @@ func BenchmarkSlice(b *testing.B) {
outValueSlice = kv.Value.AsSlice()
}
})
b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
})
}
}
Expand Down Expand Up @@ -420,8 +424,7 @@ func BenchmarkByteSlice(b *testing.B) {
}
})

b.Run("String", benchmarkString(kv))
b.Run("Emit", benchmarkEmit(kv))
runStringBenchmarks(b, kv)
}

func BenchmarkSetEquals(b *testing.B) {
Expand Down
86 changes: 86 additions & 0 deletions attribute/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
package attribute // import "go.opentelemetry.io/otel/attribute"

import (
"encoding/base64"
"fmt"
"math"
"reflect"
"strconv"
"strings"
)

// KeyValue holds a key and value pair.
Expand All @@ -13,6 +18,87 @@ type KeyValue struct {
Value Value
}

// String returns a string representation of kv using the
// [OpenTelemetry Attribute representation for non-OTLP] rules.
//
// The returned string is a JSON object containing a single key-value pair.
//
// The returned string is meant for debugging;
// the string representation is not stable.
//
// [OpenTelemetry Attribute representation for non-OTLP]: https://opentelemetry.io/docs/specs/otel/common/#attribute-representation-for-non-otlp
func (kv KeyValue) String() string {
const jsonObjectSyntaxLen = len(`{"":}`)

var b strings.Builder
n := len(kv.Key) + jsonObjectSyntaxLen
switch kv.Value.Type() {
case BOOL:
if kv.Value.AsBool() {
n += len("true")
} else {
n += len("false")
}
case BOOLSLICE:
n += jsonArrayBracketsLen
if l := reflect.ValueOf(kv.Value.slice).Len(); l > 0 {
n += l*boolArrayElemMaxLen + (l-1)*commaLen
}
case INT64:
var buf [int64ArrayElemMaxLen]byte
n += len(strconv.AppendInt(buf[:0], kv.Value.AsInt64(), 10))
case INT64SLICE:
n += jsonArrayBracketsLen
if l := reflect.ValueOf(kv.Value.slice).Len(); l > 0 {
n += l*int64ArrayElemMaxLen + (l-1)*commaLen
}
case FLOAT64:
val := kv.Value.AsFloat64()
switch {
case math.IsNaN(val):
n += len(`"NaN"`)
case math.IsInf(val, 1):
n += len(`"Infinity"`)
case math.IsInf(val, -1):
n += len(`"-Infinity"`)
default:
var buf [float64ArrayElemMaxLen]byte
n += len(strconv.AppendFloat(buf[:0], val, 'g', -1, 64))
}
case FLOAT64SLICE:
n += jsonArrayBracketsLen
if l := reflect.ValueOf(kv.Value.slice).Len(); l > 0 {
n += l*float64ArrayElemMaxLen + (l-1)*commaLen
}
case STRING:
n += len(kv.Value.stringly) + quotesLen
case STRINGSLICE:
n += jsonArrayBracketsLen
if l := reflect.ValueOf(kv.Value.slice).Len(); l > 0 {
n += l*smallObjectLen + (l-1)*commaLen
}
case BYTESLICE:
n += base64.StdEncoding.EncodedLen(len(kv.Value.stringly)) + quotesLen
case SLICE:
n += jsonArrayBracketsLen
if l := reflect.ValueOf(kv.Value.slice).Len(); l > 0 {
n += l*smallObjectLen + (l-1)*commaLen
}
Comment thread
pellared marked this conversation as resolved.
case EMPTY:
n += len("null")
default:
n += len(`"unknown"`)
}
b.Grow(n)

_ = b.WriteByte('{')
appendJSONString(&b, string(kv.Key))
_ = b.WriteByte(':')
appendJSONValue(&b, kv.Value)
_ = b.WriteByte('}')
return b.String()
}

// Valid reports whether kv is a valid OpenTelemetry attribute.
func (kv KeyValue) Valid() bool {
return kv.Key.Defined()
Expand Down
124 changes: 124 additions & 0 deletions attribute/kv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package attribute_test

import (
"math"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -149,6 +150,129 @@ func TestKeyValueValid(t *testing.T) {
}
}

func TestKeyValueString(t *testing.T) {
for _, tc := range []struct {
name string
kv attribute.KeyValue
want string
}{
{
name: "bool true",
kv: attribute.Bool("bool", true),
want: `{"bool":true}`,
},
{
name: "bool false",
kv: attribute.Bool("bool", false),
want: `{"bool":false}`,
},
{
name: "bool slice",
kv: attribute.BoolSlice("bools", []bool{true, false}),
want: `{"bools":[true,false]}`,
},
{
name: "empty bool slice",
kv: attribute.BoolSlice("bools", []bool{}),
want: `{"bools":[]}`,
},
{
name: "int64",
kv: attribute.Int64("count", -42),
want: `{"count":-42}`,
},
{
name: "int64 slice",
kv: attribute.Int64Slice("counts", []int64{42, -3}),
want: `{"counts":[42,-3]}`,
},
{
name: "empty int64 slice",
kv: attribute.Int64Slice("counts", []int64{}),
want: `{"counts":[]}`,
},
{
name: "float64",
kv: attribute.Float64("float", 3.14),
want: `{"float":3.14}`,
},
{
name: "string",
kv: attribute.String("greeting", `hello "world"`),
want: `{"greeting":"hello \"world\""}`,
},
{
name: "string slice",
kv: attribute.StringSlice("strings", []string{"one", "two"}),
want: `{"strings":["one","two"]}`,
},
{
name: "empty string slice",
kv: attribute.StringSlice("strings", []string{}),
want: `{"strings":[]}`,
},
{
name: "byte slice",
kv: attribute.ByteSlice("bytes", []byte("one")),
want: `{"bytes":"b25l"}`,
},
{
name: "float64 slice",
kv: attribute.Float64Slice("floats", []float64{1.5, -2.25}),
want: `{"floats":[1.5,-2.25]}`,
},
{
name: "empty float64 slice",
kv: attribute.Float64Slice("floats", []float64{}),
want: `{"floats":[]}`,
},
{
name: "float64 NaN",
kv: attribute.Float64("float", math.NaN()),
want: `{"float":"NaN"}`,
},
{
name: "float64 Infinity",
kv: attribute.Float64("float", math.Inf(1)),
want: `{"float":"Infinity"}`,
},
{
name: "float64 negative Infinity",
kv: attribute.Float64("float", math.Inf(-1)),
want: `{"float":"-Infinity"}`,
},
{
name: "value slice",
kv: attribute.Slice(
"slice",
attribute.StringValue("one"),
attribute.IntValue(2),
attribute.BoolValue(false),
),
want: `{"slice":["one",2,false]}`,
},
{
name: "empty value slice",
kv: attribute.Slice("slice"),
want: `{"slice":[]}`,
},
{
name: "quoted key",
kv: attribute.String(`http.request.method:raw`, "GET"),
want: `{"http.request.method:raw":"GET"}`,
},
{
name: "empty value",
kv: attribute.KeyValue{Key: "empty"},
want: `{"empty":null}`,
},
} {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, tc.kv.String())
})
}
}

func TestIncorrectCast(t *testing.T) {
testCases := []struct {
name string
Expand Down
Loading
Loading