From cb49015e987fd6dfb3d63623d441683bd790e488 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Tue, 14 Apr 2026 23:51:28 +0200 Subject: [PATCH 01/11] attribute: add String method for KeyValue --- CHANGELOG.md | 1 + attribute/kv.go | 9 +++++++++ attribute/kv_test.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad589817f10..f449436f655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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`. (#8146) - Add `Error` field on `Record` type in `go.opentelemetry.io/otel/log/logtest`. (#8148) - Add experimental support for splitting metric data across multiple batches in `go.opentelemetry.io/otel/sdk/metric`. Set `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=` to enable for all periodic readers. diff --git a/attribute/kv.go b/attribute/kv.go index 736c135cba5..3457b59e53a 100644 --- a/attribute/kv.go +++ b/attribute/kv.go @@ -13,6 +13,15 @@ type KeyValue struct { Value Value } +// String returns key-value pair as a string, formatted like "key:value". +// +// Value is formatted using the [OpenTelemetry AnyValue representation for non-OTLP protocols] rules. +// +// [OpenTelemetry AnyValue representation for non-OTLP protocols]: https://opentelemetry.io/docs/specs/otel/common/#anyvalue-representation-for-non-otlp-protocols +func (kv KeyValue) String() string { + return fmt.Sprintf("%s:%s", kv.Key, kv.Value) +} + // Valid reports whether kv is a valid OpenTelemetry attribute. func (kv KeyValue) Valid() bool { return kv.Key.Defined() diff --git a/attribute/kv_test.go b/attribute/kv_test.go index fb61676ce28..5cf4880e3fd 100644 --- a/attribute/kv_test.go +++ b/attribute/kv_test.go @@ -136,6 +136,39 @@ func TestKeyValueValid(t *testing.T) { } } +func TestKeyValueString(t *testing.T) { + for _, tc := range []struct { + name string + kv attribute.KeyValue + want string + }{ + { + name: "bool", + kv: attribute.Bool("bool", true), + want: "bool:true", + }, + { + name: "string slice", + kv: attribute.StringSlice("strings", []string{"one", "two"}), + want: `strings:["one","two"]`, + }, + { + name: "byte slice", + kv: attribute.ByteSlice("bytes", []byte("one")), + want: "bytes:b25l", + }, + { + name: "empty value", + kv: attribute.KeyValue{Key: "empty"}, + want: "empty:", + }, + } { + 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 From d3afbea844ca660abfd3594b68e1a5e086f264e5 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Tue, 14 Apr 2026 23:52:24 +0200 Subject: [PATCH 02/11] PR number --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f449436f655..e24bf34b586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +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`. (#8146) +- Add `String` method for `KeyValue` type in `go.opentelemetry.io/otel/attribute`. (#8205) - Add `Error` field on `Record` type in `go.opentelemetry.io/otel/log/logtest`. (#8148) - Add experimental support for splitting metric data across multiple batches in `go.opentelemetry.io/otel/sdk/metric`. Set `OTEL_GO_X_METRIC_EXPORT_BATCH_SIZE=` to enable for all periodic readers. From b2513accf55b4fb293367e22bc73951042b9883e Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Wed, 15 Apr 2026 12:03:54 +0200 Subject: [PATCH 03/11] KeyValue string representation is not stable --- attribute/kv.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/attribute/kv.go b/attribute/kv.go index 3457b59e53a..bd811f14aee 100644 --- a/attribute/kv.go +++ b/attribute/kv.go @@ -17,6 +17,9 @@ type KeyValue struct { // // Value is formatted using the [OpenTelemetry AnyValue representation for non-OTLP protocols] rules. // +// The returned string is meant for debugging; +// the string representation is not stable. +// // [OpenTelemetry AnyValue representation for non-OTLP protocols]: https://opentelemetry.io/docs/specs/otel/common/#anyvalue-representation-for-non-otlp-protocols func (kv KeyValue) String() string { return fmt.Sprintf("%s:%s", kv.Key, kv.Value) From e644e0a948c6ec24e1a685cd099d5d0fd6838213 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Wed, 15 Apr 2026 14:26:29 +0200 Subject: [PATCH 04/11] KeyValue.String as JSON object --- attribute/kv.go | 16 +++++++++++---- attribute/kv_test.go | 24 ++++++++++++++++++---- attribute/value.go | 48 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/attribute/kv.go b/attribute/kv.go index bd811f14aee..176adefe4bd 100644 --- a/attribute/kv.go +++ b/attribute/kv.go @@ -5,6 +5,7 @@ package attribute // import "go.opentelemetry.io/otel/attribute" import ( "fmt" + "strings" ) // KeyValue holds a key and value pair. @@ -13,16 +14,23 @@ type KeyValue struct { Value Value } -// String returns key-value pair as a string, formatted like "key:value". +// String returns a string representation of kv using the +// [OpenTelemetry Attribute representation for non-OTLP] rules. // -// Value is formatted using the [OpenTelemetry AnyValue representation for non-OTLP protocols] 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 AnyValue representation for non-OTLP protocols]: https://opentelemetry.io/docs/specs/otel/common/#anyvalue-representation-for-non-otlp-protocols +// [OpenTelemetry Attribute representation for non-OTLP]: https://opentelemetry.io/docs/specs/otel/common/#attribute-representation-for-non-otlp func (kv KeyValue) String() string { - return fmt.Sprintf("%s:%s", kv.Key, kv.Value) + var b strings.Builder + _ = 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. diff --git a/attribute/kv_test.go b/attribute/kv_test.go index 5cf4880e3fd..35578f2b464 100644 --- a/attribute/kv_test.go +++ b/attribute/kv_test.go @@ -4,6 +4,7 @@ package attribute_test import ( + "math" "testing" "github.com/google/go-cmp/cmp" @@ -145,22 +146,37 @@ func TestKeyValueString(t *testing.T) { { name: "bool", kv: attribute.Bool("bool", true), - want: "bool:true", + want: `{"bool":true}`, + }, + { + 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"]`, + want: `{"strings":["one","two"]}`, }, { name: "byte slice", kv: attribute.ByteSlice("bytes", []byte("one")), - want: "bytes:b25l", + want: `{"bytes":"b25l"}`, + }, + { + name: "float64 NaN", + kv: attribute.Float64("float", math.NaN()), + want: `{"float":"NaN"}`, + }, + { + 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:", + want: `{"empty":null}`, }, } { t.Run(tc.name, func(t *testing.T) { diff --git a/attribute/value.go b/attribute/value.go index 146f266eff5..baebd6e13f3 100644 --- a/attribute/value.go +++ b/attribute/value.go @@ -607,6 +607,54 @@ func formatStringSliceReflect(v any) string { return b.String() } +func appendJSONValue(dst *strings.Builder, v Value) { + switch v.Type() { + case BOOL: + if v.AsBool() { + _, _ = dst.WriteString("true") + } else { + _, _ = dst.WriteString("false") + } + case BOOLSLICE: + _, _ = dst.WriteString(formatBoolSliceValue(v.slice)) + case INT64: + var buf [int64ArrayElemMaxLen]byte + out := strconv.AppendInt(buf[:0], v.AsInt64(), 10) + _, _ = dst.Write(out) + case INT64SLICE: + _, _ = dst.WriteString(formatInt64SliceValue(v.slice)) + case FLOAT64: + appendJSONFloat64(dst, v.AsFloat64()) + case FLOAT64SLICE: + _, _ = dst.WriteString(formatFloat64SliceValue(v.slice)) + case STRING: + appendJSONString(dst, v.stringly) + case STRINGSLICE: + _, _ = dst.WriteString(formatStringSliceValue(v.slice)) + case BYTESLICE: + appendJSONString(dst, base64.StdEncoding.EncodeToString(v.asByteSlice())) + case EMPTY: + _, _ = dst.WriteString("null") + default: + appendJSONString(dst, "unknown") + } +} + +func appendJSONFloat64(dst *strings.Builder, v float64) { + switch { + case math.IsNaN(v): + _, _ = dst.WriteString(`"NaN"`) + case math.IsInf(v, 1): + _, _ = dst.WriteString(`"Infinity"`) + case math.IsInf(v, -1): + _, _ = dst.WriteString(`"-Infinity"`) + default: + var buf [float64ArrayElemMaxLen]byte + out := strconv.AppendFloat(buf[:0], v, 'g', -1, 64) + _, _ = dst.Write(out) + } +} + // appendJSONString appends s to dst as a JSON string literal. // // This is adapted from the Go standard library's encoding/json From 2aad2e864ef18fe4c5a95446b640efe70e40d8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 20 Apr 2026 11:04:43 +0200 Subject: [PATCH 05/11] Update attribute/value.go --- attribute/value.go | 48 ---------------------------------------------- 1 file changed, 48 deletions(-) diff --git a/attribute/value.go b/attribute/value.go index baebd6e13f3..146f266eff5 100644 --- a/attribute/value.go +++ b/attribute/value.go @@ -607,54 +607,6 @@ func formatStringSliceReflect(v any) string { return b.String() } -func appendJSONValue(dst *strings.Builder, v Value) { - switch v.Type() { - case BOOL: - if v.AsBool() { - _, _ = dst.WriteString("true") - } else { - _, _ = dst.WriteString("false") - } - case BOOLSLICE: - _, _ = dst.WriteString(formatBoolSliceValue(v.slice)) - case INT64: - var buf [int64ArrayElemMaxLen]byte - out := strconv.AppendInt(buf[:0], v.AsInt64(), 10) - _, _ = dst.Write(out) - case INT64SLICE: - _, _ = dst.WriteString(formatInt64SliceValue(v.slice)) - case FLOAT64: - appendJSONFloat64(dst, v.AsFloat64()) - case FLOAT64SLICE: - _, _ = dst.WriteString(formatFloat64SliceValue(v.slice)) - case STRING: - appendJSONString(dst, v.stringly) - case STRINGSLICE: - _, _ = dst.WriteString(formatStringSliceValue(v.slice)) - case BYTESLICE: - appendJSONString(dst, base64.StdEncoding.EncodeToString(v.asByteSlice())) - case EMPTY: - _, _ = dst.WriteString("null") - default: - appendJSONString(dst, "unknown") - } -} - -func appendJSONFloat64(dst *strings.Builder, v float64) { - switch { - case math.IsNaN(v): - _, _ = dst.WriteString(`"NaN"`) - case math.IsInf(v, 1): - _, _ = dst.WriteString(`"Infinity"`) - case math.IsInf(v, -1): - _, _ = dst.WriteString(`"-Infinity"`) - default: - var buf [float64ArrayElemMaxLen]byte - out := strconv.AppendFloat(buf[:0], v, 'g', -1, 64) - _, _ = dst.Write(out) - } -} - // appendJSONString appends s to dst as a JSON string literal. // // This is adapted from the Go standard library's encoding/json From 3361ab468bb1defb568d4ed70164ecc916d84dff Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 20 Apr 2026 20:35:40 +0200 Subject: [PATCH 06/11] add KeyValue.String benchmarks --- attribute/benchmark_test.go | 47 +++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/attribute/benchmark_test.go b/attribute/benchmark_test.go index 5710e82b03f..24cd062f2b4 100644 --- a/attribute/benchmark_test.go +++ b/attribute/benchmark_test.go @@ -4,6 +4,7 @@ package attribute_test import ( + "fmt" "math" "testing" @@ -36,15 +37,21 @@ func benchmarkEmit(kv attribute.KeyValue) func(*testing.B) { } } -func benchmarkString(kv attribute.KeyValue) func(*testing.B) { +func benchmarkStringer(s fmt.Stringer) func(*testing.B) { return func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - outStr = kv.Value.String() + outStr = s.String() } } } +func runStringBenchmarks(b *testing.B, kv attribute.KeyValue) { + b.Run("String", benchmarkStringer(kv.Value)) + b.Run("KeyValueString", benchmarkStringer(kv)) + b.Run("Emit", benchmarkEmit(kv)) +} + func BenchmarkBool(b *testing.B) { k, v := "bool", true kv := attribute.Bool(k, v) @@ -67,8 +74,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) { @@ -101,8 +107,7 @@ func BenchmarkBoolSlice(b *testing.B) { outBoolSlice = kv.Value.AsBoolSlice() } }) - b.Run("String", benchmarkString(kv)) - b.Run("Emit", benchmarkEmit(kv)) + runStringBenchmarks(b, kv) }) } } @@ -123,8 +128,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) { @@ -151,8 +155,7 @@ func BenchmarkIntSlice(b *testing.B) { outKV = attribute.IntSlice(k, v) } }) - b.Run("String", benchmarkString(kv)) - b.Run("Emit", benchmarkEmit(kv)) + runStringBenchmarks(b, kv) }) } } @@ -179,8 +182,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) { @@ -213,8 +215,7 @@ func BenchmarkInt64Slice(b *testing.B) { outInt64Slice = kv.Value.AsInt64Slice() } }) - b.Run("String", benchmarkString(kv)) - b.Run("Emit", benchmarkEmit(kv)) + runStringBenchmarks(b, kv) }) } } @@ -241,8 +242,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) { @@ -275,8 +275,7 @@ func BenchmarkFloat64Slice(b *testing.B) { outFloat64Slice = kv.Value.AsFloat64Slice() } }) - b.Run("String", benchmarkString(kv)) - b.Run("Emit", benchmarkEmit(kv)) + runStringBenchmarks(b, kv) }) } } @@ -303,8 +302,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) { @@ -337,8 +335,7 @@ func BenchmarkStringSlice(b *testing.B) { outStrSlice = kv.Value.AsStringSlice() } }) - b.Run("String", benchmarkString(kv)) - b.Run("Emit", benchmarkEmit(kv)) + runStringBenchmarks(b, kv) }) } } @@ -389,8 +386,7 @@ func BenchmarkSlice(b *testing.B) { outValueSlice = kv.Value.AsSlice() } }) - b.Run("String", benchmarkString(kv)) - b.Run("Emit", benchmarkEmit(kv)) + runStringBenchmarks(b, kv) }) } } @@ -420,8 +416,7 @@ func BenchmarkByteSlice(b *testing.B) { } }) - b.Run("String", benchmarkString(kv)) - b.Run("Emit", benchmarkEmit(kv)) + runStringBenchmarks(b, kv) } func BenchmarkSetEquals(b *testing.B) { From 27159be65646255bbbedc8fb4c1c481822623207 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 20 Apr 2026 21:00:13 +0200 Subject: [PATCH 07/11] Optimize string builder capacity estimates for KeyValue and value slices --- attribute/kv.go | 12 ++++++++++++ attribute/value.go | 10 ++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/attribute/kv.go b/attribute/kv.go index 1c31ecf0b93..3462053a7fe 100644 --- a/attribute/kv.go +++ b/attribute/kv.go @@ -24,7 +24,19 @@ type KeyValue struct { // // [OpenTelemetry Attribute representation for non-OTLP]: https://opentelemetry.io/docs/specs/otel/common/#attribute-representation-for-non-otlp func (kv KeyValue) String() string { + const jsonSyntaxOverhead = len(`{"":""}`) + var b strings.Builder + // Estimate the length of the resulting string to minimize allocations. + if kv.Value.Type() == STRING { + // For string values, we need to account for the quotes and potential escaping. + b.Grow(len(kv.Key) + len(kv.Value.stringly) + jsonSyntaxOverhead) + } else { + // For non-string values, use the smallObjectLen estimate. + // This can be improved in the future by adding specific estimates for different value types. + b.Grow(len(kv.Key) + smallObjectLen + jsonSyntaxOverhead) + } + _ = b.WriteByte('{') appendJSONString(&b, string(kv.Key)) _ = b.WriteByte(':') diff --git a/attribute/value.go b/attribute/value.go index 0529fefae23..5f8045cd9e4 100644 --- a/attribute/value.go +++ b/attribute/value.go @@ -412,6 +412,8 @@ const ( int64ArrayElemMaxLen = len("-9223372036854775808") float64ArrayElemMaxLen = len("-1.7976931348623157e+308") commaLen = len(",") + // estimate for small JSON objects to help with Builder capacity calculations. + smallObjectLen = len(`{"key":"value"}`) ) func sliceValue(v []Value) any { @@ -827,8 +829,8 @@ func appendValueSliceValue(dst *strings.Builder, v any) { } func appendValueSlice(dst *strings.Builder, vals []Value) { - // Estimate 10 bytes per value for small values and commas. - dst.Grow(jsonArrayBracketsLen + len(vals)*commaLen + len(vals)*10) + // Estimate for small values and commas. + dst.Grow(jsonArrayBracketsLen + len(vals)*commaLen + len(vals)*smallObjectLen) _ = dst.WriteByte('[') for i, val := range vals { if i > 0 { @@ -840,8 +842,8 @@ func appendValueSlice(dst *strings.Builder, vals []Value) { } func appendValueSliceReflect(dst *strings.Builder, rv reflect.Value) { - // Estimate 10 bytes per value for small values and commas. - dst.Grow(jsonArrayBracketsLen + rv.Len()*commaLen + rv.Len()*10) + // Estimate for small values and commas. + dst.Grow(jsonArrayBracketsLen + rv.Len()*commaLen + rv.Len()*smallObjectLen) _ = dst.WriteByte('[') for i := 0; i < rv.Len(); i++ { if i > 0 { From 1ca0a230756b196719a3d32bb997913f8c82c084 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 20 Apr 2026 21:15:31 +0200 Subject: [PATCH 08/11] enhance KeyValue.String method to improve string length estimation for various types --- attribute/kv.go | 72 +++++++++++++++++++++++++++++++++++------ attribute/kv_test.go | 77 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 139 insertions(+), 10 deletions(-) diff --git a/attribute/kv.go b/attribute/kv.go index 3462053a7fe..039a0607d20 100644 --- a/attribute/kv.go +++ b/attribute/kv.go @@ -4,7 +4,11 @@ package attribute // import "go.opentelemetry.io/otel/attribute" import ( + "encoding/base64" "fmt" + "math" + "reflect" + "strconv" "strings" ) @@ -24,18 +28,68 @@ type KeyValue struct { // // [OpenTelemetry Attribute representation for non-OTLP]: https://opentelemetry.io/docs/specs/otel/common/#attribute-representation-for-non-otlp func (kv KeyValue) String() string { - const jsonSyntaxOverhead = len(`{"":""}`) + const jsonObjectSyntaxLen = len(`{"":}`) var b strings.Builder - // Estimate the length of the resulting string to minimize allocations. - if kv.Value.Type() == STRING { - // For string values, we need to account for the quotes and potential escaping. - b.Grow(len(kv.Key) + len(kv.Value.stringly) + jsonSyntaxOverhead) - } else { - // For non-string values, use the smallObjectLen estimate. - // This can be improved in the future by adding specific estimates for different value types. - b.Grow(len(kv.Key) + smallObjectLen + jsonSyntaxOverhead) + 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) + 2 + 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)) + 2 + case SLICE: + n += jsonArrayBracketsLen + if l := reflect.ValueOf(kv.Value.slice).Len(); l > 0 { + n += l*smallObjectLen + (l-1)*commaLen + } + case EMPTY: + n += len("null") + default: + n += len(`"unknown"`) } + b.Grow(n) _ = b.WriteByte('{') appendJSONString(&b, string(kv.Key)) diff --git a/attribute/kv_test.go b/attribute/kv_test.go index cfe30f132eb..779eea6eadd 100644 --- a/attribute/kv_test.go +++ b/attribute/kv_test.go @@ -157,10 +157,45 @@ func TestKeyValueString(t *testing.T) { want string }{ { - name: "bool", + 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"`), @@ -171,16 +206,56 @@ func TestKeyValueString(t *testing.T) { 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"), From 3b3cc654ef69bfa65a74d3b409cffabe67b422c7 Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 20 Apr 2026 21:19:02 +0200 Subject: [PATCH 09/11] refactor: add quotesLen const --- attribute/kv.go | 4 ++-- attribute/value.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/attribute/kv.go b/attribute/kv.go index 039a0607d20..ba29e89dcf4 100644 --- a/attribute/kv.go +++ b/attribute/kv.go @@ -71,14 +71,14 @@ func (kv KeyValue) String() string { n += l*float64ArrayElemMaxLen + (l-1)*commaLen } case STRING: - n += len(kv.Value.stringly) + 2 + 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)) + 2 + n += base64.StdEncoding.EncodedLen(len(kv.Value.stringly)) + quotesLen case SLICE: n += jsonArrayBracketsLen if l := reflect.ValueOf(kv.Value.slice).Len(); l > 0 { diff --git a/attribute/value.go b/attribute/value.go index 5f8045cd9e4..eff5d4d75ad 100644 --- a/attribute/value.go +++ b/attribute/value.go @@ -412,6 +412,7 @@ const ( int64ArrayElemMaxLen = len("-9223372036854775808") float64ArrayElemMaxLen = len("-1.7976931348623157e+308") commaLen = len(",") + quotesLen = len(`""`) // estimate for small JSON objects to help with Builder capacity calculations. smallObjectLen = len(`{"key":"value"}`) ) From cfe027f5b151656477175dbbe23de5d358f6107a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Paj=C4=85k?= Date: Mon, 20 Apr 2026 23:06:56 +0200 Subject: [PATCH 10/11] Update attribute/value.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- attribute/value.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attribute/value.go b/attribute/value.go index eff5d4d75ad..07b7c7a7ea0 100644 --- a/attribute/value.go +++ b/attribute/value.go @@ -413,7 +413,7 @@ const ( float64ArrayElemMaxLen = len("-1.7976931348623157e+308") commaLen = len(",") quotesLen = len(`""`) - // estimate for small JSON objects to help with Builder capacity calculations. + // estimate for a small JSON value to help with Builder capacity calculations. smallObjectLen = len(`{"key":"value"}`) ) From 0eb35d4286730313cb7ff89fc64c239d600f6b4d Mon Sep 17 00:00:00 2001 From: Robert Pajak Date: Mon, 20 Apr 2026 23:10:05 +0200 Subject: [PATCH 11/11] refactor: update benchmark functions to use Value and KeyValue String methods --- attribute/benchmark_test.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/attribute/benchmark_test.go b/attribute/benchmark_test.go index 24cd062f2b4..57caf6635ff 100644 --- a/attribute/benchmark_test.go +++ b/attribute/benchmark_test.go @@ -4,7 +4,6 @@ package attribute_test import ( - "fmt" "math" "testing" @@ -37,18 +36,27 @@ func benchmarkEmit(kv attribute.KeyValue) func(*testing.B) { } } -func benchmarkStringer(s fmt.Stringer) 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 = s.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", benchmarkStringer(kv.Value)) - b.Run("KeyValueString", benchmarkStringer(kv)) + b.Run("String", benchmarkValueString(kv.Value)) + b.Run("KeyValueString", benchmarkKeyValueString(kv)) b.Run("Emit", benchmarkEmit(kv)) }