From d5b1bcb0172c2241a38b3c28639d7f81855ecf4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oscar=20S=C3=B6derlund?= Date: Thu, 3 Oct 2024 08:27:15 +0200 Subject: [PATCH] feat(cloudrequsetlog): initial slog support 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. --- cloudrequestlog/additionalfields.go | 113 ++++++++++++++++++++++++---- logger.go | 7 +- 2 files changed, 103 insertions(+), 17 deletions(-) diff --git a/cloudrequestlog/additionalfields.go b/cloudrequestlog/additionalfields.go index e0d5b2fa..f1f6adf4 100644 --- a/cloudrequestlog/additionalfields.go +++ b/cloudrequestlog/additionalfields.go @@ -2,6 +2,7 @@ package cloudrequestlog import ( "context" + "log/slog" "sync" "go.uber.org/zap" @@ -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 +} diff --git a/logger.go b/logger.go index faa9e179..7310c78c 100644 --- a/logger.go +++ b/logger.go @@ -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. @@ -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")