diff --git a/encoding.go b/encoding.go index 6088e65..8efd004 100644 --- a/encoding.go +++ b/encoding.go @@ -3,8 +3,6 @@ package console import ( "fmt" "log/slog" - "path/filepath" - "runtime" "time" ) @@ -75,21 +73,6 @@ func (e encoder) writeTimestamp(buf *buffer, tt time.Time) { } } -func (e encoder) writeSource(buf *buffer, pc uintptr, cwd string) { - frame, _ := runtime.CallersFrames([]uintptr{pc}).Next() - if cwd != "" { - if ff, err := filepath.Rel(cwd, frame.File); err == nil { - frame.File = ff - } - } - e.withColor(buf, e.opts.Theme.Source(), func() { - buf.AppendString(frame.File) - buf.AppendByte(':') - buf.AppendInt(int64(frame.Line)) - }) - e.writeColoredString(buf, " > ", e.opts.Theme.AttrKey()) -} - func (e encoder) writeMessage(buf *buffer, level slog.Level, msg string) { if level >= slog.LevelInfo { e.writeColoredString(buf, msg, e.opts.Theme.Message()) @@ -103,6 +86,18 @@ func (e encoder) writeAttr(buf *buffer, a slog.Attr, group string) { if a.Equal(slog.Attr{}) { return } + // Special handling for source attribute. + if a.Key == slog.SourceKey { + if v, ok := a.Value.Any().(*slog.Source); ok { + e.withColor(buf, e.opts.Theme.Source(), func() { + buf.AppendString(v.File) + buf.AppendByte(':') + buf.AppendInt(int64(v.Line)) + }) + e.writeColoredString(buf, " > ", e.opts.Theme.AttrKey()) + return + } + } value := a.Value.Resolve() if value.Kind() == slog.KindGroup { subgroup := a.Key diff --git a/example/main.go b/example/main.go index ca78958..0d8c054 100644 --- a/example/main.go +++ b/example/main.go @@ -4,13 +4,36 @@ import ( "errors" "log/slog" "os" + "strings" "github.com/phsym/console-slog" ) func main() { logger := slog.New( - console.NewHandler(os.Stderr, &console.HandlerOptions{Level: slog.LevelDebug, AddSource: true}), + console.NewHandler(os.Stderr, &console.HandlerOptions{ + Level: slog.LevelDebug, + AddSource: true, + TimeFormat: "15:04:05.000000", + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if v, ok := a.Value.Any().(*slog.Source); ok { + file := v.File + parts := strings.Split(v.File, "/") + if len(parts) > 0 { + file = parts[len(parts)-1] + } + return slog.Attr{ + Key: slog.SourceKey, + Value: slog.AnyValue(&slog.Source{ + Function: v.Function, + File: file, + Line: v.Line, + }), + } + } + return a + }, + }), ) slog.SetDefault(logger) slog.Info("Hello world!", "foo", "bar") diff --git a/handler.go b/handler.go index 82ce3ea..3ef11f2 100644 --- a/handler.go +++ b/handler.go @@ -5,6 +5,8 @@ import ( "io" "log/slog" "os" + "path/filepath" + "runtime" "strings" "sync" "time" @@ -38,6 +40,10 @@ type HandlerOptions struct { // Theme defines the colorized output using ANSI escape sequences Theme Theme + + // See [slog.HandlerOptions] for details. + // Groups are not supported though. + ReplaceAttr func(groups []string, a slog.Attr) slog.Attr } type Handler struct { @@ -87,11 +93,35 @@ func (h *Handler) Handle(_ context.Context, rec slog.Record) error { h.enc.writeTimestamp(buf, rec.Time) h.enc.writeLevel(buf, rec.Level) if h.opts.AddSource && rec.PC > 0 { - h.enc.writeSource(buf, rec.PC, cwd) + frame, _ := runtime.CallersFrames([]uintptr{rec.PC}).Next() + if cwd != "" { + if ff, err := filepath.Rel(cwd, frame.File); err == nil { + frame.File = ff + } + } + a := slog.Attr{ + Key: slog.SourceKey, + Value: slog.AnyValue(&slog.Source{ + Function: frame.Function, + File: frame.File, + Line: frame.Line, + }), + } + if h.opts.ReplaceAttr != nil { + a = h.opts.ReplaceAttr(nil, a) + } + h.enc.writeAttr( + buf, + a, + h.group, + ) } h.enc.writeMessage(buf, rec.Level, rec.Message) buf.copy(&h.context) rec.Attrs(func(a slog.Attr) bool { + if h.opts.ReplaceAttr != nil { + a = h.opts.ReplaceAttr(nil, a) + } h.enc.writeAttr(buf, a, h.group) return true }) diff --git a/handler_test.go b/handler_test.go index eb09629..89bf546 100644 --- a/handler_test.go +++ b/handler_test.go @@ -26,6 +26,29 @@ func TestHandler_TimeFormat(t *testing.T) { AssertEqual(t, expected, buf.String()) } +func TestHandler_ReplaceAttr(t *testing.T) { + ra := func(groups []string, a slog.Attr) slog.Attr { + if a.Key == "test-key" { + return slog.Attr{Key: "testkey", Value: a.Value} + } + if a.Key == "empty" { + return slog.Attr{} + } + return a + } + buf := bytes.Buffer{} + h := NewHandler(&buf, &HandlerOptions{TimeFormat: time.RFC3339Nano, NoColor: true, ReplaceAttr: ra}) + rec := slog.NewRecord(time.Time{}, slog.LevelInfo, "foobar", 0) + rec.AddAttrs( + slog.String("test-key", "test-value"), + slog.String("empty", "should not be logged"), + ) + AssertNoError(t, h.Handle(context.Background(), rec)) + + expected := fmt.Sprintf("INF foobar testkey=test-value\n") + AssertEqual(t, expected, buf.String()) +} + // Handlers should not log the time field if it is zero. // '- If r.Time is the zero time, ignore the time.' // https://pkg.go.dev/log/slog@master#Handler