Repository: open-telemetry/opentelemetry-go
Pull request #6180 (2025-01-20 11:29:27)

log: Add ValueFromAttribute and KeyValueFromAttribute 
Fixes https://github.com/open-telemetry/opentelemetry-go/issues/6158

Related spec PR: https://github.com/open-telemetry/opentelemetry-specification/pull/4373

Benchmark results:

```
goos: linux
goarch: amd64
pkg: go.opentelemetry.io/otel/log
cpu: 13th Gen Intel(R) Core(TM) i7-13800H
BenchmarkKeyValueFromAttribute/Empty-20              72029505                16.47 ns/op            0 B/op          0 allocs/op
BenchmarkKeyValueFromAttribute/Bool-20               68560222                16.99 ns/op            0 B/op          0 allocs/op
BenchmarkKeyValueFromAttribute/BoolSlice-20          14647401                76.21 ns/op           50 B/op          2 allocs/op
BenchmarkKeyValueFromAttribute/Int64-20              70737378                16.92 ns/op            0 B/op          0 allocs/op
BenchmarkKeyValueFromAttribute/Int64Slice-20         16780069                96.87 ns/op           64 B/op          2 allocs/op
BenchmarkKeyValueFromAttribute/Float64-20            59299638                16.93 ns/op            0 B/op          0 allocs/op
BenchmarkKeyValueFromAttribute/Float64Slice-20       12691222               106.2 ns/op            64 B/op          2 allocs/op
BenchmarkKeyValueFromAttribute/String-20             63837711                16.97 ns/op            0 B/op          0 allocs/op
BenchmarkKeyValueFromAttribute/StringSlice-20         9251001               114.7 ns/op            80 B/op          2 allocs/op
PASS
ok      go.opentelemetry.io/otel/log    14.776s
```

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/pull/6180?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 82.2%. Comparing base [(`e8cd4aa`)](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/commit/e8cd4aac1bb9ffa08672aa9e9c9227b2dd6c46b6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-telemetry) to head [(`dd376a4`)](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/commit/dd376a48f6a84ce7af8a9ee3298a7eec7d8c6f3c?dropdown=coverage&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/6180/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/6180?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   #6180   +/-   ##
=====================================
  Coverage   82.2%   82.2%           
=====================================
  Files        273     273           
  Lines      23674   23720   +46     
=====================================
+ Hits       19476   19520   +44     
- Misses      3851    3853    +2     
  Partials     347     347           
```

[see 2 files with indirect coverage changes](https://app.codecov.io/gh/open-telemetry/opentelemetry-go/pull/6180/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 599d59cd..30ada7a3 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)
+
 <!-- Released section -->
 <!-- Don't change this section unless doing release -->
 
diff --git a/log/keyvalue.go b/log/keyvalue.go
index 2e1d30c1..73e4e7dc 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),
+	}
+}


Test patch:
diff --git a/log/keyvalue_bench_test.go b/log/keyvalue_bench_test.go
index 1cfd839d..ac925bac 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 4d5afcfa..01e239d3 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