Skip to content

Commit

Permalink
Move convert values to internal/shared/logutil
Browse files Browse the repository at this point in the history
  • Loading branch information
m1heng committed Oct 13, 2024
1 parent c3c8538 commit 7d3a3be
Show file tree
Hide file tree
Showing 17 changed files with 1,518 additions and 132 deletions.
159 changes: 159 additions & 0 deletions bridges/otellogr/internal/logutil/convert.go
Original file line number Diff line number Diff line change
@@ -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))
}
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -300,13 +303,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)
Expand Down
8 changes: 8 additions & 0 deletions bridges/otellogr/internal/logutil/gen.go
Original file line number Diff line number Diff line change
@@ -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
5 changes: 3 additions & 2 deletions bridges/otellogr/logsink.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
15 changes: 15 additions & 0 deletions bridges/otellogr/logsink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
59 changes: 2 additions & 57 deletions bridges/otellogrus/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
16 changes: 16 additions & 0 deletions bridges/otellogrus/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Loading

0 comments on commit 7d3a3be

Please sign in to comment.