From c17dfd2b67721d29cf53a0af1e9f0ba0766b91ba Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Tue, 23 May 2023 15:31:21 -0500 Subject: [PATCH] Support keysAndArgs API of stdlib slog (#173) --- s.go | 2 +- slog.go | 66 +++++++++++++++++++++++++++++++++++++----- slog_test.go | 35 ++++++++++++++++++++-- sloggers/slogtest/t.go | 8 ++--- 4 files changed, 97 insertions(+), 14 deletions(-) diff --git a/s.go b/s.go index 250a003..c6d5df9 100644 --- a/s.go +++ b/s.go @@ -41,7 +41,7 @@ func (w stdlogWriter) Write(p []byte) (n int, err error) { // we do not want. msg = strings.TrimSuffix(msg, "\n") - w.l.log(w.ctx, w.level, msg, Map{}) + w.l.log(w.ctx, w.level, msg, nil) return len(p), nil } diff --git a/slog.go b/slog.go index adea6e2..4e901ae 100644 --- a/slog.go +++ b/slog.go @@ -80,40 +80,67 @@ func Make(sinks ...Sink) Logger { } // Debug logs the msg and fields at LevelDebug. -func (l Logger) Debug(ctx context.Context, msg string, fields ...Field) { +// See Info for information on the fields argument. +func (l Logger) Debug(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelDebug, msg, fields) } // Info logs the msg and fields at LevelInfo. -func (l Logger) Info(ctx context.Context, msg string, fields ...Field) { +// Fields may contain any combination of key value pairs, Field, and Map. +// For example: +// +// log.Info(ctx, "something happened", "user", "alex", slog.F("age", 20)) +// +// is equivalent to: +// +// log.Info(ctx, "something happened", slog.F("user", "alex"), slog.F("age", 20)) +// +// is equivalent to: +// +// log.Info(ctx, "something happened", slog.M( +// slog.F("user", "alex"), +// slog.F("age", 20), +// )) +// +// is equivalent to: +// +// log.Info(ctx, "something happened", "user", "alex", "age", 20) +// +// In general, prefer using key value pairs over Field and Map, as that is how +// the standard library's slog package works. +func (l Logger) Info(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelInfo, msg, fields) } // Warn logs the msg and fields at LevelWarn. -func (l Logger) Warn(ctx context.Context, msg string, fields ...Field) { +// See Info() for information on the fields argument. +func (l Logger) Warn(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelWarn, msg, fields) } // Error logs the msg and fields at LevelError. +// See Info() for information on the fields argument. // // It will then Sync(). -func (l Logger) Error(ctx context.Context, msg string, fields ...Field) { +func (l Logger) Error(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelError, msg, fields) l.Sync() } // Critical logs the msg and fields at LevelCritical. +// See Info() for information on the fields argument. // // It will then Sync(). -func (l Logger) Critical(ctx context.Context, msg string, fields ...Field) { +func (l Logger) Critical(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelCritical, msg, fields) l.Sync() } // Fatal logs the msg and fields at LevelFatal. +// See Info() for information on the fields argument. // // It will then Sync() and os.Exit(1). -func (l Logger) Fatal(ctx context.Context, msg string, fields ...Field) { +func (l Logger) Fatal(ctx context.Context, msg string, fields ...any) { l.log(ctx, LevelFatal, msg, fields) l.Sync() @@ -155,7 +182,32 @@ func (l Logger) AppendSinks(s ...Sink) Logger { return l } -func (l Logger) log(ctx context.Context, level Level, msg string, fields Map) { +func (l Logger) log(ctx context.Context, level Level, msg string, rawFields []any) { + fields := make(Map, 0, len(rawFields)) + var wipField Field + for i, f := range rawFields { + if wipField.Name != "" { + wipField.Value = f + fields = append(fields, wipField) + wipField = Field{} + continue + } + switch f := f.(type) { + case Field: + fields = append(fields, f) + case Map: + fields = append(fields, f...) + case string: + wipField.Name = f + default: + panic(fmt.Sprintf("unexpected field type %T at index %v (does it have a key?)", f, i)) + } + } + + if wipField.Name != "" { + panic(fmt.Sprintf("field %q has no value", wipField.Name)) + } + ent := l.entry(ctx, level, msg, fields) l.Log(ctx, ent) } diff --git a/slog_test.go b/slog_test.go index b708b51..7793b17 100644 --- a/slog_test.go +++ b/slog_test.go @@ -2,6 +2,7 @@ package slog_test import ( "context" + "fmt" "io" "runtime" "testing" @@ -75,7 +76,7 @@ func TestLogger(t *testing.T) { File: slogTestFile, Func: "cdr.dev/slog_test.TestLogger.func2", - Line: 67, + Line: 68, Fields: slog.M( slog.F("ctx", 1024), @@ -108,7 +109,7 @@ func TestLogger(t *testing.T) { File: slogTestFile, Func: "cdr.dev/slog_test.TestLogger.func3", - Line: 98, + Line: 99, SpanContext: span.SpanContext(), @@ -149,6 +150,36 @@ func TestLogger(t *testing.T) { assert.Equal(t, "level", slog.LevelFatal, s.entries[5].Level) assert.Equal(t, "exits", 1, exits) }) + + t.Run("kv", func(t *testing.T) { + s := &fakeSink{} + l := slog.Make(s) + + // All of these formats should be equivalent. + formats := [][]any{ + {"animal", "cat", "weight", 15}, + {slog.F("animal", "cat"), "weight", 15}, + {slog.M( + slog.F("animal", "cat"), + slog.F("weight", 15), + )}, + {slog.F("animal", "cat"), slog.F("weight", 15)}, + } + + for _, format := range formats { + l.Info(bg, "msg", format...) + } + + assert.Len(t, "entries", 4, s.entries) + + for i := range s.entries { + assert.Equal( + t, fmt.Sprintf("%v", i), + s.entries[0].Fields, + s.entries[i].Fields, + ) + } + }) } func TestLevel_String(t *testing.T) { diff --git a/sloggers/slogtest/t.go b/sloggers/slogtest/t.go index 724a603..9d81766 100644 --- a/sloggers/slogtest/t.go +++ b/sloggers/slogtest/t.go @@ -103,25 +103,25 @@ func l(t testing.TB) slog.Logger { } // Debug logs the given msg and fields to t via t.Log at the debug level. -func Debug(t testing.TB, msg string, fields ...slog.Field) { +func Debug(t testing.TB, msg string, fields ...any) { slog.Helper() l(t).Debug(ctx, msg, fields...) } // Info logs the given msg and fields to t via t.Log at the info level. -func Info(t testing.TB, msg string, fields ...slog.Field) { +func Info(t testing.TB, msg string, fields ...any) { slog.Helper() l(t).Info(ctx, msg, fields...) } // Error logs the given msg and fields to t via t.Error at the error level. -func Error(t testing.TB, msg string, fields ...slog.Field) { +func Error(t testing.TB, msg string, fields ...any) { slog.Helper() l(t).Error(ctx, msg, fields...) } // Fatal logs the given msg and fields to t via t.Fatal at the fatal level. -func Fatal(t testing.TB, msg string, fields ...slog.Field) { +func Fatal(t testing.TB, msg string, fields ...any) { slog.Helper() l(t).Fatal(ctx, msg, fields...) }