From 7b3382e4dcee645eb9825f12f195e4e275b38760 Mon Sep 17 00:00:00 2001 From: Tyler Yahn Date: Wed, 21 Feb 2024 13:19:41 -0800 Subject: [PATCH] log: Implement Value and KeyValue types (#4949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement `Value` and `KeyValue` * Add tests for `Value` and `KeyValue` --------- Co-authored-by: Robert PajÄ…k --- log/go.mod | 4 + log/go.sum | 5 + log/keyvalue.go | 306 ++++++++++++++++++++++++++++++++++++++----- log/keyvalue_test.go | 302 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 587 insertions(+), 30 deletions(-) create mode 100644 log/keyvalue_test.go diff --git a/log/go.mod b/log/go.mod index 2dfceaf6ed3..6a974a2ae01 100644 --- a/log/go.mod +++ b/log/go.mod @@ -3,6 +3,8 @@ module go.opentelemetry.io/otel/log go 1.20 require ( + github.com/go-logr/logr v1.4.1 + github.com/go-logr/stdr v1.2.2 github.com/stretchr/testify v1.8.4 go.opentelemetry.io/otel v1.23.1 ) @@ -10,6 +12,8 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v1.23.1 // indirect + go.opentelemetry.io/otel/trace v1.23.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/log/go.sum b/log/go.sum index a6bcd03a15e..75d8b1f55b4 100644 --- a/log/go.sum +++ b/log/go.sum @@ -1,5 +1,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/log/keyvalue.go b/log/keyvalue.go index b39c00a4b45..12a79b8b7c3 100644 --- a/log/keyvalue.go +++ b/log/keyvalue.go @@ -16,6 +16,18 @@ package log // import "go.opentelemetry.io/otel/log" +import ( + "bytes" + "errors" + "math" + "unsafe" + + "go.opentelemetry.io/otel/internal/global" +) + +// errKind is logged when a Value is decoded to an incompatible type. +var errKind = errors.New("invalid Kind") + // Kind is the kind of a [Value]. type Kind int @@ -32,70 +44,286 @@ const ( ) // A Value represents a structured log value. -type Value struct{} // TODO (#4914): implement. +type Value struct { + // Ensure forward compatibility by explicitly making this not comparable. + noCmp [0]func() //nolint: unused // This is indeed used. + + // num holds the value for Int64, Float64, and Bool. It holds the length + // for String, Bytes, Slice, Map. + num uint64 + // any holds either the KindBool, KindInt64, KindFloat64, stringptr, + // bytesptr, sliceptr, or mapptr. If KindBool, KindInt64, or KindFloat64 + // then the value of Value is in num as described above. Otherwise, it + // contains the value wrapped in the appropriate type. + any any +} + +type ( + // sliceptr represents a value in Value.any for KindString Values. + stringptr *byte + // bytesptr represents a value in Value.any for KindBytes Values. + bytesptr *byte + // sliceptr represents a value in Value.any for KindSlice Values. + sliceptr *Value + // mapptr represents a value in Value.any for KindMap Values. + mapptr *KeyValue +) // StringValue returns a new [Value] for a string. -func StringValue(v string) Value { return Value{} } // TODO (#4914): implement. +func StringValue(v string) Value { + return Value{ + num: uint64(len(v)), + any: stringptr(unsafe.StringData(v)), + } +} // IntValue returns a [Value] for an int. -func IntValue(v int) Value { return Value{} } // TODO (#4914): implement. +func IntValue(v int) Value { return Int64Value(int64(v)) } // Int64Value returns a [Value] for an int64. -func Int64Value(v int64) Value { return Value{} } // TODO (#4914): implement. +func Int64Value(v int64) Value { + return Value{num: uint64(v), any: KindInt64} +} // Float64Value returns a [Value] for a float64. -func Float64Value(v float64) Value { return Value{} } // TODO (#4914): implement. +func Float64Value(v float64) Value { + return Value{num: math.Float64bits(v), any: KindFloat64} +} // BoolValue returns a [Value] for a bool. func BoolValue(v bool) Value { //nolint:revive // Not a control flag. - // TODO (#4914): implement. - return Value{} + var n uint64 + if v { + n = 1 + } + return Value{num: n, any: KindBool} } // BytesValue returns a [Value] for a byte slice. The passed slice must not be // changed after it is passed. -func BytesValue(v []byte) Value { return Value{} } // TODO (#4914): implement. +func BytesValue(v []byte) Value { + return Value{ + num: uint64(len(v)), + any: bytesptr(unsafe.SliceData(v)), + } +} // SliceValue returns a [Value] for a slice of [Value]. The passed slice must // not be changed after it is passed. -func SliceValue(vs ...Value) Value { return Value{} } // TODO (#4914): implement. +func SliceValue(vs ...Value) Value { + return Value{ + num: uint64(len(vs)), + any: sliceptr(unsafe.SliceData(vs)), + } +} // MapValue returns a new [Value] for a slice of key-value pairs. The passed // slice must not be changed after it is passed. -func MapValue(kvs ...KeyValue) Value { return Value{} } // TODO (#4914): implement. +func MapValue(kvs ...KeyValue) Value { + return Value{ + num: uint64(len(kvs)), + any: mapptr(unsafe.SliceData(kvs)), + } +} // AsAny returns the value held by v as an any. -func (v Value) AsAny() any { return nil } // TODO (#4914): implement +func (v Value) AsAny() any { + switch v.Kind() { + case KindMap: + return v.asMap() + case KindSlice: + return v.asSlice() + case KindInt64: + return v.asInt64() + case KindFloat64: + return v.asFloat64() + case KindString: + return v.asString() + case KindBool: + return v.asBool() + case KindBytes: + return v.asBytes() + case KindEmpty: + return nil + default: + global.Error(errKind, "AsAny", "Kind", v.Kind()) + return nil + } +} // AsString returns the value held by v as a string. -func (v Value) AsString() string { return "" } // TODO (#4914): implement +func (v Value) AsString() string { + if sp, ok := v.any.(stringptr); ok { + return unsafe.String(sp, v.num) + } + global.Error(errKind, "AsString", "Kind", v.Kind()) + return "" +} + +// asString returns the value held by v as a string. It will panic if the Value +// is not KindString. +func (v Value) asString() string { + return unsafe.String(v.any.(stringptr), v.num) +} // AsInt64 returns the value held by v as an int64. -func (v Value) AsInt64() int64 { return 0 } // TODO (#4914): implement +func (v Value) AsInt64() int64 { + if v.Kind() != KindInt64 { + global.Error(errKind, "AsInt64", "Kind", v.Kind()) + return 0 + } + return v.asInt64() +} + +// asInt64 returns the value held by v as an int64. If v is not of KindInt64, +// this will return garbage. +func (v Value) asInt64() int64 { return int64(v.num) } // AsBool returns the value held by v as a bool. -func (v Value) AsBool() bool { return false } // TODO (#4914): implement +func (v Value) AsBool() bool { + if v.Kind() != KindBool { + global.Error(errKind, "AsBool", "Kind", v.Kind()) + return false + } + return v.asBool() +} + +// asBool returns the value held by v as a bool. If v is not of KindBool, this +// will return garbage. +func (v Value) asBool() bool { return v.num == 1 } // AsFloat64 returns the value held by v as a float64. -func (v Value) AsFloat64() float64 { return 0 } // TODO (#4914): implement +func (v Value) AsFloat64() float64 { + if v.Kind() != KindFloat64 { + global.Error(errKind, "AsFloat64", "Kind", v.Kind()) + return 0 + } + return v.asFloat64() +} + +// asFloat64 returns the value held by v as a float64. If v is not of +// KindFloat64, this will return garbage. +func (v Value) asFloat64() float64 { return math.Float64frombits(v.num) } // AsBytes returns the value held by v as a []byte. -func (v Value) AsBytes() []byte { return nil } // TODO (#4914): implement +func (v Value) AsBytes() []byte { + if sp, ok := v.any.(bytesptr); ok { + return unsafe.Slice((*byte)(sp), v.num) + } + global.Error(errKind, "AsBytes", "Kind", v.Kind()) + return nil +} + +// asBytes returns the value held by v as a []byte. It will panic if the Value +// is not KindBytes. +func (v Value) asBytes() []byte { + return unsafe.Slice((*byte)(v.any.(bytesptr)), v.num) +} // AsSlice returns the value held by v as a []Value. -func (v Value) AsSlice() []Value { return nil } // TODO (#4914): implement +func (v Value) AsSlice() []Value { + if sp, ok := v.any.(sliceptr); ok { + return unsafe.Slice((*Value)(sp), v.num) + } + global.Error(errKind, "AsSlice", "Kind", v.Kind()) + return nil +} + +// asSlice returns the value held by v as a []Value. It will panic if the Value +// is not KindSlice. +func (v Value) asSlice() []Value { + return unsafe.Slice((*Value)(v.any.(sliceptr)), v.num) +} // AsMap returns the value held by v as a []KeyValue. -func (v Value) AsMap() []KeyValue { return nil } // TODO (#4914): implement +func (v Value) AsMap() []KeyValue { + if sp, ok := v.any.(mapptr); ok { + return unsafe.Slice((*KeyValue)(sp), v.num) + } + global.Error(errKind, "AsMap", "Kind", v.Kind()) + return nil +} + +// asMap returns the value held by v as a []KeyValue. It will panic if the +// Value is not KindMap. +func (v Value) asMap() []KeyValue { + return unsafe.Slice((*KeyValue)(v.any.(mapptr)), v.num) +} // Kind returns the Kind of v. -func (v Value) Kind() Kind { return KindEmpty } // TODO (#4914): implement. +func (v Value) Kind() Kind { + switch x := v.any.(type) { + case Kind: + return x + case stringptr: + return KindString + case bytesptr: + return KindBytes + case sliceptr: + return KindSlice + case mapptr: + return KindMap + default: + return KindEmpty + } +} // Empty returns if v does not hold any value. -func (v Value) Empty() bool { return false } // TODO (#4914): implement +func (v Value) Empty() bool { return v.Kind() == KindEmpty } // Equal returns if v is equal to w. -func (v Value) Equal(w Value) bool { return false } // TODO (#4914): implement +func (v Value) Equal(w Value) bool { + k1 := v.Kind() + k2 := w.Kind() + if k1 != k2 { + return false + } + switch k1 { + case KindInt64, KindBool: + return v.num == w.num + case KindString: + return v.asString() == w.asString() + case KindFloat64: + return v.asFloat64() == w.asFloat64() + case KindSlice: + // TODO: replace with slices.EqualFunc when Go 1.20 support dropped. + return sliceEqualFunc(v.asSlice(), w.asSlice(), Value.Equal) + case KindMap: + // TODO: replace with slices.EqualFunc when Go 1.20 support dropped. + return sliceEqualFunc(v.asMap(), w.asMap(), KeyValue.Equal) + case KindBytes: + return bytes.Equal(v.asBytes(), w.asBytes()) + case KindEmpty: + return true + default: + global.Error(errKind, "Equal", "Kind", k1) + return false + } +} + +// sliceEqualFunc reports whether two slices are equal using an equality +// function on each pair of elements. If the lengths are different, +// sliceEqualFunc returns false. Otherwise, the elements are compared in +// increasing index order, and the comparison stops at the first index for +// which eq returns false. +// +// This is based on [EqualFunc]. It was added to provide backwards +// compatibility for Go 1.20. When Go 1.20 is no longer supported it can be +// removed. +// +// EqualFunc: https://pkg.go.dev/slices#EqualFunc +func sliceEqualFunc[T any](s1 []T, s2 []T, eq func(T, T) bool) bool { + if len(s1) != len(s2) { + return false + } + for i, v1 := range s1 { + v2 := s2[i] + if !eq(v1, v2) { + return false + } + } + return true +} // An KeyValue is a key-value pair used to represent a log attribute (a // superset of [go.opentelemetry.io/otel/attribute.KeyValue]) and map item. @@ -105,28 +333,46 @@ type KeyValue struct { } // Equal returns if a is equal to b. -func (a KeyValue) Equal(b KeyValue) bool { return false } // TODO (#4914): implement +func (a KeyValue) Equal(b KeyValue) bool { + return a.Key == b.Key && a.Value.Equal(b.Value) +} // String returns an KeyValue for a string value. -func String(key, value string) KeyValue { return KeyValue{} } // TODO (#4914): implement +func String(key, value string) KeyValue { + return KeyValue{key, StringValue(value)} +} // Int64 returns an KeyValue for an int64 value. -func Int64(key string, value int64) KeyValue { return KeyValue{} } // TODO (#4914): implement +func Int64(key string, value int64) KeyValue { + return KeyValue{key, Int64Value(value)} +} // Int returns an KeyValue for an int value. -func Int(key string, value int) KeyValue { return KeyValue{} } // TODO (#4914): implement +func Int(key string, value int) KeyValue { + return KeyValue{key, IntValue(value)} +} // Float64 returns an KeyValue for a float64 value. -func Float64(key string, v float64) KeyValue { return KeyValue{} } // TODO (#4914): implement +func Float64(key string, value float64) KeyValue { + return KeyValue{key, Float64Value(value)} +} // Bool returns an KeyValue for a bool value. -func Bool(key string, v bool) KeyValue { return KeyValue{} } // TODO (#4914): implement +func Bool(key string, value bool) KeyValue { + return KeyValue{key, BoolValue(value)} +} // Bytes returns an KeyValue for a []byte value. -func Bytes(key string, v []byte) KeyValue { return KeyValue{} } // TODO (#4914): implement +func Bytes(key string, value []byte) KeyValue { + return KeyValue{key, BytesValue(value)} +} // Slice returns an KeyValue for a []Value value. -func Slice(key string, args ...Value) KeyValue { return KeyValue{} } // TODO (#4914): implement +func Slice(key string, value ...Value) KeyValue { + return KeyValue{key, SliceValue(value...)} +} // Map returns an KeyValue for a map value. -func Map(key string, args ...KeyValue) KeyValue { return KeyValue{} } // TODO (#4914): implement +func Map(key string, value ...KeyValue) KeyValue { + return KeyValue{key, MapValue(value...)} +} diff --git a/log/keyvalue_test.go b/log/keyvalue_test.go new file mode 100644 index 00000000000..4390bfae538 --- /dev/null +++ b/log/keyvalue_test.go @@ -0,0 +1,302 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package log_test + +import ( + golog "log" + "os" + "testing" + + "github.com/go-logr/logr" + "github.com/go-logr/logr/testr" + "github.com/go-logr/stdr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.opentelemetry.io/otel/internal/global" + "go.opentelemetry.io/otel/log" +) + +func TestKind(t *testing.T) { + testCases := []struct { + kind log.Kind + str string + value int + }{ + {log.KindBool, "Bool", 1}, + {log.KindBytes, "Bytes", 5}, + {log.KindEmpty, "Empty", 0}, + {log.KindFloat64, "Float64", 2}, + {log.KindInt64, "Int64", 3}, + {log.KindSlice, "Slice", 6}, + {log.KindMap, "Map", 7}, + {log.KindString, "String", 4}, + } + for _, tc := range testCases { + t.Run(tc.str, func(t *testing.T) { + assert.Equal(t, tc.value, int(tc.kind), "Kind value") + assert.Equal(t, tc.str, tc.kind.String(), "Kind string") + }) + } +} + +func TestValueEqual(t *testing.T) { + vals := []log.Value{ + {}, + log.Int64Value(1), + log.Int64Value(2), + log.Float64Value(3.5), + log.Float64Value(3.7), + log.BoolValue(true), + log.BoolValue(false), + log.StringValue("hi"), + log.StringValue("bye"), + log.BytesValue([]byte{1, 3, 5}), + log.SliceValue(log.StringValue("foo")), + log.SliceValue(log.IntValue(3), log.StringValue("foo")), + log.MapValue(log.Bool("b", true), log.Int("i", 3)), + log.MapValue( + log.Slice("l", log.IntValue(3), log.StringValue("foo")), + log.Bytes("b", []byte{3, 5, 7}), + ), + } + for i, v1 := range vals { + for j, v2 := range vals { + assert.Equal(t, i == j, v1.Equal(v2), "%v.Equal(%v)", v1, v2) + } + } +} + +func TestEmpty(t *testing.T) { + v := log.Value{} + t.Run("Value.Empty", func(t *testing.T) { + assert.True(t, v.Empty()) + }) + t.Run("Value.AsAny", func(t *testing.T) { + assert.Nil(t, v.AsAny()) + }) + + t.Run("Bytes", func(t *testing.T) { + assert.Nil(t, log.Bytes("b", nil).Value.AsBytes()) + }) + t.Run("Slice", func(t *testing.T) { + assert.Nil(t, log.Slice("s").Value.AsSlice()) + }) + t.Run("Map", func(t *testing.T) { + assert.Nil(t, log.Map("m").Value.AsMap()) + }) +} + +func TestEmptyGroupsPreserved(t *testing.T) { + t.Run("Map", func(t *testing.T) { + assert.Equal(t, []log.KeyValue{ + log.Int("a", 1), + log.Map("g1", log.Map("g2")), + log.Map("g3", log.Map("g4", log.Int("b", 2))), + }, log.MapValue( + log.Int("a", 1), + log.Map("g1", log.Map("g2")), + log.Map("g3", log.Map("g4", log.Int("b", 2))), + ).AsMap()) + }) + + t.Run("Slice", func(t *testing.T) { + assert.Equal(t, []log.Value{{}}, log.SliceValue(log.Value{}).AsSlice()) + }) +} + +func TestBool(t *testing.T) { + const key, val = "key", true + kv := log.Bool(key, val) + testKV(t, key, val, kv) + + v, k := kv.Value, log.KindBool + t.Run("AsBool", func(t *testing.T) { + assert.Equal(t, val, kv.Value.AsBool(), "AsBool") + }) + t.Run("AsFloat64", testErrKind(v.AsFloat64, "AsFloat64", k)) + t.Run("AsInt64", testErrKind(v.AsInt64, "AsInt64", k)) + t.Run("AsString", testErrKind(v.AsString, "AsString", k)) + t.Run("AsBytes", testErrKind(v.AsBytes, "AsBytes", k)) + t.Run("AsSlice", testErrKind(v.AsSlice, "AsSlice", k)) + t.Run("AsMap", testErrKind(v.AsMap, "AsMap", k)) +} + +func TestFloat64(t *testing.T) { + const key, val = "key", 3.0 + kv := log.Float64(key, val) + testKV(t, key, val, kv) + + v, k := kv.Value, log.KindFloat64 + t.Run("AsBool", testErrKind(v.AsBool, "AsBool", k)) + t.Run("AsFloat64", func(t *testing.T) { + assert.Equal(t, val, v.AsFloat64(), "AsFloat64") + }) + t.Run("AsInt64", testErrKind(v.AsInt64, "AsInt64", k)) + t.Run("AsString", testErrKind(v.AsString, "AsString", k)) + t.Run("AsBytes", testErrKind(v.AsBytes, "AsBytes", k)) + t.Run("AsSlice", testErrKind(v.AsSlice, "AsSlice", k)) + t.Run("AsMap", testErrKind(v.AsMap, "AsMap", k)) +} + +func TestInt(t *testing.T) { + const key, val = "key", 1 + kv := log.Int(key, val) + testKV[int64](t, key, val, kv) + + v, k := kv.Value, log.KindInt64 + t.Run("AsBool", testErrKind(v.AsBool, "AsBool", k)) + t.Run("AsFloat64", testErrKind(v.AsFloat64, "AsFloat64", k)) + t.Run("AsInt64", func(t *testing.T) { + assert.Equal(t, int64(val), v.AsInt64(), "AsInt64") + }) + t.Run("AsString", testErrKind(v.AsString, "AsString", k)) + t.Run("AsBytes", testErrKind(v.AsBytes, "AsBytes", k)) + t.Run("AsSlice", testErrKind(v.AsSlice, "AsSlice", k)) + t.Run("AsMap", testErrKind(v.AsMap, "AsMap", k)) +} + +func TestInt64(t *testing.T) { + const key, val = "key", 1 + kv := log.Int64(key, val) + testKV[int64](t, key, val, kv) + + v, k := kv.Value, log.KindInt64 + t.Run("AsBool", testErrKind(v.AsBool, "AsBool", k)) + t.Run("AsFloat64", testErrKind(v.AsFloat64, "AsFloat64", k)) + t.Run("AsInt64", func(t *testing.T) { + assert.Equal(t, int64(val), v.AsInt64(), "AsInt64") + }) + t.Run("AsString", testErrKind(v.AsString, "AsString", k)) + t.Run("AsBytes", testErrKind(v.AsBytes, "AsBytes", k)) + t.Run("AsSlice", testErrKind(v.AsSlice, "AsSlice", k)) + t.Run("AsMap", testErrKind(v.AsMap, "AsMap", k)) +} + +func TestString(t *testing.T) { + const key, val = "key", "test string value" + kv := log.String(key, val) + testKV(t, key, val, kv) + + v, k := kv.Value, log.KindString + t.Run("AsBool", testErrKind(v.AsBool, "AsBool", k)) + t.Run("AsFloat64", testErrKind(v.AsFloat64, "AsFloat64", k)) + t.Run("AsInt64", testErrKind(v.AsInt64, "AsInt64", k)) + t.Run("AsString", func(t *testing.T) { + assert.Equal(t, val, v.AsString(), "AsString") + }) + t.Run("AsBytes", testErrKind(v.AsBytes, "AsBytes", k)) + t.Run("AsSlice", testErrKind(v.AsSlice, "AsSlice", k)) + t.Run("AsMap", testErrKind(v.AsMap, "AsMap", k)) +} + +func TestBytes(t *testing.T) { + const key = "key" + val := []byte{3, 2, 1} + kv := log.Bytes(key, val) + testKV(t, key, val, kv) + + v, k := kv.Value, log.KindBytes + t.Run("AsBool", testErrKind(v.AsBool, "AsBool", k)) + t.Run("AsFloat64", testErrKind(v.AsFloat64, "AsFloat64", k)) + t.Run("AsInt64", testErrKind(v.AsInt64, "AsInt64", k)) + t.Run("AsString", testErrKind(v.AsString, "AsString", k)) + t.Run("AsBytes", func(t *testing.T) { + assert.Equal(t, val, v.AsBytes(), "AsBytes") + }) + t.Run("AsSlice", testErrKind(v.AsSlice, "AsSlice", k)) + t.Run("AsMap", testErrKind(v.AsMap, "AsMap", k)) +} + +func TestSlice(t *testing.T) { + const key = "key" + val := []log.Value{log.IntValue(3), log.StringValue("foo")} + kv := log.Slice(key, val...) + testKV(t, key, val, kv) + + v, k := kv.Value, log.KindSlice + t.Run("AsBool", testErrKind(v.AsBool, "AsBool", k)) + t.Run("AsFloat64", testErrKind(v.AsFloat64, "AsFloat64", k)) + t.Run("AsInt64", testErrKind(v.AsInt64, "AsInt64", k)) + t.Run("AsString", testErrKind(v.AsString, "AsString", k)) + t.Run("AsBytes", testErrKind(v.AsBytes, "AsBytes", k)) + t.Run("AsSlice", func(t *testing.T) { + assert.Equal(t, val, v.AsSlice(), "AsSlice") + }) + t.Run("AsMap", testErrKind(v.AsMap, "AsMap", k)) +} + +func TestMap(t *testing.T) { + const key = "key" + val := []log.KeyValue{ + log.Slice("l", log.IntValue(3), log.StringValue("foo")), + log.Bytes("b", []byte{3, 5, 7}), + } + kv := log.Map(key, val...) + testKV(t, key, val, kv) + + v, k := kv.Value, log.KindMap + t.Run("AsBool", testErrKind(v.AsBool, "AsBool", k)) + t.Run("AsFloat64", testErrKind(v.AsFloat64, "AsFloat64", k)) + t.Run("AsInt64", testErrKind(v.AsInt64, "AsInt64", k)) + t.Run("AsString", testErrKind(v.AsString, "AsString", k)) + t.Run("AsBytes", testErrKind(v.AsBytes, "AsBytes", k)) + t.Run("AsSlice", testErrKind(v.AsSlice, "AsSlice", k)) + t.Run("AsMap", func(t *testing.T) { + assert.Equal(t, val, v.AsMap(), "AsMap") + }) +} + +type logSink struct { + logr.LogSink + + err error + msg string + keysAndValues []interface{} +} + +func (l *logSink) Error(err error, msg string, keysAndValues ...interface{}) { + l.err, l.msg, l.keysAndValues = err, msg, keysAndValues + l.LogSink.Error(err, msg, keysAndValues) +} + +var stdLogger = stdr.New(golog.New(os.Stderr, "", golog.LstdFlags|golog.Lshortfile)) + +func testErrKind[T any](f func() T, msg string, k log.Kind) func(*testing.T) { + return func(t *testing.T) { + l := &logSink{LogSink: testr.New(t).GetSink()} + global.SetLogger(logr.New(l)) + t.Cleanup(func() { global.SetLogger(stdLogger) }) + + assert.Zero(t, f()) + + assert.ErrorContains(t, l.err, "invalid Kind") + assert.Equal(t, msg, l.msg) + require.Len(t, l.keysAndValues, 2, "logged attributes") + assert.Equal(t, l.keysAndValues[1], k) + } +} + +func testKV[T any](t *testing.T, key string, val T, kv log.KeyValue) { + t.Helper() + + assert.Equal(t, key, kv.Key, "incorrect key") + assert.False(t, kv.Value.Empty(), "value empty") + assert.Equal(t, kv.Value.AsAny(), T(val), "AsAny wrong value") +}