From 0dcb4779fe74870ea1cb7aa9781bf8f1ab93d022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Sun, 14 Dec 2025 18:48:20 +0100 Subject: [PATCH] Add pretty slog handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- go.mod | 3 + go.sum | 9 +++ log/buffer.go | 20 ++++++ log/log.go | 12 +++- log/pretty_handler.go | 152 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 log/buffer.go create mode 100644 log/pretty_handler.go diff --git a/go.mod b/go.mod index f41e48b..1ef13f3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module go.gearno.de/kit go 1.25.0 require ( + github.com/fatih/color v1.18.0 github.com/go-chi/chi/v5 v5.2.3 github.com/jackc/pgx/v5 v5.7.6 github.com/prometheus/client_golang v1.23.2 @@ -29,6 +30,8 @@ require ( github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/compress v1.18.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect diff --git a/go.sum b/go.sum index ea315af..dcc8ac5 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -38,6 +40,11 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -93,6 +100,8 @@ golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= diff --git a/log/buffer.go b/log/buffer.go new file mode 100644 index 0000000..91a6b42 --- /dev/null +++ b/log/buffer.go @@ -0,0 +1,20 @@ +package log + +import ( + "bytes" + "sync" +) + +var bufPool = sync.Pool{ + New: func() any { + return &bytes.Buffer{} + }, +} + +func getBuffer() *bytes.Buffer { + return bufPool.Get().(*bytes.Buffer) +} + +func freeBuffer(bf *bytes.Buffer) { + bufPool.Put(bf) +} diff --git a/log/log.go b/log/log.go index a26b5fb..5b02f29 100644 --- a/log/log.go +++ b/log/log.go @@ -58,8 +58,9 @@ var ( LevelWarn = slog.LevelWarn LevelDebug = slog.LevelDebug - FormatJSON Format = "json" - FormatText Format = "text" + FormatJSON Format = "json" + FormatPretty Format = "pretty" + FormatText Format = "text" ) // WithLevel sets the logging level for the Logger. @@ -166,6 +167,13 @@ func NewLogger(options ...Option) *Logger { var handler slog.Handler switch l.format { + case FormatPretty: + handler = NewPrettyHandler( + l.output, + &slog.HandlerOptions{ + Level: l.level, + }, + ) case FormatText: handler = slog.NewTextHandler( l.output, diff --git a/log/pretty_handler.go b/log/pretty_handler.go new file mode 100644 index 0000000..6c210eb --- /dev/null +++ b/log/pretty_handler.go @@ -0,0 +1,152 @@ +package log + +import ( + "context" + "fmt" + "io" + "log/slog" + "runtime" + "strings" + "sync" + "time" + + "github.com/fatih/color" +) + +// Handler is a colored slog handler. +type PrettyHandler struct { + groups []string + attrs []slog.Attr + + opts slog.HandlerOptions + + mu *sync.Mutex + out io.Writer +} + +var LevelTags = map[slog.Level]string{ + slog.LevelDebug: color.New(color.FgWhite, color.Bold).Sprint("DEBUG"), + slog.LevelInfo: color.New(color.FgBlue, color.Bold).Sprint("INFO"), + slog.LevelWarn: color.New(color.FgYellow, color.Bold).Sprint("WARN"), + slog.LevelError: color.New(color.FgRed, color.Bold).Sprint("ERROR"), +} + +// NewHandler creates a new [Handler] with the specified options. If opts is nil, uses [DefaultOptions]. +func NewPrettyHandler(out io.Writer, opts *slog.HandlerOptions) *PrettyHandler { + h := &PrettyHandler{out: out, mu: &sync.Mutex{}} + if opts == nil { + opts = &slog.HandlerOptions{} + } + h.opts = *opts + + return h +} + +func (h *PrettyHandler) clone() *PrettyHandler { + return &PrettyHandler{ + groups: h.groups, + attrs: h.attrs, + opts: h.opts, + mu: h.mu, + out: h.out, + } +} + +// Enabled implements slog.Handler.Enabled . +func (h *PrettyHandler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.opts.Level.Level() +} + +// Handle implements slog.Handler.Handle . +func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error { + bf := getBuffer() + bf.Reset() + + fmt.Fprint(bf, color.New(color.Faint).Sprint(r.Time.Format(time.RFC3339))) + fmt.Fprint(bf, " ") + + fmt.Fprint(bf, LevelTags[r.Level]) + fmt.Fprint(bf, " ") + + // we need the attributes here, as we can print a longer string if there are no attributes + stacktrace := "" + name := "" + var attrs []slog.Attr + attrs = append(attrs, h.attrs...) + r.Attrs(func(a slog.Attr) bool { + if a.Key == "stack" { + stacktrace = a.Value.String() + return true + } + if a.Key == "name" { + name = a.Value.String() + return true + } + attrs = append(attrs, a) + return true + }) + + if name != "" { + fmt.Fprint(bf, color.New(color.Faint, color.Bold).Sprint(name)) + fmt.Fprint(bf, " ") + } + + if stacktrace != "" { + if r.PC != 0 { + f, _ := runtime.CallersFrames([]uintptr{r.PC}).Next() + + filename := f.File + lineStr := fmt.Sprintf(":%d", f.Line) + formatted := fmt.Sprintf("%s ", filename+lineStr) + fmt.Fprint(bf, formatted) + } + } + + fmt.Fprint(bf, color.New(color.FgHiWhite).Sprint(r.Message)) + + for _, a := range attrs { + fmt.Fprint(bf, " ") + for i, g := range h.groups { + fmt.Fprint(bf, color.New(color.FgWhite).Sprint(g)) + if i != len(h.groups) { + fmt.Fprint(bf, color.New(color.FgWhite).Sprint(".")) + } + } + + value := color.New(color.FgWhite).Sprint(a.Value.String()) + if strings.Contains(a.Key, "err") { + fmt.Fprint(bf, color.New(color.FgRed).Sprintf("%s=", a.Key)+value) + } else { + fmt.Fprint(bf, color.New(color.Faint).Sprintf("%s=", a.Key)+value) + } + } + + if stacktrace != "" { + fmt.Fprint(bf, "\n") + fmt.Fprint(bf, stacktrace) + } + + fmt.Fprint(bf, "\n") + + h.mu.Lock() + _, err := io.Copy(h.out, bf) + h.mu.Unlock() + + freeBuffer(bf) + + return err +} + +// WithGroup implements slog.Handler.WithGroup . +func (h *PrettyHandler) WithGroup(name string) slog.Handler { + h2 := h.clone() + h2.groups = append(h2.groups, name) + return h2 +} + +// WithAttrs implements slog.Handler.WithAttrs . +func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + h2 := h.clone() + h2.attrs = append(h2.attrs, attrs...) + return h2 +}