Skip to content

Commit 0dcb477

Browse files
committed
Add pretty slog handler
Signed-off-by: Émile Ré <nemile.re@gmail.com>
1 parent 33c5834 commit 0dcb477

5 files changed

Lines changed: 194 additions & 2 deletions

File tree

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module go.gearno.de/kit
33
go 1.25.0
44

55
require (
6+
github.com/fatih/color v1.18.0
67
github.com/go-chi/chi/v5 v5.2.3
78
github.com/jackc/pgx/v5 v5.7.6
89
github.com/prometheus/client_golang v1.23.2
@@ -29,6 +30,8 @@ require (
2930
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
3031
github.com/jackc/puddle/v2 v2.2.2 // indirect
3132
github.com/klauspost/compress v1.18.2 // indirect
33+
github.com/mattn/go-colorable v0.1.13 // indirect
34+
github.com/mattn/go-isatty v0.0.20 // indirect
3235
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
3336
github.com/pmezard/go-difflib v1.0.0 // indirect
3437
github.com/prometheus/client_model v0.6.2 // indirect

go.sum

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
77
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
88
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
99
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10+
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
11+
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
1012
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
1113
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
1214
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=
3840
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
3941
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
4042
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
43+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
44+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
45+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
46+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
47+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
4148
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
4249
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
4350
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=
93100
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
94101
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
95102
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
103+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
104+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
96105
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
97106
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
98107
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=

log/buffer.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package log
2+
3+
import (
4+
"bytes"
5+
"sync"
6+
)
7+
8+
var bufPool = sync.Pool{
9+
New: func() any {
10+
return &bytes.Buffer{}
11+
},
12+
}
13+
14+
func getBuffer() *bytes.Buffer {
15+
return bufPool.Get().(*bytes.Buffer)
16+
}
17+
18+
func freeBuffer(bf *bytes.Buffer) {
19+
bufPool.Put(bf)
20+
}

log/log.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ var (
5858
LevelWarn = slog.LevelWarn
5959
LevelDebug = slog.LevelDebug
6060

61-
FormatJSON Format = "json"
62-
FormatText Format = "text"
61+
FormatJSON Format = "json"
62+
FormatPretty Format = "pretty"
63+
FormatText Format = "text"
6364
)
6465

6566
// WithLevel sets the logging level for the Logger.
@@ -166,6 +167,13 @@ func NewLogger(options ...Option) *Logger {
166167

167168
var handler slog.Handler
168169
switch l.format {
170+
case FormatPretty:
171+
handler = NewPrettyHandler(
172+
l.output,
173+
&slog.HandlerOptions{
174+
Level: l.level,
175+
},
176+
)
169177
case FormatText:
170178
handler = slog.NewTextHandler(
171179
l.output,

log/pretty_handler.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package log
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"log/slog"
8+
"runtime"
9+
"strings"
10+
"sync"
11+
"time"
12+
13+
"github.com/fatih/color"
14+
)
15+
16+
// Handler is a colored slog handler.
17+
type PrettyHandler struct {
18+
groups []string
19+
attrs []slog.Attr
20+
21+
opts slog.HandlerOptions
22+
23+
mu *sync.Mutex
24+
out io.Writer
25+
}
26+
27+
var LevelTags = map[slog.Level]string{
28+
slog.LevelDebug: color.New(color.FgWhite, color.Bold).Sprint("DEBUG"),
29+
slog.LevelInfo: color.New(color.FgBlue, color.Bold).Sprint("INFO"),
30+
slog.LevelWarn: color.New(color.FgYellow, color.Bold).Sprint("WARN"),
31+
slog.LevelError: color.New(color.FgRed, color.Bold).Sprint("ERROR"),
32+
}
33+
34+
// NewHandler creates a new [Handler] with the specified options. If opts is nil, uses [DefaultOptions].
35+
func NewPrettyHandler(out io.Writer, opts *slog.HandlerOptions) *PrettyHandler {
36+
h := &PrettyHandler{out: out, mu: &sync.Mutex{}}
37+
if opts == nil {
38+
opts = &slog.HandlerOptions{}
39+
}
40+
h.opts = *opts
41+
42+
return h
43+
}
44+
45+
func (h *PrettyHandler) clone() *PrettyHandler {
46+
return &PrettyHandler{
47+
groups: h.groups,
48+
attrs: h.attrs,
49+
opts: h.opts,
50+
mu: h.mu,
51+
out: h.out,
52+
}
53+
}
54+
55+
// Enabled implements slog.Handler.Enabled .
56+
func (h *PrettyHandler) Enabled(_ context.Context, level slog.Level) bool {
57+
return level >= h.opts.Level.Level()
58+
}
59+
60+
// Handle implements slog.Handler.Handle .
61+
func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error {
62+
bf := getBuffer()
63+
bf.Reset()
64+
65+
fmt.Fprint(bf, color.New(color.Faint).Sprint(r.Time.Format(time.RFC3339)))
66+
fmt.Fprint(bf, " ")
67+
68+
fmt.Fprint(bf, LevelTags[r.Level])
69+
fmt.Fprint(bf, " ")
70+
71+
// we need the attributes here, as we can print a longer string if there are no attributes
72+
stacktrace := ""
73+
name := ""
74+
var attrs []slog.Attr
75+
attrs = append(attrs, h.attrs...)
76+
r.Attrs(func(a slog.Attr) bool {
77+
if a.Key == "stack" {
78+
stacktrace = a.Value.String()
79+
return true
80+
}
81+
if a.Key == "name" {
82+
name = a.Value.String()
83+
return true
84+
}
85+
attrs = append(attrs, a)
86+
return true
87+
})
88+
89+
if name != "" {
90+
fmt.Fprint(bf, color.New(color.Faint, color.Bold).Sprint(name))
91+
fmt.Fprint(bf, " ")
92+
}
93+
94+
if stacktrace != "" {
95+
if r.PC != 0 {
96+
f, _ := runtime.CallersFrames([]uintptr{r.PC}).Next()
97+
98+
filename := f.File
99+
lineStr := fmt.Sprintf(":%d", f.Line)
100+
formatted := fmt.Sprintf("%s ", filename+lineStr)
101+
fmt.Fprint(bf, formatted)
102+
}
103+
}
104+
105+
fmt.Fprint(bf, color.New(color.FgHiWhite).Sprint(r.Message))
106+
107+
for _, a := range attrs {
108+
fmt.Fprint(bf, " ")
109+
for i, g := range h.groups {
110+
fmt.Fprint(bf, color.New(color.FgWhite).Sprint(g))
111+
if i != len(h.groups) {
112+
fmt.Fprint(bf, color.New(color.FgWhite).Sprint("."))
113+
}
114+
}
115+
116+
value := color.New(color.FgWhite).Sprint(a.Value.String())
117+
if strings.Contains(a.Key, "err") {
118+
fmt.Fprint(bf, color.New(color.FgRed).Sprintf("%s=", a.Key)+value)
119+
} else {
120+
fmt.Fprint(bf, color.New(color.Faint).Sprintf("%s=", a.Key)+value)
121+
}
122+
}
123+
124+
if stacktrace != "" {
125+
fmt.Fprint(bf, "\n")
126+
fmt.Fprint(bf, stacktrace)
127+
}
128+
129+
fmt.Fprint(bf, "\n")
130+
131+
h.mu.Lock()
132+
_, err := io.Copy(h.out, bf)
133+
h.mu.Unlock()
134+
135+
freeBuffer(bf)
136+
137+
return err
138+
}
139+
140+
// WithGroup implements slog.Handler.WithGroup .
141+
func (h *PrettyHandler) WithGroup(name string) slog.Handler {
142+
h2 := h.clone()
143+
h2.groups = append(h2.groups, name)
144+
return h2
145+
}
146+
147+
// WithAttrs implements slog.Handler.WithAttrs .
148+
func (h *PrettyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
149+
h2 := h.clone()
150+
h2.attrs = append(h2.attrs, attrs...)
151+
return h2
152+
}

0 commit comments

Comments
 (0)