From 2cb0fec0a0f38ede0c8f1238531b21ad0e6911ba Mon Sep 17 00:00:00 2001 From: Joakim Recht Date: Tue, 10 Oct 2023 00:44:12 +0200 Subject: [PATCH 1/4] Make it possible to extract fields from the context in the slog adapter Since slog has context logging built in it would be nice to be able to utilize that with a zap handler, for example to log tracing context. --- exp/zapslog/handler.go | 23 +++++++++++++++++------ exp/zapslog/handler_test.go | 20 ++++++++++++++++++++ exp/zapslog/options.go | 7 +++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/exp/zapslog/handler.go b/exp/zapslog/handler.go index 982d9bccd..63e5ae6a4 100644 --- a/exp/zapslog/handler.go +++ b/exp/zapslog/handler.go @@ -32,13 +32,17 @@ import ( "go.uber.org/zap/zapcore" ) +// ContextExtractor can be used to extract fields from a context. +type ContextExtractor func(ctx context.Context) []zapcore.Field + // Handler implements the slog.Handler by writing to a zap Core. type Handler struct { - core zapcore.Core - name string // logger name - addCaller bool - addStackAt slog.Level - callerSkip int + core zapcore.Core + name string // logger name + addCaller bool + addStackAt slog.Level + callerSkip int + contextExtractor ContextExtractor } // NewHandler builds a [Handler] that writes to the supplied [zapcore.Core] @@ -157,7 +161,14 @@ func (h *Handler) Handle(ctx context.Context, record slog.Record) error { ce.Stack = stacktrace.Take(3 + h.callerSkip) } - fields := make([]zapcore.Field, 0, record.NumAttrs()) + var contextFields []zapcore.Field + if h.contextExtractor != nil { + contextFields = h.contextExtractor(ctx) + } + + fields := make([]zapcore.Field, 0, record.NumAttrs()+len(contextFields)) + fields = append(fields, contextFields...) + record.Attrs(func(attr slog.Attr) bool { fields = append(fields, convertAttrToField(attr)) return true diff --git a/exp/zapslog/handler_test.go b/exp/zapslog/handler_test.go index 68339df62..381b58e54 100644 --- a/exp/zapslog/handler_test.go +++ b/exp/zapslog/handler_test.go @@ -23,16 +23,20 @@ package zapslog import ( + "context" "log/slog" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/zap" "go.uber.org/zap/zapcore" "go.uber.org/zap/zaptest/observer" ) +type testContextKey string + func TestAddCaller(t *testing.T) { t.Parallel() @@ -189,3 +193,19 @@ func TestAttrKinds(t *testing.T) { }, entry.ContextMap()) } + +func TestContextExtractor(t *testing.T) { + key := testContextKey("testkey") + fac, logs := observer.New(zapcore.DebugLevel) + ctx := context.WithValue(context.Background(), key, "testvalue") + + sl := slog.New(NewHandler(fac, WithContextExtractor(func(ctx context.Context) []zapcore.Field { + v := ctx.Value(key).(string) + return []zapcore.Field{zap.String("testkey", v)} + }))) + sl.InfoContext(ctx, "msg") + lines := logs.TakeAll() + + require.Len(t, lines, 1) + require.Equal(t, "testvalue", lines[0].ContextMap()["testkey"]) +} diff --git a/exp/zapslog/options.go b/exp/zapslog/options.go index 0eb5c8c0e..7eb818131 100644 --- a/exp/zapslog/options.go +++ b/exp/zapslog/options.go @@ -70,3 +70,10 @@ func AddStacktraceAt(lvl slog.Level) Option { log.addStackAt = lvl }) } + +// .WithContextExtractor configures the Logger to extract fields from the context, +func WithContextExtractor(extractor ContextExtractor) Option { + return optionFunc(func(log *Handler) { + log.contextExtractor = extractor + }) +} From 63a8de980f52f687b556381733e10fdfe8c7a920 Mon Sep 17 00:00:00 2001 From: Joakim Recht Date: Thu, 7 Dec 2023 23:22:55 +0100 Subject: [PATCH 2/4] rename to ContextFieldExtractor and allow multiple extractors --- exp/zapslog/handler.go | 20 ++++++++++---------- exp/zapslog/handler_test.go | 4 ++-- exp/zapslog/options.go | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/exp/zapslog/handler.go b/exp/zapslog/handler.go index 63e5ae6a4..4ab4a647d 100644 --- a/exp/zapslog/handler.go +++ b/exp/zapslog/handler.go @@ -32,17 +32,17 @@ import ( "go.uber.org/zap/zapcore" ) -// ContextExtractor can be used to extract fields from a context. -type ContextExtractor func(ctx context.Context) []zapcore.Field +// ContextFieldExtractor can be used to extract a field from a context. +type ContextFieldExtractor func(ctx context.Context) []zapcore.Field // Handler implements the slog.Handler by writing to a zap Core. type Handler struct { - core zapcore.Core - name string // logger name - addCaller bool - addStackAt slog.Level - callerSkip int - contextExtractor ContextExtractor + core zapcore.Core + name string // logger name + addCaller bool + addStackAt slog.Level + callerSkip int + contextFieldExtractors ContextFieldExtractor } // NewHandler builds a [Handler] that writes to the supplied [zapcore.Core] @@ -162,8 +162,8 @@ func (h *Handler) Handle(ctx context.Context, record slog.Record) error { } var contextFields []zapcore.Field - if h.contextExtractor != nil { - contextFields = h.contextExtractor(ctx) + for _, extractor := range h.contextFieldExtractors { + contextFields = append(contextFields, extractor(ctx)...) } fields := make([]zapcore.Field, 0, record.NumAttrs()+len(contextFields)) diff --git a/exp/zapslog/handler_test.go b/exp/zapslog/handler_test.go index 381b58e54..797e5e413 100644 --- a/exp/zapslog/handler_test.go +++ b/exp/zapslog/handler_test.go @@ -194,12 +194,12 @@ func TestAttrKinds(t *testing.T) { entry.ContextMap()) } -func TestContextExtractor(t *testing.T) { +func TestContextFieldExtractor(t *testing.T) { key := testContextKey("testkey") fac, logs := observer.New(zapcore.DebugLevel) ctx := context.WithValue(context.Background(), key, "testvalue") - sl := slog.New(NewHandler(fac, WithContextExtractor(func(ctx context.Context) []zapcore.Field { + sl := slog.New(NewHandler(fac, WithContextFieldExtractor(func(ctx context.Context) []zapcore.Field { v := ctx.Value(key).(string) return []zapcore.Field{zap.String("testkey", v)} }))) diff --git a/exp/zapslog/options.go b/exp/zapslog/options.go index 7eb818131..267a866be 100644 --- a/exp/zapslog/options.go +++ b/exp/zapslog/options.go @@ -71,9 +71,9 @@ func AddStacktraceAt(lvl slog.Level) Option { }) } -// .WithContextExtractor configures the Logger to extract fields from the context, -func WithContextExtractor(extractor ContextExtractor) Option { +// .WithContextFieldExtractor configures the Logger to extract fields from the context, +func WithContextFieldExtractors(extractors ...ContextFieldExtractor) Option { return optionFunc(func(log *Handler) { - log.contextExtractor = extractor + log.contextFieldExtractors = append(log.contextFieldExtractors, extractors...) }) } From acee0a834211286ee997ab440532efbaced3deca Mon Sep 17 00:00:00 2001 From: Joakim Recht Date: Wed, 13 Dec 2023 14:34:32 +0100 Subject: [PATCH 3/4] fixes --- exp/zapslog/handler.go | 2 +- exp/zapslog/handler_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exp/zapslog/handler.go b/exp/zapslog/handler.go index 4ab4a647d..d9d800951 100644 --- a/exp/zapslog/handler.go +++ b/exp/zapslog/handler.go @@ -42,7 +42,7 @@ type Handler struct { addCaller bool addStackAt slog.Level callerSkip int - contextFieldExtractors ContextFieldExtractor + contextFieldExtractors []ContextFieldExtractor } // NewHandler builds a [Handler] that writes to the supplied [zapcore.Core] diff --git a/exp/zapslog/handler_test.go b/exp/zapslog/handler_test.go index 797e5e413..487129a6d 100644 --- a/exp/zapslog/handler_test.go +++ b/exp/zapslog/handler_test.go @@ -199,7 +199,7 @@ func TestContextFieldExtractor(t *testing.T) { fac, logs := observer.New(zapcore.DebugLevel) ctx := context.WithValue(context.Background(), key, "testvalue") - sl := slog.New(NewHandler(fac, WithContextFieldExtractor(func(ctx context.Context) []zapcore.Field { + sl := slog.New(NewHandler(fac, WithContextFieldExtractors(func(ctx context.Context) []zapcore.Field { v := ctx.Value(key).(string) return []zapcore.Field{zap.String("testkey", v)} }))) From a241cd5c488db53248babe2119af2bc7217eb7fb Mon Sep 17 00:00:00 2001 From: Joakim Recht Date: Thu, 14 Dec 2023 08:58:32 +0100 Subject: [PATCH 4/4] lint --- exp/zapslog/options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exp/zapslog/options.go b/exp/zapslog/options.go index 267a866be..859524213 100644 --- a/exp/zapslog/options.go +++ b/exp/zapslog/options.go @@ -71,7 +71,7 @@ func AddStacktraceAt(lvl slog.Level) Option { }) } -// .WithContextFieldExtractor configures the Logger to extract fields from the context, +// .WithContextFieldExtractors configures the Logger to extract fields from the context. func WithContextFieldExtractors(extractors ...ContextFieldExtractor) Option { return optionFunc(func(log *Handler) { log.contextFieldExtractors = append(log.contextFieldExtractors, extractors...)