Skip to content

Commit 9ae8593

Browse files
authored
feat: add slog handler (#123)
1 parent 1678bf2 commit 9ae8593

4 files changed

Lines changed: 805 additions & 0 deletions

File tree

bench_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package logger_test
22

33
import (
4+
"log/slog"
45
"os"
56
"testing"
67

@@ -101,6 +102,90 @@ func BenchmarkLogger_JsonCtx(b *testing.B) {
101102
})
102103
}
103104

105+
func BenchmarkHandler_Logfmt(b *testing.B) {
106+
h := logger.NewHandler(discard{}, logger.LogfmtFormat(), slog.LevelDebug)
107+
log := slog.New(h)
108+
109+
b.ReportAllocs()
110+
b.ResetTimer()
111+
b.RunParallel(func(pb *testing.PB) {
112+
for pb.Next() {
113+
log.Error("some message")
114+
}
115+
})
116+
}
117+
118+
func BenchmarkHandler_Json(b *testing.B) {
119+
h := logger.NewHandler(discard{}, logger.JSONFormat(), slog.LevelDebug)
120+
log := slog.New(h)
121+
122+
b.ReportAllocs()
123+
b.ResetTimer()
124+
b.RunParallel(func(pb *testing.PB) {
125+
for pb.Next() {
126+
log.Error("some message")
127+
}
128+
})
129+
}
130+
131+
func BenchmarkHandler_LogfmtWithGroup(b *testing.B) {
132+
h := logger.NewHandler(discard{}, logger.LogfmtFormat(), slog.LevelDebug)
133+
log := slog.New(h.WithGroup("service"))
134+
135+
b.ReportAllocs()
136+
b.ResetTimer()
137+
b.RunParallel(func(pb *testing.PB) {
138+
for pb.Next() {
139+
log.Error("some message")
140+
}
141+
})
142+
}
143+
144+
func BenchmarkHandler_JsonWithGroup(b *testing.B) {
145+
h := logger.NewHandler(discard{}, logger.JSONFormat(), slog.LevelDebug)
146+
log := slog.New(h.WithGroup("service"))
147+
148+
b.ReportAllocs()
149+
b.ResetTimer()
150+
b.RunParallel(func(pb *testing.PB) {
151+
for pb.Next() {
152+
log.Error("some message")
153+
}
154+
})
155+
}
156+
157+
func BenchmarkHandler_LogfmtWithGroupAndAttrs(b *testing.B) {
158+
h := logger.NewHandler(discard{}, logger.LogfmtFormat(), slog.LevelDebug)
159+
log := slog.New(h.WithGroup("db").WithAttrs([]slog.Attr{
160+
slog.String("host", "localhost"),
161+
slog.Int("port", 5432),
162+
}))
163+
164+
b.ReportAllocs()
165+
b.ResetTimer()
166+
b.RunParallel(func(pb *testing.PB) {
167+
for pb.Next() {
168+
log.Error("some message", slog.String("driver", "pgx"))
169+
}
170+
})
171+
}
172+
173+
func BenchmarkHandler_JsonWithGroupAndAttrs(b *testing.B) {
174+
h := logger.NewHandler(discard{}, logger.JSONFormat(), slog.LevelDebug)
175+
log := slog.New(h.WithGroup("db").WithAttrs([]slog.Attr{
176+
slog.String("host", "localhost"),
177+
slog.Int("port", 5432),
178+
}))
179+
180+
b.ReportAllocs()
181+
b.ResetTimer()
182+
b.RunParallel(func(pb *testing.PB) {
183+
for pb.Next() {
184+
log.Error("some message", slog.String("driver", "pgx"))
185+
}
186+
})
187+
}
188+
104189
func BenchmarkLevelLogger_Logfmt(b *testing.B) {
105190
log := logger.New(discard{}, logger.LogfmtFormat(), logger.Debug).With(ctx.Str("_n", "bench"), ctx.Int("_p", os.Getpid()))
106191

example_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package logger_test
22

33
import (
4+
"log/slog"
45
"os"
56

67
"github.com/hamba/logger/v2"
@@ -18,3 +19,19 @@ func ExampleSyncWriter() {
1819

1920
log.Info("redis connection", ctx.Str("redis", "some redis name"), ctx.Int("timeout", 10))
2021
}
22+
23+
func ExampleNewHandler() {
24+
h := logger.NewHandler(os.Stdout, logger.JSONFormat(), slog.LevelInfo)
25+
26+
log := slog.New(h).With(slog.String("env", "prod")).WithGroup("db")
27+
28+
log.Info("connected", slog.String("driver", "pgx"))
29+
}
30+
31+
func ExampleNewHandler_logfmt() {
32+
h := logger.NewHandler(os.Stdout, logger.LogfmtFormat(), slog.LevelInfo)
33+
34+
log := slog.New(h).With(slog.String("env", "prod")).WithGroup("db")
35+
36+
log.Info("connected", slog.String("driver", "pgx"))
37+
}

slog.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package logger
2+
3+
import (
4+
"context"
5+
"io"
6+
"log/slog"
7+
)
8+
9+
// Handler is a slog.Handler backed by hamba/logger. It does not synchronize writes;
10+
// wrap the writer with NewSyncWriter for concurrent use in non-posix environments.
11+
type Handler struct {
12+
w io.Writer
13+
isDiscard bool
14+
fmtr Formatter
15+
lvl slog.Level
16+
17+
ctx []byte
18+
prefix []byte
19+
20+
openGroups int
21+
}
22+
23+
// NewHandler returns a new Handler.
24+
func NewHandler(w io.Writer, fmtr Formatter, lvl slog.Level) *Handler {
25+
return &Handler{
26+
w: w,
27+
isDiscard: w == io.Discard,
28+
fmtr: fmtr,
29+
lvl: lvl,
30+
}
31+
}
32+
33+
// Enabled returns false when lvl is below the configured minimum or the
34+
// underlying writer is io.Discard.
35+
func (h *Handler) Enabled(_ context.Context, lvl slog.Level) bool {
36+
return !h.isDiscard && lvl >= h.lvl
37+
}
38+
39+
// Handle writes the record to the underlying writer.
40+
func (h *Handler) Handle(_ context.Context, r slog.Record) error {
41+
e := newEvent(h.fmtr)
42+
e.prefix = append(e.prefix, h.prefix...)
43+
44+
e.fmtr.AppendBeginMarker(e.buf)
45+
e.fmtr.WriteMessage(e.buf, r.Time, mapSlogLevel(r.Level), r.Message)
46+
e.buf.Write(h.ctx)
47+
48+
r.Attrs(func(a slog.Attr) bool {
49+
appendAttr(e, a)
50+
return true
51+
})
52+
53+
for range h.openGroups {
54+
e.prefix = e.fmtr.AppendGroupEnd(e.buf, e.prefix)
55+
}
56+
57+
e.fmtr.AppendEndMarker(e.buf)
58+
e.fmtr.AppendLineBreak(e.buf)
59+
60+
_, err := h.w.Write(e.buf.Bytes())
61+
putEvent(e)
62+
return err
63+
}
64+
65+
// WithAttrs returns a new Handler with attrs pre-serialised into the context.
66+
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
67+
if len(attrs) == 0 {
68+
return h
69+
}
70+
71+
e := newEvent(h.fmtr)
72+
e.buf.Write(h.ctx)
73+
e.prefix = append(e.prefix, h.prefix...)
74+
75+
for _, a := range attrs {
76+
appendAttr(e, a)
77+
}
78+
79+
newCtx := make([]byte, e.buf.Len())
80+
copy(newCtx, e.buf.Bytes())
81+
82+
putEvent(e)
83+
84+
return &Handler{
85+
fmtr: h.fmtr,
86+
w: h.w,
87+
isDiscard: h.isDiscard,
88+
lvl: h.lvl,
89+
ctx: newCtx,
90+
prefix: h.prefix,
91+
openGroups: h.openGroups,
92+
}
93+
}
94+
95+
// WithGroup returns a new Handler with name appended to the group stack.
96+
// An empty name is a no-op per the slog spec.
97+
func (h *Handler) WithGroup(name string) slog.Handler {
98+
if name == "" {
99+
return h
100+
}
101+
102+
e := newEvent(h.fmtr)
103+
e.buf.Write(h.ctx)
104+
e.prefix = append(e.prefix, h.prefix...)
105+
106+
e.prefix = h.fmtr.AppendGroupStart(e.buf, e.prefix, name)
107+
108+
newCtx := make([]byte, e.buf.Len())
109+
copy(newCtx, e.buf.Bytes())
110+
newPrefix := make([]byte, len(e.prefix))
111+
copy(newPrefix, e.prefix)
112+
113+
putEvent(e)
114+
115+
return &Handler{
116+
fmtr: h.fmtr,
117+
w: h.w,
118+
isDiscard: h.isDiscard,
119+
lvl: h.lvl,
120+
ctx: newCtx,
121+
prefix: newPrefix,
122+
openGroups: h.openGroups + 1,
123+
}
124+
}
125+
126+
func appendAttr(e *Event, a slog.Attr) {
127+
a.Value = a.Value.Resolve()
128+
if a.Equal(slog.Attr{}) {
129+
return
130+
}
131+
132+
switch a.Value.Kind() {
133+
case slog.KindGroup:
134+
appendGroup(e, a.Key, a.Value)
135+
case slog.KindString:
136+
e.AppendString(a.Key, a.Value.String())
137+
case slog.KindInt64:
138+
e.AppendInt(a.Key, a.Value.Int64())
139+
case slog.KindUint64:
140+
e.AppendUint(a.Key, a.Value.Uint64())
141+
case slog.KindFloat64:
142+
e.AppendFloat(a.Key, a.Value.Float64())
143+
case slog.KindBool:
144+
e.AppendBool(a.Key, a.Value.Bool())
145+
case slog.KindTime:
146+
e.AppendTime(a.Key, a.Value.Time())
147+
case slog.KindDuration:
148+
e.AppendDuration(a.Key, a.Value.Duration())
149+
default:
150+
e.AppendInterface(a.Key, a.Value.Any())
151+
}
152+
}
153+
154+
func appendGroup(e *Event, name string, val slog.Value) {
155+
subs := val.Group()
156+
if len(subs) == 0 {
157+
return
158+
}
159+
160+
if name == "" {
161+
// Per the slog spec, an anonymous group is flattened into the
162+
// enclosing scope.
163+
for _, a := range subs {
164+
appendAttr(e, a)
165+
}
166+
return
167+
}
168+
169+
e.OpenGroup(name)
170+
for _, a := range subs {
171+
appendAttr(e, a)
172+
}
173+
e.CloseGroup()
174+
}
175+
176+
// mapSlogLevel maps to logger.Level. Custom levels below Debug clamp to
177+
// Debug; above Error clamp to Error. Trace and Crit are never produced.
178+
func mapSlogLevel(lvl slog.Level) Level {
179+
switch {
180+
case lvl >= slog.LevelError:
181+
return Error
182+
case lvl >= slog.LevelWarn:
183+
return Warn
184+
case lvl >= slog.LevelInfo:
185+
return Info
186+
default:
187+
return Debug
188+
}
189+
}

0 commit comments

Comments
 (0)