From 74ac4bf208da1cdb6aa5fdacc95c9bfc83f3b804 Mon Sep 17 00:00:00 2001 From: m1heng <18018422+m1heng@users.noreply.github.com> Date: Sat, 12 Oct 2024 08:15:06 +0000 Subject: [PATCH] Move convert values to internal/shared/logutil --- bridges/otellogr/internal/logutil/convert.go | 159 +++++++++ .../{ => internal/logutil}/convert_test.go | 19 +- bridges/otellogr/internal/logutil/gen.go | 8 + bridges/otellogr/logsink.go | 5 +- bridges/otellogr/logsink_test.go | 15 + bridges/otellogrus/hook.go | 59 +--- bridges/otellogrus/hook_test.go | 16 + .../otellogrus/internal/logutil/convert.go | 159 +++++++++ .../internal/logutil/convert_test.go | 324 ++++++++++++++++++ bridges/otellogrus/internal/logutil/gen.go | 8 + bridges/otelzap/encoder.go | 60 +--- bridges/otelzap/encoder_test.go | 8 + bridges/otelzap/internal/logutil/convert.go | 159 +++++++++ .../otelzap/internal/logutil/convert_test.go | 324 ++++++++++++++++++ bridges/otelzap/internal/logutil/gen.go | 8 + .../shared/logutil/convert.go.tmpl | 27 +- internal/shared/logutil/convert_test.go.tmpl | 324 ++++++++++++++++++ 17 files changed, 1550 insertions(+), 132 deletions(-) create mode 100644 bridges/otellogr/internal/logutil/convert.go rename bridges/otellogr/{ => internal/logutil}/convert_test.go (94%) create mode 100644 bridges/otellogr/internal/logutil/gen.go create mode 100644 bridges/otellogrus/internal/logutil/convert.go create mode 100644 bridges/otellogrus/internal/logutil/convert_test.go create mode 100644 bridges/otellogrus/internal/logutil/gen.go create mode 100644 bridges/otelzap/internal/logutil/convert.go create mode 100644 bridges/otelzap/internal/logutil/convert_test.go create mode 100644 bridges/otelzap/internal/logutil/gen.go rename bridges/otellogr/convert.go => internal/shared/logutil/convert.go.tmpl (83%) create mode 100644 internal/shared/logutil/convert_test.go.tmpl diff --git a/bridges/otellogr/internal/logutil/convert.go b/bridges/otellogr/internal/logutil/convert.go new file mode 100644 index 00000000000..64edebbb07f --- /dev/null +++ b/bridges/otellogr/internal/logutil/convert.go @@ -0,0 +1,159 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/logutil/convert.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logutil // import "go.opentelemetry.io/contrib/bridges/otellogr/internal/logutil" + +import ( + "context" + "fmt" + "math" + "reflect" + "strconv" + "time" + + "go.opentelemetry.io/otel/log" +) + +// convertKVs converts a list of key-value pairs to a list of [log.KeyValue]. +// The last [context.Context] value is returned as the context. +// If no context is found, the original context is returned. +func ConvertKVs(ctx context.Context, keysAndValues ...any) (context.Context, []log.KeyValue) { + if len(keysAndValues) == 0 { + return ctx, nil + } + if len(keysAndValues)%2 != 0 { + // Ensure an odd number of items here does not corrupt the list. + keysAndValues = append(keysAndValues, nil) + } + + kvs := make([]log.KeyValue, 0, len(keysAndValues)/2) + for i := 0; i < len(keysAndValues); i += 2 { + k, ok := keysAndValues[i].(string) + if !ok { + // Ensure that the key is a string. + k = fmt.Sprintf("%v", keysAndValues[i]) + } + + v := keysAndValues[i+1] + if vCtx, ok := v.(context.Context); ok { + // Special case when a field is of context.Context type. + ctx = vCtx + continue + } + + kvs = append(kvs, log.KeyValue{ + Key: k, + Value: ConvertValue(v), + }) + } + + return ctx, kvs +} + +func ConvertValue(v any) log.Value { + // Handling the most common types without reflect is a small perf win. + switch val := v.(type) { + case bool: + return log.BoolValue(val) + case string: + return log.StringValue(val) + case int: + return log.Int64Value(int64(val)) + case int8: + return log.Int64Value(int64(val)) + case int16: + return log.Int64Value(int64(val)) + case int32: + return log.Int64Value(int64(val)) + case int64: + return log.Int64Value(val) + case uint: + return ConvertUintValue(uint64(val)) + case uint8: + return log.Int64Value(int64(val)) + case uint16: + return log.Int64Value(int64(val)) + case uint32: + return log.Int64Value(int64(val)) + case uint64: + return ConvertUintValue(val) + case uintptr: + return ConvertUintValue(uint64(val)) + case float32: + return log.Float64Value(float64(val)) + case float64: + return log.Float64Value(val) + case time.Duration: + return log.Int64Value(val.Nanoseconds()) + case complex64: + r := log.Float64("r", real(complex128(val))) + i := log.Float64("i", imag(complex128(val))) + return log.MapValue(r, i) + case complex128: + r := log.Float64("r", real(val)) + i := log.Float64("i", imag(val)) + return log.MapValue(r, i) + case time.Time: + return log.Int64Value(val.UnixNano()) + case []byte: + return log.BytesValue(val) + case error: + return log.StringValue(val.Error()) + } + + t := reflect.TypeOf(v) + if t == nil { + return log.Value{} + } + val := reflect.ValueOf(v) + switch t.Kind() { + case reflect.Struct: + return log.StringValue(fmt.Sprintf("%+v", v)) + case reflect.Slice, reflect.Array: + items := make([]log.Value, 0, val.Len()) + for i := 0; i < val.Len(); i++ { + items = append(items, ConvertValue(val.Index(i).Interface())) + } + return log.SliceValue(items...) + case reflect.Map: + kvs := make([]log.KeyValue, 0, val.Len()) + for _, k := range val.MapKeys() { + var key string + switch k.Kind() { + case reflect.String: + key = k.String() + default: + key = fmt.Sprintf("%+v", k.Interface()) + } + kvs = append(kvs, log.KeyValue{ + Key: key, + Value: ConvertValue(val.MapIndex(k).Interface()), + }) + } + return log.MapValue(kvs...) + case reflect.Ptr, reflect.Interface: + if val.IsNil() { + return log.Value{} + } + return ConvertValue(val.Elem().Interface()) + } + + // Try to handle this as gracefully as possible. + // + // Don't panic here. 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 log.StringValue(fmt.Sprintf("unhandled: (%s) %+v", t, v)) +} + +// ConvertUintValue converts a uint64 to a log.Value. +// If the value is too large to fit in an int64, it is converted to a string. +func ConvertUintValue(v uint64) log.Value { + if v > math.MaxInt64 { + return log.StringValue(strconv.FormatUint(v, 10)) + } + return log.Int64Value(int64(v)) +} diff --git a/bridges/otellogr/convert_test.go b/bridges/otellogr/internal/logutil/convert_test.go similarity index 94% rename from bridges/otellogr/convert_test.go rename to bridges/otellogr/internal/logutil/convert_test.go index d39afb9137e..a88905e4146 100644 --- a/bridges/otellogr/convert_test.go +++ b/bridges/otellogr/internal/logutil/convert_test.go @@ -1,7 +1,10 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/logutil/convert_test.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package otellogr +package logutil import ( "context" @@ -72,7 +75,7 @@ func TestConvertKVs(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - ctx, kvs := convertKVs(nil, tt.kvs...) // nolint: staticcheck // pass nil context + ctx, kvs := ConvertKVs(nil, tt.kvs...) // nolint: staticcheck // pass nil context assert.Equal(t, tt.wantKVs, kvs) assert.Equal(t, tt.wantCtx, ctx) }) @@ -293,6 +296,14 @@ func TestConvertValue(t *testing.T) { }, wantValue: log.StringValue("{Name:John Age:42}"), }, + { + name: "nil_struct_ptr", + value: (*struct { + Name string + Age int + })(nil), + wantValue: log.Value{}, + }, { name: "ctx", value: context.Background(), @@ -300,13 +311,13 @@ func TestConvertValue(t *testing.T) { }, } { t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.wantValue, convertValue(tt.value)) + assert.Equal(t, tt.wantValue, ConvertValue(tt.value)) }) } } func TestConvertValueFloat32(t *testing.T) { - value := convertValue(float32(3.14)) + value := ConvertValue(float32(3.14)) want := log.Float64Value(3.14) assert.InDelta(t, value.AsFloat64(), want.AsFloat64(), 0.0001) diff --git a/bridges/otellogr/internal/logutil/gen.go b/bridges/otellogr/internal/logutil/gen.go new file mode 100644 index 00000000000..603f6d7a3a6 --- /dev/null +++ b/bridges/otellogr/internal/logutil/gen.go @@ -0,0 +1,8 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logutil // import "go.opentelemetry.io/contrib/bridges/otellogr/internal/logutil" + +// Generate logutil package: +//go:generate gotmpl --body=../../../../internal/shared/logutil/convert_test.go.tmpl "--data={}" --out=convert_test.go +//go:generate gotmpl --body=../../../../internal/shared/logutil/convert.go.tmpl "--data={}" --out=convert.go diff --git a/bridges/otellogr/logsink.go b/bridges/otellogr/logsink.go index 1acd6c55e56..1ae9b4fa7c6 100644 --- a/bridges/otellogr/logsink.go +++ b/bridges/otellogr/logsink.go @@ -48,6 +48,7 @@ import ( "github.com/go-logr/logr" + "go.opentelemetry.io/contrib/bridges/otellogr/internal/logutil" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/global" ) @@ -173,7 +174,7 @@ func (l *LogSink) Info(level int, msg string, keysAndValues ...any) { record.AddAttributes(l.attr...) - ctx, attr := convertKVs(l.ctx, keysAndValues...) + ctx, attr := logutil.ConvertKVs(l.ctx, keysAndValues...) record.AddAttributes(attr...) l.logger.Emit(ctx, record) @@ -193,7 +194,7 @@ func (l LogSink) WithName(name string) logr.LogSink { // WithValues returns a new LogSink with additional key/value pairs. func (l LogSink) WithValues(keysAndValues ...any) logr.LogSink { - ctx, attr := convertKVs(l.ctx, keysAndValues...) + ctx, attr := logutil.ConvertKVs(l.ctx, keysAndValues...) l.attr = append(l.attr, attr...) l.ctx = ctx return &l diff --git a/bridges/otellogr/logsink_test.go b/bridges/otellogr/logsink_test.go index 8b47f871313..d330fcc4b7d 100644 --- a/bridges/otellogr/logsink_test.go +++ b/bridges/otellogr/logsink_test.go @@ -216,6 +216,21 @@ func TestLogSink(t *testing.T) { }, }, }, + { + name: "info_with_normal_attr_and_nil_pointer_attr", + f: func(l *logr.Logger) { + var p *int + l.WithValues("key", "value", "nil_pointer", p).Info("info message with attrs") + }, + wantRecords: map[string][]log.Record{ + name: { + buildRecord(log.StringValue("info message with attrs"), time.Time{}, log.SeverityInfo, []log.KeyValue{ + log.String("key", "value"), + log.Empty("nil_pointer"), + }), + }, + }, + }, } { t.Run(tt.name, func(t *testing.T) { rec := logtest.NewRecorder() diff --git a/bridges/otellogrus/hook.go b/bridges/otellogrus/hook.go index 6ebe85d138f..d2bf344c572 100644 --- a/bridges/otellogrus/hook.go +++ b/bridges/otellogrus/hook.go @@ -33,11 +33,9 @@ package otellogrus // import "go.opentelemetry.io/contrib/bridges/otellogrus" import ( - "fmt" - "reflect" - "github.com/sirupsen/logrus" + "go.opentelemetry.io/contrib/bridges/otellogrus/internal/logutil" "go.opentelemetry.io/otel/log" "go.opentelemetry.io/otel/log/global" ) @@ -177,7 +175,7 @@ func convertFields(fields logrus.Fields) []log.KeyValue { for k, v := range fields { kvs = append(kvs, log.KeyValue{ Key: k, - Value: convertValue(v), + Value: logutil.ConvertValue(v), }) } return kvs @@ -206,56 +204,3 @@ func convertSeverity(level logrus.Level) log.Severity { return log.SeverityUndefined } } - -func convertValue(v interface{}) log.Value { - switch v := v.(type) { - case bool: - return log.BoolValue(v) - case []byte: - return log.BytesValue(v) - case float64: - return log.Float64Value(v) - case int: - return log.IntValue(v) - case int64: - return log.Int64Value(v) - case string: - return log.StringValue(v) - } - - t := reflect.TypeOf(v) - if t == nil { - return log.Value{} - } - val := reflect.ValueOf(v) - switch t.Kind() { - case reflect.Struct: - return log.StringValue(fmt.Sprintf("%+v", v)) - case reflect.Slice, reflect.Array: - items := make([]log.Value, 0, val.Len()) - for i := 0; i < val.Len(); i++ { - items = append(items, convertValue(val.Index(i).Interface())) - } - return log.SliceValue(items...) - case reflect.Map: - kvs := make([]log.KeyValue, 0, val.Len()) - for _, k := range val.MapKeys() { - var key string - // If the key is a struct, use %+v to print the struct fields. - if k.Kind() == reflect.Struct { - key = fmt.Sprintf("%+v", k.Interface()) - } else { - key = fmt.Sprintf("%v", k.Interface()) - } - kvs = append(kvs, log.KeyValue{ - Key: key, - Value: convertValue(val.MapIndex(k).Interface()), - }) - } - return log.MapValue(kvs...) - case reflect.Ptr, reflect.Interface: - return convertValue(val.Elem().Interface()) - } - - return log.StringValue(fmt.Sprintf("unhandled attribute type: (%s) %+v", t, v)) -} diff --git a/bridges/otellogrus/hook_test.go b/bridges/otellogrus/hook_test.go index 6e95bb93ddd..2a241c8eebf 100644 --- a/bridges/otellogrus/hook_test.go +++ b/bridges/otellogrus/hook_test.go @@ -148,6 +148,7 @@ func TestHookLevels(t *testing.T) { func TestHookFire(t *testing.T) { const name = "name" now := time.Now() + var nilPointer *struct{} for _, tt := range []struct { name string @@ -269,6 +270,21 @@ func TestHookFire(t *testing.T) { }, }, }, + { + name: "emits a log entry with data cotaining a nil pointer", + entry: &logrus.Entry{ + Data: logrus.Fields{ + "nil_pointer": nilPointer, + }, + }, + wantRecords: map[string][]log.Record{ + name: { + buildRecord(log.StringValue(""), time.Time{}, log.SeverityFatal4, []log.KeyValue{ + {Key: "nil_pointer", Value: log.Value{}}, + }), + }, + }, + }, } { t.Run(tt.name, func(t *testing.T) { rec := logtest.NewRecorder() diff --git a/bridges/otellogrus/internal/logutil/convert.go b/bridges/otellogrus/internal/logutil/convert.go new file mode 100644 index 00000000000..b43130389aa --- /dev/null +++ b/bridges/otellogrus/internal/logutil/convert.go @@ -0,0 +1,159 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/logutil/convert.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logutil // import "go.opentelemetry.io/contrib/bridges/otellogrus/internal/logutil" + +import ( + "context" + "fmt" + "math" + "reflect" + "strconv" + "time" + + "go.opentelemetry.io/otel/log" +) + +// convertKVs converts a list of key-value pairs to a list of [log.KeyValue]. +// The last [context.Context] value is returned as the context. +// If no context is found, the original context is returned. +func ConvertKVs(ctx context.Context, keysAndValues ...any) (context.Context, []log.KeyValue) { + if len(keysAndValues) == 0 { + return ctx, nil + } + if len(keysAndValues)%2 != 0 { + // Ensure an odd number of items here does not corrupt the list. + keysAndValues = append(keysAndValues, nil) + } + + kvs := make([]log.KeyValue, 0, len(keysAndValues)/2) + for i := 0; i < len(keysAndValues); i += 2 { + k, ok := keysAndValues[i].(string) + if !ok { + // Ensure that the key is a string. + k = fmt.Sprintf("%v", keysAndValues[i]) + } + + v := keysAndValues[i+1] + if vCtx, ok := v.(context.Context); ok { + // Special case when a field is of context.Context type. + ctx = vCtx + continue + } + + kvs = append(kvs, log.KeyValue{ + Key: k, + Value: ConvertValue(v), + }) + } + + return ctx, kvs +} + +func ConvertValue(v any) log.Value { + // Handling the most common types without reflect is a small perf win. + switch val := v.(type) { + case bool: + return log.BoolValue(val) + case string: + return log.StringValue(val) + case int: + return log.Int64Value(int64(val)) + case int8: + return log.Int64Value(int64(val)) + case int16: + return log.Int64Value(int64(val)) + case int32: + return log.Int64Value(int64(val)) + case int64: + return log.Int64Value(val) + case uint: + return ConvertUintValue(uint64(val)) + case uint8: + return log.Int64Value(int64(val)) + case uint16: + return log.Int64Value(int64(val)) + case uint32: + return log.Int64Value(int64(val)) + case uint64: + return ConvertUintValue(val) + case uintptr: + return ConvertUintValue(uint64(val)) + case float32: + return log.Float64Value(float64(val)) + case float64: + return log.Float64Value(val) + case time.Duration: + return log.Int64Value(val.Nanoseconds()) + case complex64: + r := log.Float64("r", real(complex128(val))) + i := log.Float64("i", imag(complex128(val))) + return log.MapValue(r, i) + case complex128: + r := log.Float64("r", real(val)) + i := log.Float64("i", imag(val)) + return log.MapValue(r, i) + case time.Time: + return log.Int64Value(val.UnixNano()) + case []byte: + return log.BytesValue(val) + case error: + return log.StringValue(val.Error()) + } + + t := reflect.TypeOf(v) + if t == nil { + return log.Value{} + } + val := reflect.ValueOf(v) + switch t.Kind() { + case reflect.Struct: + return log.StringValue(fmt.Sprintf("%+v", v)) + case reflect.Slice, reflect.Array: + items := make([]log.Value, 0, val.Len()) + for i := 0; i < val.Len(); i++ { + items = append(items, ConvertValue(val.Index(i).Interface())) + } + return log.SliceValue(items...) + case reflect.Map: + kvs := make([]log.KeyValue, 0, val.Len()) + for _, k := range val.MapKeys() { + var key string + switch k.Kind() { + case reflect.String: + key = k.String() + default: + key = fmt.Sprintf("%+v", k.Interface()) + } + kvs = append(kvs, log.KeyValue{ + Key: key, + Value: ConvertValue(val.MapIndex(k).Interface()), + }) + } + return log.MapValue(kvs...) + case reflect.Ptr, reflect.Interface: + if val.IsNil() { + return log.Value{} + } + return ConvertValue(val.Elem().Interface()) + } + + // Try to handle this as gracefully as possible. + // + // Don't panic here. 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 log.StringValue(fmt.Sprintf("unhandled: (%s) %+v", t, v)) +} + +// ConvertUintValue converts a uint64 to a log.Value. +// If the value is too large to fit in an int64, it is converted to a string. +func ConvertUintValue(v uint64) log.Value { + if v > math.MaxInt64 { + return log.StringValue(strconv.FormatUint(v, 10)) + } + return log.Int64Value(int64(v)) +} diff --git a/bridges/otellogrus/internal/logutil/convert_test.go b/bridges/otellogrus/internal/logutil/convert_test.go new file mode 100644 index 00000000000..a88905e4146 --- /dev/null +++ b/bridges/otellogrus/internal/logutil/convert_test.go @@ -0,0 +1,324 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/logutil/convert_test.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logutil + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" +) + +func TestConvertKVs(t *testing.T) { + ctx := context.WithValue(context.Background(), "key", "value") // nolint: revive,staticcheck // test context + + for _, tt := range []struct { + name string + kvs []any + wantKVs []log.KeyValue + wantCtx context.Context + }{ + { + name: "empty", + kvs: []any{}, + }, + { + name: "single_value", + kvs: []any{"key", "value"}, + wantKVs: []log.KeyValue{ + log.String("key", "value"), + }, + }, + { + name: "multiple_values", + kvs: []any{"key1", "value1", "key2", "value2"}, + wantKVs: []log.KeyValue{ + log.String("key1", "value1"), + log.String("key2", "value2"), + }, + }, + { + name: "missing_value", + kvs: []any{"key1", "value1", "key2"}, + wantKVs: []log.KeyValue{ + log.String("key1", "value1"), + {Key: "key2", Value: log.Value{}}, + }, + }, + { + name: "key_not_string", + kvs: []any{42, "value"}, + wantKVs: []log.KeyValue{ + log.String("42", "value"), + }, + }, + { + name: "context", + kvs: []any{"ctx", ctx, "key", "value"}, + wantKVs: []log.KeyValue{log.String("key", "value")}, + wantCtx: ctx, + }, + { + name: "last_context", + kvs: []any{"key", context.Background(), "ctx", ctx}, + wantKVs: []log.KeyValue{}, + wantCtx: ctx, + }, + } { + t.Run(tt.name, func(t *testing.T) { + ctx, kvs := ConvertKVs(nil, tt.kvs...) // nolint: staticcheck // pass nil context + assert.Equal(t, tt.wantKVs, kvs) + assert.Equal(t, tt.wantCtx, ctx) + }) + } +} + +func TestConvertValue(t *testing.T) { + for _, tt := range []struct { + name string + value any + wantValue log.Value + }{ + { + name: "bool", + value: true, + wantValue: log.BoolValue(true), + }, + { + name: "string", + value: "value", + wantValue: log.StringValue("value"), + }, + { + name: "int", + value: 10, + wantValue: log.Int64Value(10), + }, + { + name: "int8", + value: int8(127), + wantValue: log.Int64Value(127), + }, + { + name: "int16", + value: int16(32767), + wantValue: log.Int64Value(32767), + }, + { + name: "int32", + value: int32(2147483647), + wantValue: log.Int64Value(2147483647), + }, + { + name: "int64", + value: int64(9223372036854775807), + wantValue: log.Int64Value(9223372036854775807), + }, + { + name: "uint", + value: uint(42), + wantValue: log.Int64Value(42), + }, + { + name: "uint8", + value: uint8(255), + wantValue: log.Int64Value(255), + }, + { + name: "uint16", + value: uint16(65535), + wantValue: log.Int64Value(65535), + }, + { + name: "uint32", + value: uint32(4294967295), + wantValue: log.Int64Value(4294967295), + }, + { + name: "uint64", + value: uint64(9223372036854775807), + wantValue: log.Int64Value(9223372036854775807), + }, + { + name: "uint64-max", + value: uint64(18446744073709551615), + wantValue: log.StringValue("18446744073709551615"), + }, + { + name: "uintptr", + value: uintptr(12345), + wantValue: log.Int64Value(12345), + }, + { + name: "float64", + value: float64(3.14159), + wantValue: log.Float64Value(3.14159), + }, + { + name: "time.Duration", + value: time.Second, + wantValue: log.Int64Value(1_000_000_000), + }, + { + name: "complex64", + value: complex64(complex(float32(1), float32(2))), + wantValue: log.MapValue(log.Float64("r", 1), log.Float64("i", 2)), + }, + { + name: "complex128", + value: complex(float64(3), float64(4)), + wantValue: log.MapValue(log.Float64("r", 3), log.Float64("i", 4)), + }, + { + name: "time.Time", + value: time.Unix(1000, 1000), + wantValue: log.Int64Value(time.Unix(1000, 1000).UnixNano()), + }, + { + name: "[]byte", + value: []byte("hello"), + wantValue: log.BytesValue([]byte("hello")), + }, + { + name: "error", + value: errors.New("test error"), + wantValue: log.StringValue("test error"), + }, + { + name: "error", + value: errors.New("test error"), + wantValue: log.StringValue("test error"), + }, + { + name: "error-nested", + value: fmt.Errorf("test error: %w", errors.New("nested error")), + wantValue: log.StringValue("test error: nested error"), + }, + { + name: "nil", + value: nil, + wantValue: log.Value{}, + }, + { + name: "nil_ptr", + value: (*int)(nil), + wantValue: log.Value{}, + }, + { + name: "int_ptr", + value: func() *int { i := 93; return &i }(), + wantValue: log.Int64Value(93), + }, + { + name: "string_ptr", + value: func() *string { s := "hello"; return &s }(), + wantValue: log.StringValue("hello"), + }, + { + name: "bool_ptr", + value: func() *bool { b := true; return &b }(), + wantValue: log.BoolValue(true), + }, + { + name: "int_empty_array", + value: []int{}, + wantValue: log.SliceValue([]log.Value{}...), + }, + { + name: "int_array", + value: []int{1, 2, 3}, + wantValue: log.SliceValue([]log.Value{ + log.Int64Value(1), + log.Int64Value(2), + log.Int64Value(3), + }...), + }, + { + name: "key_value_map", + value: map[string]int{"one": 1}, + wantValue: log.MapValue( + log.Int64("one", 1), + ), + }, + { + name: "int_string_map", + value: map[int]string{1: "one"}, + wantValue: log.MapValue( + log.String("1", "one"), + ), + }, + { + name: "nested_map", + value: map[string]map[string]int{"nested": {"one": 1}}, + wantValue: log.MapValue( + log.Map("nested", + log.Int64("one", 1), + ), + ), + }, + { + name: "struct_key_map", + value: map[struct{ Name string }]int{ + {Name: "John"}: 42, + }, + wantValue: log.MapValue( + log.Int64("{Name:John}", 42), + ), + }, + { + name: "struct", + value: struct { + Name string + Age int + }{ + Name: "John", + Age: 42, + }, + wantValue: log.StringValue("{Name:John Age:42}"), + }, + { + name: "struct_ptr", + value: &struct { + Name string + Age int + }{ + Name: "John", + Age: 42, + }, + wantValue: log.StringValue("{Name:John Age:42}"), + }, + { + name: "nil_struct_ptr", + value: (*struct { + Name string + Age int + })(nil), + wantValue: log.Value{}, + }, + { + name: "ctx", + value: context.Background(), + wantValue: log.StringValue("context.Background"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantValue, ConvertValue(tt.value)) + }) + } +} + +func TestConvertValueFloat32(t *testing.T) { + value := ConvertValue(float32(3.14)) + want := log.Float64Value(3.14) + + assert.InDelta(t, value.AsFloat64(), want.AsFloat64(), 0.0001) +} diff --git a/bridges/otellogrus/internal/logutil/gen.go b/bridges/otellogrus/internal/logutil/gen.go new file mode 100644 index 00000000000..2871e1cf957 --- /dev/null +++ b/bridges/otellogrus/internal/logutil/gen.go @@ -0,0 +1,8 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logutil // import "go.opentelemetry.io/contrib/bridges/otellogrus/internal/logutil" + +// Generate logutil package: +//go:generate gotmpl --body=../../../../internal/shared/logutil/convert_test.go.tmpl "--data={}" --out=convert_test.go +//go:generate gotmpl --body=../../../../internal/shared/logutil/convert.go.tmpl "--data={}" --out=convert.go diff --git a/bridges/otelzap/encoder.go b/bridges/otelzap/encoder.go index a5ddfb48d32..fe3f93fc9b8 100644 --- a/bridges/otelzap/encoder.go +++ b/bridges/otelzap/encoder.go @@ -4,12 +4,11 @@ package otelzap // import "go.opentelemetry.io/contrib/bridges/otelzap" import ( - "fmt" - "reflect" "time" "go.uber.org/zap/zapcore" + "go.opentelemetry.io/contrib/bridges/otelzap/internal/logutil" "go.opentelemetry.io/otel/log" ) @@ -121,7 +120,7 @@ func (m *objectEncoder) AddReflected(k string, v interface{}) error { m.cur.attrs = append(m.cur.attrs, log.KeyValue{ Key: k, - Value: convertValue(v), + Value: logutil.ConvertValue(v), }) return nil } @@ -221,7 +220,7 @@ func (a *arrayEncoder) AppendObject(v zapcore.ObjectMarshaler) error { } func (a *arrayEncoder) AppendReflected(v interface{}) error { - a.elems = append(a.elems, convertValue(v)) + a.elems = append(a.elems, logutil.ConvertValue(v)) return nil } @@ -274,56 +273,3 @@ func (a *arrayEncoder) AppendUint32(v uint32) { a.AppendInt64(int64(v)) func (a *arrayEncoder) AppendUint16(v uint16) { a.AppendInt64(int64(v)) } func (a *arrayEncoder) AppendUint8(v uint8) { a.AppendInt64(int64(v)) } func (a *arrayEncoder) AppendUintptr(v uintptr) { a.AppendUint64(uint64(v)) } - -func convertValue(v interface{}) log.Value { - switch v := v.(type) { - case bool: - return log.BoolValue(v) - case []byte: - return log.BytesValue(v) - case float64: - return log.Float64Value(v) - case int: - return log.IntValue(v) - case int64: - return log.Int64Value(v) - case string: - return log.StringValue(v) - } - - t := reflect.TypeOf(v) - if t == nil { - return log.Value{} - } - val := reflect.ValueOf(v) - switch t.Kind() { - case reflect.Struct: - return log.StringValue(fmt.Sprintf("%+v", v)) - case reflect.Slice, reflect.Array: - items := make([]log.Value, 0, val.Len()) - for i := 0; i < val.Len(); i++ { - items = append(items, convertValue(val.Index(i).Interface())) - } - return log.SliceValue(items...) - case reflect.Map: - kvs := make([]log.KeyValue, 0, val.Len()) - for _, k := range val.MapKeys() { - var key string - // If the key is a struct, use %+v to print the struct fields. - if k.Kind() == reflect.Struct { - key = fmt.Sprintf("%+v", k.Interface()) - } else { - key = fmt.Sprintf("%v", k.Interface()) - } - kvs = append(kvs, log.KeyValue{ - Key: key, - Value: convertValue(val.MapIndex(k).Interface()), - }) - } - return log.MapValue(kvs...) - case reflect.Ptr, reflect.Interface: - return convertValue(val.Elem().Interface()) - } - - return log.StringValue(fmt.Sprintf("unhandled attribute type: (%s) %+v", t, v)) -} diff --git a/bridges/otelzap/encoder_test.go b/bridges/otelzap/encoder_test.go index 4e284b82d35..ba3d726e3c6 100644 --- a/bridges/otelzap/encoder_test.go +++ b/bridges/otelzap/encoder_test.go @@ -72,6 +72,14 @@ func TestObjectEncoder(t *testing.T) { }, expected: map[string]interface{}{"foo": int64(5)}, }, + { + desc: "AddReflected (nil pointer)", + f: func(e zapcore.ObjectEncoder) { + var p *struct{} + assert.NoError(t, e.AddReflected("k", p), "Expected AddReflected to succeed.") + }, + expected: nil, + }, { desc: "AddBinary", f: func(e zapcore.ObjectEncoder) { e.AddBinary("k", []byte("foo")) }, diff --git a/bridges/otelzap/internal/logutil/convert.go b/bridges/otelzap/internal/logutil/convert.go new file mode 100644 index 00000000000..271708a0752 --- /dev/null +++ b/bridges/otelzap/internal/logutil/convert.go @@ -0,0 +1,159 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/logutil/convert.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logutil // import "go.opentelemetry.io/contrib/bridges/otelzap/internal/logutil" + +import ( + "context" + "fmt" + "math" + "reflect" + "strconv" + "time" + + "go.opentelemetry.io/otel/log" +) + +// convertKVs converts a list of key-value pairs to a list of [log.KeyValue]. +// The last [context.Context] value is returned as the context. +// If no context is found, the original context is returned. +func ConvertKVs(ctx context.Context, keysAndValues ...any) (context.Context, []log.KeyValue) { + if len(keysAndValues) == 0 { + return ctx, nil + } + if len(keysAndValues)%2 != 0 { + // Ensure an odd number of items here does not corrupt the list. + keysAndValues = append(keysAndValues, nil) + } + + kvs := make([]log.KeyValue, 0, len(keysAndValues)/2) + for i := 0; i < len(keysAndValues); i += 2 { + k, ok := keysAndValues[i].(string) + if !ok { + // Ensure that the key is a string. + k = fmt.Sprintf("%v", keysAndValues[i]) + } + + v := keysAndValues[i+1] + if vCtx, ok := v.(context.Context); ok { + // Special case when a field is of context.Context type. + ctx = vCtx + continue + } + + kvs = append(kvs, log.KeyValue{ + Key: k, + Value: ConvertValue(v), + }) + } + + return ctx, kvs +} + +func ConvertValue(v any) log.Value { + // Handling the most common types without reflect is a small perf win. + switch val := v.(type) { + case bool: + return log.BoolValue(val) + case string: + return log.StringValue(val) + case int: + return log.Int64Value(int64(val)) + case int8: + return log.Int64Value(int64(val)) + case int16: + return log.Int64Value(int64(val)) + case int32: + return log.Int64Value(int64(val)) + case int64: + return log.Int64Value(val) + case uint: + return ConvertUintValue(uint64(val)) + case uint8: + return log.Int64Value(int64(val)) + case uint16: + return log.Int64Value(int64(val)) + case uint32: + return log.Int64Value(int64(val)) + case uint64: + return ConvertUintValue(val) + case uintptr: + return ConvertUintValue(uint64(val)) + case float32: + return log.Float64Value(float64(val)) + case float64: + return log.Float64Value(val) + case time.Duration: + return log.Int64Value(val.Nanoseconds()) + case complex64: + r := log.Float64("r", real(complex128(val))) + i := log.Float64("i", imag(complex128(val))) + return log.MapValue(r, i) + case complex128: + r := log.Float64("r", real(val)) + i := log.Float64("i", imag(val)) + return log.MapValue(r, i) + case time.Time: + return log.Int64Value(val.UnixNano()) + case []byte: + return log.BytesValue(val) + case error: + return log.StringValue(val.Error()) + } + + t := reflect.TypeOf(v) + if t == nil { + return log.Value{} + } + val := reflect.ValueOf(v) + switch t.Kind() { + case reflect.Struct: + return log.StringValue(fmt.Sprintf("%+v", v)) + case reflect.Slice, reflect.Array: + items := make([]log.Value, 0, val.Len()) + for i := 0; i < val.Len(); i++ { + items = append(items, ConvertValue(val.Index(i).Interface())) + } + return log.SliceValue(items...) + case reflect.Map: + kvs := make([]log.KeyValue, 0, val.Len()) + for _, k := range val.MapKeys() { + var key string + switch k.Kind() { + case reflect.String: + key = k.String() + default: + key = fmt.Sprintf("%+v", k.Interface()) + } + kvs = append(kvs, log.KeyValue{ + Key: key, + Value: ConvertValue(val.MapIndex(k).Interface()), + }) + } + return log.MapValue(kvs...) + case reflect.Ptr, reflect.Interface: + if val.IsNil() { + return log.Value{} + } + return ConvertValue(val.Elem().Interface()) + } + + // Try to handle this as gracefully as possible. + // + // Don't panic here. 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 log.StringValue(fmt.Sprintf("unhandled: (%s) %+v", t, v)) +} + +// ConvertUintValue converts a uint64 to a log.Value. +// If the value is too large to fit in an int64, it is converted to a string. +func ConvertUintValue(v uint64) log.Value { + if v > math.MaxInt64 { + return log.StringValue(strconv.FormatUint(v, 10)) + } + return log.Int64Value(int64(v)) +} diff --git a/bridges/otelzap/internal/logutil/convert_test.go b/bridges/otelzap/internal/logutil/convert_test.go new file mode 100644 index 00000000000..a88905e4146 --- /dev/null +++ b/bridges/otelzap/internal/logutil/convert_test.go @@ -0,0 +1,324 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/logutil/convert_test.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logutil + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" +) + +func TestConvertKVs(t *testing.T) { + ctx := context.WithValue(context.Background(), "key", "value") // nolint: revive,staticcheck // test context + + for _, tt := range []struct { + name string + kvs []any + wantKVs []log.KeyValue + wantCtx context.Context + }{ + { + name: "empty", + kvs: []any{}, + }, + { + name: "single_value", + kvs: []any{"key", "value"}, + wantKVs: []log.KeyValue{ + log.String("key", "value"), + }, + }, + { + name: "multiple_values", + kvs: []any{"key1", "value1", "key2", "value2"}, + wantKVs: []log.KeyValue{ + log.String("key1", "value1"), + log.String("key2", "value2"), + }, + }, + { + name: "missing_value", + kvs: []any{"key1", "value1", "key2"}, + wantKVs: []log.KeyValue{ + log.String("key1", "value1"), + {Key: "key2", Value: log.Value{}}, + }, + }, + { + name: "key_not_string", + kvs: []any{42, "value"}, + wantKVs: []log.KeyValue{ + log.String("42", "value"), + }, + }, + { + name: "context", + kvs: []any{"ctx", ctx, "key", "value"}, + wantKVs: []log.KeyValue{log.String("key", "value")}, + wantCtx: ctx, + }, + { + name: "last_context", + kvs: []any{"key", context.Background(), "ctx", ctx}, + wantKVs: []log.KeyValue{}, + wantCtx: ctx, + }, + } { + t.Run(tt.name, func(t *testing.T) { + ctx, kvs := ConvertKVs(nil, tt.kvs...) // nolint: staticcheck // pass nil context + assert.Equal(t, tt.wantKVs, kvs) + assert.Equal(t, tt.wantCtx, ctx) + }) + } +} + +func TestConvertValue(t *testing.T) { + for _, tt := range []struct { + name string + value any + wantValue log.Value + }{ + { + name: "bool", + value: true, + wantValue: log.BoolValue(true), + }, + { + name: "string", + value: "value", + wantValue: log.StringValue("value"), + }, + { + name: "int", + value: 10, + wantValue: log.Int64Value(10), + }, + { + name: "int8", + value: int8(127), + wantValue: log.Int64Value(127), + }, + { + name: "int16", + value: int16(32767), + wantValue: log.Int64Value(32767), + }, + { + name: "int32", + value: int32(2147483647), + wantValue: log.Int64Value(2147483647), + }, + { + name: "int64", + value: int64(9223372036854775807), + wantValue: log.Int64Value(9223372036854775807), + }, + { + name: "uint", + value: uint(42), + wantValue: log.Int64Value(42), + }, + { + name: "uint8", + value: uint8(255), + wantValue: log.Int64Value(255), + }, + { + name: "uint16", + value: uint16(65535), + wantValue: log.Int64Value(65535), + }, + { + name: "uint32", + value: uint32(4294967295), + wantValue: log.Int64Value(4294967295), + }, + { + name: "uint64", + value: uint64(9223372036854775807), + wantValue: log.Int64Value(9223372036854775807), + }, + { + name: "uint64-max", + value: uint64(18446744073709551615), + wantValue: log.StringValue("18446744073709551615"), + }, + { + name: "uintptr", + value: uintptr(12345), + wantValue: log.Int64Value(12345), + }, + { + name: "float64", + value: float64(3.14159), + wantValue: log.Float64Value(3.14159), + }, + { + name: "time.Duration", + value: time.Second, + wantValue: log.Int64Value(1_000_000_000), + }, + { + name: "complex64", + value: complex64(complex(float32(1), float32(2))), + wantValue: log.MapValue(log.Float64("r", 1), log.Float64("i", 2)), + }, + { + name: "complex128", + value: complex(float64(3), float64(4)), + wantValue: log.MapValue(log.Float64("r", 3), log.Float64("i", 4)), + }, + { + name: "time.Time", + value: time.Unix(1000, 1000), + wantValue: log.Int64Value(time.Unix(1000, 1000).UnixNano()), + }, + { + name: "[]byte", + value: []byte("hello"), + wantValue: log.BytesValue([]byte("hello")), + }, + { + name: "error", + value: errors.New("test error"), + wantValue: log.StringValue("test error"), + }, + { + name: "error", + value: errors.New("test error"), + wantValue: log.StringValue("test error"), + }, + { + name: "error-nested", + value: fmt.Errorf("test error: %w", errors.New("nested error")), + wantValue: log.StringValue("test error: nested error"), + }, + { + name: "nil", + value: nil, + wantValue: log.Value{}, + }, + { + name: "nil_ptr", + value: (*int)(nil), + wantValue: log.Value{}, + }, + { + name: "int_ptr", + value: func() *int { i := 93; return &i }(), + wantValue: log.Int64Value(93), + }, + { + name: "string_ptr", + value: func() *string { s := "hello"; return &s }(), + wantValue: log.StringValue("hello"), + }, + { + name: "bool_ptr", + value: func() *bool { b := true; return &b }(), + wantValue: log.BoolValue(true), + }, + { + name: "int_empty_array", + value: []int{}, + wantValue: log.SliceValue([]log.Value{}...), + }, + { + name: "int_array", + value: []int{1, 2, 3}, + wantValue: log.SliceValue([]log.Value{ + log.Int64Value(1), + log.Int64Value(2), + log.Int64Value(3), + }...), + }, + { + name: "key_value_map", + value: map[string]int{"one": 1}, + wantValue: log.MapValue( + log.Int64("one", 1), + ), + }, + { + name: "int_string_map", + value: map[int]string{1: "one"}, + wantValue: log.MapValue( + log.String("1", "one"), + ), + }, + { + name: "nested_map", + value: map[string]map[string]int{"nested": {"one": 1}}, + wantValue: log.MapValue( + log.Map("nested", + log.Int64("one", 1), + ), + ), + }, + { + name: "struct_key_map", + value: map[struct{ Name string }]int{ + {Name: "John"}: 42, + }, + wantValue: log.MapValue( + log.Int64("{Name:John}", 42), + ), + }, + { + name: "struct", + value: struct { + Name string + Age int + }{ + Name: "John", + Age: 42, + }, + wantValue: log.StringValue("{Name:John Age:42}"), + }, + { + name: "struct_ptr", + value: &struct { + Name string + Age int + }{ + Name: "John", + Age: 42, + }, + wantValue: log.StringValue("{Name:John Age:42}"), + }, + { + name: "nil_struct_ptr", + value: (*struct { + Name string + Age int + })(nil), + wantValue: log.Value{}, + }, + { + name: "ctx", + value: context.Background(), + wantValue: log.StringValue("context.Background"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantValue, ConvertValue(tt.value)) + }) + } +} + +func TestConvertValueFloat32(t *testing.T) { + value := ConvertValue(float32(3.14)) + want := log.Float64Value(3.14) + + assert.InDelta(t, value.AsFloat64(), want.AsFloat64(), 0.0001) +} diff --git a/bridges/otelzap/internal/logutil/gen.go b/bridges/otelzap/internal/logutil/gen.go new file mode 100644 index 00000000000..e5502b6077e --- /dev/null +++ b/bridges/otelzap/internal/logutil/gen.go @@ -0,0 +1,8 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logutil // import "go.opentelemetry.io/contrib/bridges/otelzap/internal/logutil" + +// Generate logutil package: +//go:generate gotmpl --body=../../../../internal/shared/logutil/convert_test.go.tmpl "--data={}" --out=convert_test.go +//go:generate gotmpl --body=../../../../internal/shared/logutil/convert.go.tmpl "--data={}" --out=convert.go diff --git a/bridges/otellogr/convert.go b/internal/shared/logutil/convert.go.tmpl similarity index 83% rename from bridges/otellogr/convert.go rename to internal/shared/logutil/convert.go.tmpl index 35a79e5248f..782a2412184 100644 --- a/bridges/otellogr/convert.go +++ b/internal/shared/logutil/convert.go.tmpl @@ -1,7 +1,10 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/logutil/convert.go.tmpl + // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package otellogr // import "go.opentelemetry.io/contrib/bridges/otellogr" +package logutil // import "go.opentelemetry.io/contrib/internal/shared/logutil" import ( "context" @@ -17,7 +20,7 @@ import ( // convertKVs converts a list of key-value pairs to a list of [log.KeyValue]. // The last [context.Context] value is returned as the context. // If no context is found, the original context is returned. -func convertKVs(ctx context.Context, keysAndValues ...any) (context.Context, []log.KeyValue) { +func ConvertKVs(ctx context.Context, keysAndValues ...any) (context.Context, []log.KeyValue) { if len(keysAndValues) == 0 { return ctx, nil } @@ -43,14 +46,14 @@ func convertKVs(ctx context.Context, keysAndValues ...any) (context.Context, []l kvs = append(kvs, log.KeyValue{ Key: k, - Value: convertValue(v), + Value: ConvertValue(v), }) } return ctx, kvs } -func convertValue(v any) log.Value { +func ConvertValue(v any) log.Value { // Handling the most common types without reflect is a small perf win. switch val := v.(type) { case bool: @@ -68,7 +71,7 @@ func convertValue(v any) log.Value { case int64: return log.Int64Value(val) case uint: - return convertUintValue(uint64(val)) + return ConvertUintValue(uint64(val)) case uint8: return log.Int64Value(int64(val)) case uint16: @@ -76,9 +79,9 @@ func convertValue(v any) log.Value { case uint32: return log.Int64Value(int64(val)) case uint64: - return convertUintValue(val) + return ConvertUintValue(val) case uintptr: - return convertUintValue(uint64(val)) + return ConvertUintValue(uint64(val)) case float32: return log.Float64Value(float64(val)) case float64: @@ -112,7 +115,7 @@ func convertValue(v any) log.Value { case reflect.Slice, reflect.Array: items := make([]log.Value, 0, val.Len()) for i := 0; i < val.Len(); i++ { - items = append(items, convertValue(val.Index(i).Interface())) + items = append(items, ConvertValue(val.Index(i).Interface())) } return log.SliceValue(items...) case reflect.Map: @@ -127,7 +130,7 @@ func convertValue(v any) log.Value { } kvs = append(kvs, log.KeyValue{ Key: key, - Value: convertValue(val.MapIndex(k).Interface()), + Value: ConvertValue(val.MapIndex(k).Interface()), }) } return log.MapValue(kvs...) @@ -135,7 +138,7 @@ func convertValue(v any) log.Value { if val.IsNil() { return log.Value{} } - return convertValue(val.Elem().Interface()) + return ConvertValue(val.Elem().Interface()) } // Try to handle this as gracefully as possible. @@ -146,9 +149,9 @@ func convertValue(v any) log.Value { return log.StringValue(fmt.Sprintf("unhandled: (%s) %+v", t, v)) } -// convertUintValue converts a uint64 to a log.Value. +// ConvertUintValue converts a uint64 to a log.Value. // If the value is too large to fit in an int64, it is converted to a string. -func convertUintValue(v uint64) log.Value { +func ConvertUintValue(v uint64) log.Value { if v > math.MaxInt64 { return log.StringValue(strconv.FormatUint(v, 10)) } diff --git a/internal/shared/logutil/convert_test.go.tmpl b/internal/shared/logutil/convert_test.go.tmpl new file mode 100644 index 00000000000..a88905e4146 --- /dev/null +++ b/internal/shared/logutil/convert_test.go.tmpl @@ -0,0 +1,324 @@ +// Code created by gotmpl. DO NOT MODIFY. +// source: internal/shared/logutil/convert_test.go.tmpl + +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package logutil + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "go.opentelemetry.io/otel/log" +) + +func TestConvertKVs(t *testing.T) { + ctx := context.WithValue(context.Background(), "key", "value") // nolint: revive,staticcheck // test context + + for _, tt := range []struct { + name string + kvs []any + wantKVs []log.KeyValue + wantCtx context.Context + }{ + { + name: "empty", + kvs: []any{}, + }, + { + name: "single_value", + kvs: []any{"key", "value"}, + wantKVs: []log.KeyValue{ + log.String("key", "value"), + }, + }, + { + name: "multiple_values", + kvs: []any{"key1", "value1", "key2", "value2"}, + wantKVs: []log.KeyValue{ + log.String("key1", "value1"), + log.String("key2", "value2"), + }, + }, + { + name: "missing_value", + kvs: []any{"key1", "value1", "key2"}, + wantKVs: []log.KeyValue{ + log.String("key1", "value1"), + {Key: "key2", Value: log.Value{}}, + }, + }, + { + name: "key_not_string", + kvs: []any{42, "value"}, + wantKVs: []log.KeyValue{ + log.String("42", "value"), + }, + }, + { + name: "context", + kvs: []any{"ctx", ctx, "key", "value"}, + wantKVs: []log.KeyValue{log.String("key", "value")}, + wantCtx: ctx, + }, + { + name: "last_context", + kvs: []any{"key", context.Background(), "ctx", ctx}, + wantKVs: []log.KeyValue{}, + wantCtx: ctx, + }, + } { + t.Run(tt.name, func(t *testing.T) { + ctx, kvs := ConvertKVs(nil, tt.kvs...) // nolint: staticcheck // pass nil context + assert.Equal(t, tt.wantKVs, kvs) + assert.Equal(t, tt.wantCtx, ctx) + }) + } +} + +func TestConvertValue(t *testing.T) { + for _, tt := range []struct { + name string + value any + wantValue log.Value + }{ + { + name: "bool", + value: true, + wantValue: log.BoolValue(true), + }, + { + name: "string", + value: "value", + wantValue: log.StringValue("value"), + }, + { + name: "int", + value: 10, + wantValue: log.Int64Value(10), + }, + { + name: "int8", + value: int8(127), + wantValue: log.Int64Value(127), + }, + { + name: "int16", + value: int16(32767), + wantValue: log.Int64Value(32767), + }, + { + name: "int32", + value: int32(2147483647), + wantValue: log.Int64Value(2147483647), + }, + { + name: "int64", + value: int64(9223372036854775807), + wantValue: log.Int64Value(9223372036854775807), + }, + { + name: "uint", + value: uint(42), + wantValue: log.Int64Value(42), + }, + { + name: "uint8", + value: uint8(255), + wantValue: log.Int64Value(255), + }, + { + name: "uint16", + value: uint16(65535), + wantValue: log.Int64Value(65535), + }, + { + name: "uint32", + value: uint32(4294967295), + wantValue: log.Int64Value(4294967295), + }, + { + name: "uint64", + value: uint64(9223372036854775807), + wantValue: log.Int64Value(9223372036854775807), + }, + { + name: "uint64-max", + value: uint64(18446744073709551615), + wantValue: log.StringValue("18446744073709551615"), + }, + { + name: "uintptr", + value: uintptr(12345), + wantValue: log.Int64Value(12345), + }, + { + name: "float64", + value: float64(3.14159), + wantValue: log.Float64Value(3.14159), + }, + { + name: "time.Duration", + value: time.Second, + wantValue: log.Int64Value(1_000_000_000), + }, + { + name: "complex64", + value: complex64(complex(float32(1), float32(2))), + wantValue: log.MapValue(log.Float64("r", 1), log.Float64("i", 2)), + }, + { + name: "complex128", + value: complex(float64(3), float64(4)), + wantValue: log.MapValue(log.Float64("r", 3), log.Float64("i", 4)), + }, + { + name: "time.Time", + value: time.Unix(1000, 1000), + wantValue: log.Int64Value(time.Unix(1000, 1000).UnixNano()), + }, + { + name: "[]byte", + value: []byte("hello"), + wantValue: log.BytesValue([]byte("hello")), + }, + { + name: "error", + value: errors.New("test error"), + wantValue: log.StringValue("test error"), + }, + { + name: "error", + value: errors.New("test error"), + wantValue: log.StringValue("test error"), + }, + { + name: "error-nested", + value: fmt.Errorf("test error: %w", errors.New("nested error")), + wantValue: log.StringValue("test error: nested error"), + }, + { + name: "nil", + value: nil, + wantValue: log.Value{}, + }, + { + name: "nil_ptr", + value: (*int)(nil), + wantValue: log.Value{}, + }, + { + name: "int_ptr", + value: func() *int { i := 93; return &i }(), + wantValue: log.Int64Value(93), + }, + { + name: "string_ptr", + value: func() *string { s := "hello"; return &s }(), + wantValue: log.StringValue("hello"), + }, + { + name: "bool_ptr", + value: func() *bool { b := true; return &b }(), + wantValue: log.BoolValue(true), + }, + { + name: "int_empty_array", + value: []int{}, + wantValue: log.SliceValue([]log.Value{}...), + }, + { + name: "int_array", + value: []int{1, 2, 3}, + wantValue: log.SliceValue([]log.Value{ + log.Int64Value(1), + log.Int64Value(2), + log.Int64Value(3), + }...), + }, + { + name: "key_value_map", + value: map[string]int{"one": 1}, + wantValue: log.MapValue( + log.Int64("one", 1), + ), + }, + { + name: "int_string_map", + value: map[int]string{1: "one"}, + wantValue: log.MapValue( + log.String("1", "one"), + ), + }, + { + name: "nested_map", + value: map[string]map[string]int{"nested": {"one": 1}}, + wantValue: log.MapValue( + log.Map("nested", + log.Int64("one", 1), + ), + ), + }, + { + name: "struct_key_map", + value: map[struct{ Name string }]int{ + {Name: "John"}: 42, + }, + wantValue: log.MapValue( + log.Int64("{Name:John}", 42), + ), + }, + { + name: "struct", + value: struct { + Name string + Age int + }{ + Name: "John", + Age: 42, + }, + wantValue: log.StringValue("{Name:John Age:42}"), + }, + { + name: "struct_ptr", + value: &struct { + Name string + Age int + }{ + Name: "John", + Age: 42, + }, + wantValue: log.StringValue("{Name:John Age:42}"), + }, + { + name: "nil_struct_ptr", + value: (*struct { + Name string + Age int + })(nil), + wantValue: log.Value{}, + }, + { + name: "ctx", + value: context.Background(), + wantValue: log.StringValue("context.Background"), + }, + } { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.wantValue, ConvertValue(tt.value)) + }) + } +} + +func TestConvertValueFloat32(t *testing.T) { + value := ConvertValue(float32(3.14)) + want := log.Float64Value(3.14) + + assert.InDelta(t, value.AsFloat64(), want.AsFloat64(), 0.0001) +}