Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -16,6 +16,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Add `go.opentelemetry.io/otel/semconv/v1.39.0` package.
The package contains semantic conventions from the `v1.39.0` version of the OpenTelemetry Semantic Conventions.
See the [migration documentation](./semconv/v1.39.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.38.0.`(#7783)
- Add `String` method to `attribute.Value` and `KeyValue`. (#7812)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Add `String` method to `attribute.Value` and `KeyValue`. (#7812)
- Add `String` method to `attribute.Value` and `KeyValue` in `go.opentelemetry.io/otel/attribute`. (#7812)


### Changed

Expand Down
5 changes: 5 additions & 0 deletions attribute/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ func (kv KeyValue) Valid() bool {
return kv.Key.Defined() && kv.Value.Type() != INVALID
}

// String implements the Stringer interface, used when you pass an object to fmt.Println, etc.
func (kv KeyValue) String() string {
return "{" + string(kv.Key) + ":" + kv.Value.String() + "}"
}
Comment on lines +21 to +24
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// String implements the Stringer interface, used when you pass an object to fmt.Println, etc.
func (kv KeyValue) String() string {
return "{" + string(kv.Key) + ":" + kv.Value.String() + "}"
}
// 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 (kv KeyValue) String() string {
return string(kv.Key) + ":" + kv.Value.String()
}

Why? To follow prior art https://pkg.go.dev/go.opentelemetry.io/otel/log#KeyValue.String (more: #5117).


// Bool creates a KeyValue with a BOOL Value type.
func Bool(k string, v bool) KeyValue {
return Key(k).Bool(v)
Expand Down
81 changes: 81 additions & 0 deletions attribute/kv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,84 @@ func TestIncorrectCast(t *testing.T) {
})
}
}

func TestKeyValueString(t *testing.T) {
tests := []struct {
name string
kv attribute.KeyValue
want string
}{
{
name: "int positive",
kv: attribute.Int("key", 42),
want: "{key:42}",
},
{
name: "float64 negative",
kv: attribute.Float64("key", -3.14),
want: "{key:-3.14}",
},
{
name: "string simple",
kv: attribute.String("key", "value"),
want: "{key:value}",
},
{
name: "string empty",
kv: attribute.String("key", ""),
want: "{key:}",
},
{
name: "string with spaces",
kv: attribute.String("key", "hello world"),
want: "{key:hello world}",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it surprising that strings are not quoted, especially with embedded spaces like here.
But this is what Value.Emit() did, so leaving this here for comment.

},
{
name: "bool slice",
kv: attribute.BoolSlice("key", []bool{true, false, true}),
want: "{key:[true false true]}",
},
{
name: "int slice",
kv: attribute.IntSlice("key", []int{1, 2, 3}),
want: "{key:[1,2,3]}",
},
{
name: "int64 slice",
kv: attribute.Int64Slice("key", []int64{1, 2, 3}),
want: "{key:[1,2,3]}",
},
{
name: "float64 slice",
kv: attribute.Float64Slice("key", []float64{1.5, 2.5, 3.5}),
want: "{key:[1.5,2.5,3.5]}",
},
{
name: "string slice",
kv: attribute.StringSlice("key", []string{"foo", "bar"}),
want: `{key:["foo","bar"]}`,
},
{
name: "empty key",
kv: attribute.String("", "value"),
want: "{:value}",
},
{
name: "invalid/uninitialized KeyValue",
kv: attribute.KeyValue{},
want: "{:unknown}",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should print something clearer?
Originally I wanted this for debugging purposes, so exactly what is in the object may be important.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose <invalid>.

},
{
name: "key with special characters",
kv: attribute.String("key-with-dashes", "value"),
want: "{key-with-dashes:value}",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.kv.String()
assert.Equal(t, tt.want, got)
})
}
}
6 changes: 6 additions & 0 deletions attribute/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ func (v Value) AsInterface() any {
return unknownValueType{}
}

// String implements the Stringer interface, used when you pass an object to fmt.Println, etc.
// Do not confuse with AsString, which you should call if you know the value is of String type.
Comment on lines +225 to +226
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// String implements the Stringer interface, used when you pass an object to fmt.Println, etc.
// Do not confuse with AsString, which you should call if you know the value is of String type.
// 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 {
return v.Emit()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we deprecate Emit so that users migrate to the new method?

}

// Emit returns a string representation of Value's data.
func (v Value) Emit() string {
switch v.Type() {
Expand Down
101 changes: 101 additions & 0 deletions attribute/value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,104 @@ func TestAsSlice(t *testing.T) {
ss2 := kv.Value.AsStringSlice()
assert.Equal(t, ss1, ss2)
}

func TestValueString(t *testing.T) {
tests := []struct {
name string
value attribute.Value
want string
}{
{
name: "bool true",
value: attribute.BoolValue(true),
want: "true",
},
{
name: "int64 positive",
value: attribute.Int64Value(42),
want: "42",
},
{
name: "int negative",
value: attribute.IntValue(-42),
want: "-42",
},
{
name: "float64 positive",
value: attribute.Float64Value(42.5),
want: "42.5",
},
{
name: "float64 scientific notation",
value: attribute.Float64Value(1.23e-10),
want: "1.23e-10",
},
{
name: "string empty",
value: attribute.StringValue(""),
want: "",
},
{
name: "string with spaces",
value: attribute.StringValue("hello world"),
want: "hello world",
},
{
name: "string with special characters",
value: attribute.StringValue("hello\nworld\t!"),
want: "hello\nworld\t!",
},
{
name: "bool slice",
value: attribute.BoolSliceValue([]bool{true, false, true}),
want: "[true false true]",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it was surprising to me that this one has no commas, while the int, float and string slices do.

},
{
name: "bool slice empty",
value: attribute.BoolSliceValue([]bool{}),
want: "[]",
},
{
name: "int64 slice",
value: attribute.Int64SliceValue([]int64{1, -2, 3}),
want: "[1,-2,3]",
},
{
name: "int64 slice empty",
value: attribute.Int64SliceValue([]int64{}),
want: "[]",
},
{
name: "int slice",
value: attribute.IntSliceValue([]int{1, -2, 3}),
want: "[1,-2,3]",
},
{
name: "float64 slice",
value: attribute.Float64SliceValue([]float64{1.5, -2.5, 3.0}),
want: "[1.5,-2.5,3]",
},
{
name: "string slice",
value: attribute.StringSliceValue([]string{"foo", "bar", "baz"}),
want: `["foo","bar","baz"]`,
},
{
name: "string slice with empty strings",
value: attribute.StringSliceValue([]string{"", "bar", ""}),
want: `["","bar",""]`,
},
{
name: "invalid type",
value: attribute.Value{},
want: "unknown",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.value.String()
assert.Equal(t, tt.want, got)
})
}
}