Repository: open-telemetry/opentelemetry-go
Pull request #5117 (2024-03-29 09:10:27)

log: Add String method to Value and KeyValue
Fixes https://github.com/open-telemetry/opentelemetry-go/issues/4952

Implementation based on: https://pkg.go.dev/log/slog#Value.String and https://pkg.go.dev/log/slog#Attr.String.

Comment that it is for debugging purpose only inspired by https://pkg.go.dev/time#Time.String

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/pull/5117?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) Report
Attention: Patch coverage is `90.90909%` with `2 lines` in your changes are missing coverage. Please review.
> Project coverage is 83.8%. Comparing base [(`ba5d126`)](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/commit/ba5d1268082fcc4ccb8fa967eb5133fa23e4f2ba?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) to head [(`67ca630`)](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/pull/5117?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry).

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/pull/5117/graphs/tree.svg?width=650&height=150&src=pr&token=8efTmh4kvf&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/pull/5117?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)

```diff
@@           Coverage Diff           @@
##            main   #5117     +/-   ##
=======================================
- Coverage   83.8%   83.8%   -0.1%     
=======================================
  Files        248     248             
  Lines      16230   16252     +22     
=======================================
+ Hits       13602   13620     +18     
- Misses      2340    2344      +4     
  Partials     288     288             
```


| [Files](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/pull/5117?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) | Coverage Δ | |
|---|---|---|
| [log/keyvalue.go](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/pull/5117?src=pr&el=tree&filepath=log%2Fkeyvalue.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry#diff-bG9nL2tleXZhbHVlLmdv) | `96.7% <90.9%> (-1.0%)` | :arrow_down: |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/pull/5117/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry)

</details>

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6a9a9c1..3a5af468 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 - Add support for `Summary` metrics in the `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp` and `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc` exporters. (#5100)
 - Add `otel.scope.name` and `otel.scope.version` tags to spans exported by `go.opentelemetry.io/otel/exporters/zipkin`. (#5108)
 - Add support for `AddLink` to `go.opentelemetry.io/otel/bridge/opencensus`. (#5116)
+- Add `String` method to `Value` and `KeyValue` in `go.opentelemetry.io/otel/log`. (#5117)
 
 ### Changed
 
diff --git a/log/keyvalue.go b/log/keyvalue.go
index da3d55c4..10920d21 100644
--- a/log/keyvalue.go
+++ b/log/keyvalue.go
@@ -8,8 +8,10 @@ package log // import "go.opentelemetry.io/otel/log"
 import (
 	"bytes"
 	"errors"
+	"fmt"
 	"math"
 	"slices"
+	"strconv"
 	"unsafe"
 
 	"go.opentelemetry.io/otel/internal/global"
@@ -265,6 +267,39 @@ func (v Value) Equal(w Value) bool {
 	}
 }
 
+// String returns Value's value as a string, formatted like [fmt.Sprint].
+//
+// The returned string is meant for debugging;
+// the string representation is not stable.
+func (v Value) String() string {
+	switch v.Kind() {
+	case KindString:
+		return v.asString()
+	case KindInt64:
+		return strconv.FormatInt(int64(v.num), 10)
+	case KindFloat64:
+		return strconv.FormatFloat(v.asFloat64(), 'g', -1, 64)
+	case KindBool:
+		return strconv.FormatBool(v.asBool())
+	case KindBytes:
+		return fmt.Sprint(v.asBytes())
+	case KindMap:
+		return fmt.Sprint(v.asMap())
+	case KindSlice:
+		return fmt.Sprint(v.asSlice())
+	case KindEmpty:
+		return "<nil>"
+	default:
+		// Try to handle this as gracefully as possible.
+		//
+		// Don't panic here. The goal here is to have developers find this
+		// first if a slog.Kind is is not handled. It is
+		// preferable to have user's open issue asking why their attributes
+		// have a "unhandled: " prefix than say that their code is panicking.
+		return fmt.Sprintf("<unhandled log.Kind: %s>", v.Kind())
+	}
+}
+
 // A KeyValue is a key-value pair used to represent a log attribute (a
 // superset of [go.opentelemetry.io/otel/attribute.KeyValue]) and map item.
 type KeyValue struct {
@@ -321,3 +356,11 @@ func Map(key string, value ...KeyValue) KeyValue {
 func Empty(key string) KeyValue {
 	return KeyValue{key, Value{}}
 }
+
+// String returns key-value pair as a string, formatted like "key:value".
+//
+// The returned string is meant for debugging;
+// the string representation is not stable.
+func (a KeyValue) String() string {
+	return fmt.Sprintf("%s:%s", a.Key, a.Value)
+}


Test patch:
diff --git a/log/keyvalue_test.go b/log/keyvalue_test.go
index f7c6602d..2f021116 100644
--- a/log/keyvalue_test.go
+++ b/log/keyvalue_test.go
@@ -264,6 +264,25 @@ func TestEmpty(t *testing.T) {
 	t.Run("AsMap", testErrKind(v.AsMap, "AsMap", k))
 }
 
+func TestValueString(t *testing.T) {
+	for _, test := range []struct {
+		v    log.Value
+		want string
+	}{
+		{log.Int64Value(-3), "-3"},
+		{log.Float64Value(.15), "0.15"},
+		{log.BoolValue(true), "true"},
+		{log.StringValue("foo"), "foo"},
+		{log.BytesValue([]byte{2, 4, 6}), "[2 4 6]"},
+		{log.SliceValue(log.IntValue(3), log.StringValue("foo")), "[3 foo]"},
+		{log.MapValue(log.Int("a", 1), log.Bool("b", true)), "[a:1 b:true]"},
+		{log.Value{}, "<nil>"},
+	} {
+		got := test.v.String()
+		assert.Equal(t, test.want, got)
+	}
+}
+
 type logSink struct {
 	logr.LogSink