Skip to content

Commit 1f5ab2b

Browse files
committed
Add pretty slog handler
Signed-off-by: Émile Ré <nemile.re@gmail.com>
1 parent 84aa307 commit 1f5ab2b

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
@@ -5,6 +5,7 @@ go 1.23.0
55
toolchain go1.23.5
66

77
require (
8+
github.com/fatih/color v1.18.0
89
github.com/go-chi/chi/v5 v5.2.1
910
github.com/jackc/pgx/v5 v5.7.3
1011
github.com/prometheus/client_golang v1.21.1
@@ -31,6 +32,8 @@ require (
3132
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
3233
github.com/jackc/puddle/v2 v2.2.2 // indirect
3334
github.com/klauspost/compress v1.18.0 // indirect
35+
github.com/mattn/go-colorable v0.1.13 // indirect
36+
github.com/mattn/go-isatty v0.0.20 // indirect
3437
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
3538
github.com/pmezard/go-difflib v1.0.0 // indirect
3639
github.com/prometheus/client_model v0.6.1 // 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.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
1113
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
1214
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -39,6 +41,11 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
3941
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
4042
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
4143
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
44+
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
45+
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
46+
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
47+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
48+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
4249
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
4350
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
4451
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -90,6 +97,8 @@ golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
9097
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
9198
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
9299
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
100+
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
93102
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
94103
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
95104
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=

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)