diff --git a/CHANGELOG.md b/CHANGELOG.md index 599d59cd130..30ada7a39e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- Add `ValueFromAttribute` and `KeyValueFromAttribute` in `go.opentelemetry.io/otel/log`. (#6180) + diff --git a/log/keyvalue.go b/log/keyvalue.go index 2e1d30c1b88..73e4e7dca10 100644 --- a/log/keyvalue.go +++ b/log/keyvalue.go @@ -15,6 +15,7 @@ import ( "strconv" "unsafe" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/internal/global" ) @@ -385,3 +386,58 @@ func Empty(key string) KeyValue { func (a KeyValue) String() string { return fmt.Sprintf("%s:%s", a.Key, a.Value) } + +// ValueFromAttribute converts [attribute.Value] to [Value]. +func ValueFromAttribute(value attribute.Value) Value { + switch value.Type() { + case attribute.INVALID: + return Value{} + case attribute.BOOL: + return BoolValue(value.AsBool()) + case attribute.BOOLSLICE: + val := value.AsBoolSlice() + res := make([]Value, 0, len(val)) + for _, v := range val { + res = append(res, BoolValue(v)) + } + return SliceValue(res...) + case attribute.INT64: + return Int64Value(value.AsInt64()) + case attribute.INT64SLICE: + val := value.AsInt64Slice() + res := make([]Value, 0, len(val)) + for _, v := range val { + res = append(res, Int64Value(v)) + } + return SliceValue(res...) + case attribute.FLOAT64: + return Float64Value(value.AsFloat64()) + case attribute.FLOAT64SLICE: + val := value.AsFloat64Slice() + res := make([]Value, 0, len(val)) + for _, v := range val { + res = append(res, Float64Value(v)) + } + return SliceValue(res...) + case attribute.STRING: + return StringValue(value.AsString()) + case attribute.STRINGSLICE: + val := value.AsStringSlice() + res := make([]Value, 0, len(val)) + for _, v := range val { + res = append(res, StringValue(v)) + } + return SliceValue(res...) + } + // This code should never be reached + // as log attributes are a superset of standard attributes. + panic("unknown attribute type") +} + +// KeyValueFromAttribute converts [attribute.KeyValue] to [KeyValue]. +func KeyValueFromAttribute(kv attribute.KeyValue) KeyValue { + return KeyValue{ + Key: string(kv.Key), + Value: ValueFromAttribute(kv.Value), + } +} diff --git a/log/keyvalue_bench_test.go b/log/keyvalue_bench_test.go index 1cfd839d045..ac925bac2c2 100644 --- a/log/keyvalue_bench_test.go +++ b/log/keyvalue_bench_test.go @@ -6,6 +6,7 @@ package log_test import ( "testing" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/log" ) @@ -232,3 +233,55 @@ func BenchmarkValueEqual(b *testing.B) { } } } + +func BenchmarkKeyValueFromAttribute(b *testing.B) { + testCases := []struct { + desc string + kv attribute.KeyValue + }{ + { + desc: "Empty", + kv: attribute.KeyValue{}, + }, + { + desc: "Bool", + kv: attribute.Bool("k", true), + }, + { + desc: "BoolSlice", + kv: attribute.BoolSlice("k", []bool{true, false}), + }, + { + desc: "Int64", + kv: attribute.Int64("k", 13), + }, + { + desc: "Int64Slice", + kv: attribute.Int64Slice("k", []int64{12, 34}), + }, + { + desc: "Float64", + kv: attribute.Float64("k", 3.14), + }, + { + desc: "Float64Slice", + kv: attribute.Float64Slice("k", []float64{3.14, 2.72}), + }, + { + desc: "String", + kv: attribute.String("k", "foo"), + }, + { + desc: "StringSlice", + kv: attribute.StringSlice("k", []string{"foo", "bar"}), + }, + } + for _, tc := range testCases { + b.Run(tc.desc, func(b *testing.B) { + b.ReportAllocs() + for range b.N { + outKV = log.KeyValueFromAttribute(tc.kv) + } + }) + } +} diff --git a/log/keyvalue_test.go b/log/keyvalue_test.go index 4d5afcfa328..01e239d356d 100644 --- a/log/keyvalue_test.go +++ b/log/keyvalue_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/log" ) @@ -309,6 +310,130 @@ func TestValueString(t *testing.T) { } } +func TestValueFromAttribute(t *testing.T) { + testCases := []struct { + desc string + v attribute.Value + want log.Value + }{ + { + desc: "Empty", + v: attribute.Value{}, + want: log.Value{}, + }, + { + desc: "Bool", + v: attribute.BoolValue(true), + want: log.BoolValue(true), + }, + { + desc: "BoolSlice", + v: attribute.BoolSliceValue([]bool{true, false}), + want: log.SliceValue(log.BoolValue(true), log.BoolValue(false)), + }, + { + desc: "Int64", + v: attribute.Int64Value(13), + want: log.Int64Value(13), + }, + { + desc: "Int64Slice", + v: attribute.Int64SliceValue([]int64{12, 34}), + want: log.SliceValue(log.Int64Value(12), log.Int64Value(34)), + }, + { + desc: "Float64", + v: attribute.Float64Value(3.14), + want: log.Float64Value(3.14), + }, + { + desc: "Float64Slice", + v: attribute.Float64SliceValue([]float64{3.14, 2.72}), + want: log.SliceValue(log.Float64Value(3.14), log.Float64Value(2.72)), + }, + { + desc: "String", + v: attribute.StringValue("foo"), + want: log.StringValue("foo"), + }, + { + desc: "StringSlice", + v: attribute.StringSliceValue([]string{"foo", "bar"}), + want: log.SliceValue(log.StringValue("foo"), log.StringValue("bar")), + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + got := log.ValueFromAttribute(tc.v) + if !got.Equal(tc.want) { + t.Errorf("got: %v; want:%v", got, tc.want) + } + }) + } +} + +func TestKeyValueFromAttribute(t *testing.T) { + testCases := []struct { + desc string + kv attribute.KeyValue + want log.KeyValue + }{ + { + desc: "Empty", + kv: attribute.KeyValue{}, + want: log.KeyValue{}, + }, + { + desc: "Bool", + kv: attribute.Bool("k", true), + want: log.Bool("k", true), + }, + { + desc: "BoolSlice", + kv: attribute.BoolSlice("k", []bool{true, false}), + want: log.Slice("k", log.BoolValue(true), log.BoolValue(false)), + }, + { + desc: "Int64", + kv: attribute.Int64("k", 13), + want: log.Int64("k", 13), + }, + { + desc: "Int64Slice", + kv: attribute.Int64Slice("k", []int64{12, 34}), + want: log.Slice("k", log.Int64Value(12), log.Int64Value(34)), + }, + { + desc: "Float64", + kv: attribute.Float64("k", 3.14), + want: log.Float64("k", 3.14), + }, + { + desc: "Float64Slice", + kv: attribute.Float64Slice("k", []float64{3.14, 2.72}), + want: log.Slice("k", log.Float64Value(3.14), log.Float64Value(2.72)), + }, + { + desc: "String", + kv: attribute.String("k", "foo"), + want: log.String("k", "foo"), + }, + { + desc: "StringSlice", + kv: attribute.StringSlice("k", []string{"foo", "bar"}), + want: log.Slice("k", log.StringValue("foo"), log.StringValue("bar")), + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + got := log.KeyValueFromAttribute(tc.kv) + if !got.Equal(tc.want) { + t.Errorf("got: %v; want:%v", got, tc.want) + } + }) + } +} + type logSink struct { logr.LogSink