Skip to content

Commit

Permalink
feat(cloudrequsetlog): initial slog support
Browse files Browse the repository at this point in the history
Step 1 is to change the public API in a backwards-compatible way so that
both slog and zap logger fields/attributes are supported. This is part
of step 1.

Step 2 is to change the underlying representation from zap to slog -
this will come in a follow up.

The conversion code is copied from the stdlib and the zap library.
  • Loading branch information
odsod committed Oct 4, 2024
1 parent a6e4bd3 commit d5b1bcb
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 17 deletions.
113 changes: 100 additions & 13 deletions cloudrequestlog/additionalfields.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cloudrequestlog

import (
"context"
"log/slog"
"sync"

"go.uber.org/zap"
Expand All @@ -25,44 +26,130 @@ func WithAdditionalFields(ctx context.Context) context.Context {
type AdditionalFields struct {
mu sync.Mutex
fields []zap.Field
arrays map[string][]zapcore.ObjectMarshaler
arrays []*arrayField
}

type arrayField struct {
key string
values []any
}

// Add additional fields.
func (m *AdditionalFields) Add(fields ...zap.Field) {
func (m *AdditionalFields) Add(args ...any) {
m.mu.Lock()
m.fields = append(m.fields, fields...)
m.fields = append(m.fields, argsToFieldSlice(args)...)
m.mu.Unlock()
}

// AddToArray adds additional objects to an array field.
func (m *AdditionalFields) AddToArray(key string, objects ...zapcore.ObjectMarshaler) {
func (m *AdditionalFields) AddToArray(key string, objects ...any) {
m.mu.Lock()
if m.arrays == nil {
m.arrays = map[string][]zapcore.ObjectMarshaler{}
defer m.mu.Unlock()
var array *arrayField
for _, needle := range m.arrays {
if needle.key == key {
array = needle
break
}
}
m.arrays[key] = append(m.arrays[key], objects...)
m.mu.Unlock()
if array == nil {
array = &arrayField{key: key}
m.arrays = append(m.arrays, array)
}
array.values = append(array.values, objects...)
}

// AppendTo appends the additional fields to the input fields.
func (m *AdditionalFields) AppendTo(fields []zap.Field) []zap.Field {
m.mu.Lock()
fields = append(fields, m.fields...)
for key, objects := range m.arrays {
fields = append(fields, zap.Array(key, objectArray(objects)))
for _, array := range m.arrays {
fields = append(fields, zap.Array(array.key, anyArray(array.values)))
}
m.mu.Unlock()
return fields
}

type objectArray []zapcore.ObjectMarshaler
type anyArray []any

func (oa objectArray) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
func (oa anyArray) MarshalLogArray(encoder zapcore.ArrayEncoder) error {
for _, o := range oa {
if err := encoder.AppendObject(o); err != nil {
if err := encoder.AppendReflected(o); err != nil {
return err
}
}
return nil
}

func argsToFieldSlice(args []any) []zap.Field {
var attr slog.Attr
fields := make([]zap.Field, 0, len(args))
for len(args) > 0 {
attr, args = argsToAttr(args)
fields = append(fields, convertAttrToField(attr))
}
return fields
}

// argsToAttr is copied from the slog stdlib.
func argsToAttr(args []any) (slog.Attr, []any) {
const badKey = "!BADKEY"
switch x := args[0].(type) {
case string:
if len(args) == 1 {
return slog.String(badKey, x), nil
}
return slog.Any(x, args[1]), args[2:]
case slog.Attr:
return x, args[1:]
default:
return slog.Any(badKey, x), args[1:]
}
}

// convertAttrToField is copied from go.uber.org/zap/exp/zapslog.
func convertAttrToField(attr slog.Attr) zap.Field {
if attr.Equal(slog.Attr{}) {
// Ignore empty attrs.
return zap.Skip()
}
switch attr.Value.Kind() {
case slog.KindBool:
return zap.Bool(attr.Key, attr.Value.Bool())
case slog.KindDuration:
return zap.Duration(attr.Key, attr.Value.Duration())
case slog.KindFloat64:
return zap.Float64(attr.Key, attr.Value.Float64())
case slog.KindInt64:
return zap.Int64(attr.Key, attr.Value.Int64())
case slog.KindString:
return zap.String(attr.Key, attr.Value.String())
case slog.KindTime:
return zap.Time(attr.Key, attr.Value.Time())
case slog.KindUint64:
return zap.Uint64(attr.Key, attr.Value.Uint64())
case slog.KindGroup:
if attr.Key == "" {
// Inlines recursively.
return zap.Inline(groupObject(attr.Value.Group()))
}
return zap.Object(attr.Key, groupObject(attr.Value.Group()))
case slog.KindLogValuer:
return convertAttrToField(slog.Attr{
Key: attr.Key,
Value: attr.Value.Resolve(),
})
default:
return zap.Any(attr.Key, attr.Value.Any())
}
}

// groupObject holds all the Attrs saved in a slog.GroupValue.
type groupObject []slog.Attr

func (gs groupObject) MarshalLogObject(enc zapcore.ObjectEncoder) error {
for _, attr := range gs {
convertAttrToField(attr).AddTo(enc)
}
return nil
}
7 changes: 3 additions & 4 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"go.einride.tech/cloudrunner/cloudrequestlog"
"go.einride.tech/cloudrunner/cloudzap"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

// Logger returns the logger for the current context.
Expand All @@ -28,16 +27,16 @@ func WithLoggerFields(ctx context.Context, fields ...zap.Field) context.Context
}

// AddRequestLogFields adds fields to the current request log, and is safe to call concurrently.
func AddRequestLogFields(ctx context.Context, fields ...zap.Field) {
func AddRequestLogFields(ctx context.Context, args ...any) {
requestLogFields, ok := cloudrequestlog.GetAdditionalFields(ctx)
if !ok {
panic("cloudrunner.AddRequestLogFields must be called with a context from cloudrequestlog.Middleware")
}
requestLogFields.Add(fields...)
requestLogFields.Add(args...)
}

// AddRequestLogFieldsToArray appends objects to an array field in the request log and is safe to call concurrently.
func AddRequestLogFieldsToArray(ctx context.Context, key string, objects ...zapcore.ObjectMarshaler) {
func AddRequestLogFieldsToArray(ctx context.Context, key string, objects ...any) {
additionalFields, ok := cloudrequestlog.GetAdditionalFields(ctx)
if !ok {
panic("cloudrunner.AddRequestLogFieldsToArray must be called with a context from cloudrequestlog.Middleware")
Expand Down

0 comments on commit d5b1bcb

Please sign in to comment.