Skip to content

Commit 30bb064

Browse files
authored
Merge pull request #2 from rafaelespinoza/feat-group_attrs
feat: indent group attributes, add tests
2 parents 6dea6a5 + 04762d9 commit 30bb064

2 files changed

Lines changed: 350 additions & 26 deletions

File tree

devslog.go

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package devslog
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"io"
@@ -11,11 +12,10 @@ import (
1112

1213
// A Handler handles log records produced by a Logger.
1314
type Handler struct {
14-
attrs []slog.Attr
15-
groups []string
16-
opts slog.HandlerOptions
17-
mu *sync.Mutex
18-
w io.Writer
15+
opts slog.HandlerOptions
16+
mu *sync.Mutex
17+
w io.Writer
18+
goas []groupOrAttrs
1919
}
2020

2121
// NewHandler creates a handler that writes to w, using the given options.
@@ -44,52 +44,142 @@ func (h *Handler) Enabled(_ context.Context, level slog.Level) bool {
4444
// WithAttrs returns a new handler whose attributes consists of h's attributes
4545
// followed by attrs.
4646
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
47-
return &Handler{
48-
attrs: append(h.attrs, attrs...),
49-
groups: h.groups,
50-
opts: h.opts,
51-
mu: h.mu,
52-
w: h.w,
47+
if len(attrs) < 1 {
48+
return h
5349
}
50+
51+
return h.withGroupOrAttrs(groupOrAttrs{attrs: attrs})
5452
}
5553

5654
// WithGroup returns a new Handler with the given group appended to
5755
// the receiver's existing groups.
5856
func (h *Handler) WithGroup(name string) slog.Handler {
59-
return &Handler{
60-
attrs: h.attrs,
61-
groups: append(h.groups, name),
62-
opts: h.opts,
63-
mu: h.mu,
64-
w: h.w,
57+
if name == "" {
58+
return h
6559
}
60+
61+
return h.withGroupOrAttrs(groupOrAttrs{group: name})
6662
}
6763

6864
// Handle formats its argument Record so that message is followed by each
6965
// of it's attributes on seperate lines.
7066
func (h *Handler) Handle(_ context.Context, r slog.Record) error {
71-
var attrs string
67+
var buf bytes.Buffer
7268

73-
for _, a := range h.attrs {
74-
if !a.Equal(slog.Attr{}) {
75-
attrs += gray(" ↳ " + a.Key + ": " + a.Value.String() + "\n")
76-
}
69+
// From slog handler docs:
70+
// If r.Time is the zero time, ignore the time.
71+
if !r.Time.IsZero() {
72+
_, _ = buf.WriteString(r.Time.Format(time.TimeOnly) + " ")
7773
}
74+
_, _ = buf.WriteString(text(levelColour(r.Level), r.Level.String()) + " " + r.Message + "\n")
7875

79-
r.Attrs(func(a slog.Attr) bool {
80-
if !a.Equal(slog.Attr{}) {
81-
attrs += gray(" ↳ " + a.Key + ": " + a.Value.String() + "\n")
76+
// In this handler, each attribute that is not one of the built-in attributes
77+
// is written on its own line. For group attributes, use indentation level to
78+
// display different levels.
79+
var indentLevel int
80+
goas := h.goas
81+
if r.NumAttrs() == 0 {
82+
// If the record has no Attrs, remove groups at the end of the list; they are empty.
83+
for len(goas) > 0 && goas[len(goas)-1].group != "" {
84+
goas = goas[:len(goas)-1]
85+
}
86+
}
87+
for _, goa := range goas {
88+
if goa.group != "" {
89+
_, _ = fmt.Fprintf(&buf, "%*s %s %s:\n", indentLevel*numSpacesPerLevel, "", attrPrefix, gray(goa.group))
90+
indentLevel++
91+
} else {
92+
for _, a := range goa.attrs {
93+
h.appendAttr(&buf, a, indentLevel)
94+
}
8295
}
96+
}
97+
r.Attrs(func(a slog.Attr) bool {
98+
h.appendAttr(&buf, a, indentLevel)
8399
return true
84100
})
85101

86102
h.mu.Lock()
87-
_, err := fmt.Fprintf(h.w, "%s %s %s\n%s", r.Time.Format(time.TimeOnly), text(levelColour(r.Level), r.Level.String()), r.Message, attrs)
103+
_, err := h.w.Write(buf.Bytes())
88104
h.mu.Unlock()
89105

90106
return err
91107
}
92108

109+
const (
110+
// attrPrefix denotes that another attribute value will be printed in the
111+
// output. For this handler, it will be preceded by a newline character.
112+
attrPrefix = "↳"
113+
// kvd is the key value delimiter output between an attribute's key and value.
114+
kvd = ":"
115+
// numSpacesPerLevel is an indentation value for spacing group attributes.
116+
numSpacesPerLevel = 4
117+
)
118+
119+
func (h *Handler) appendAttr(buf *bytes.Buffer, a slog.Attr, indentLevel int) {
120+
// From slog handler docs:
121+
// Attr's values should be resolved.
122+
a.Value = a.Value.Resolve()
123+
124+
// From slog handler docs:
125+
// If an Attr's key and value are both the zero value, ignore the Attr.
126+
if a.Equal(slog.Attr{}) {
127+
return
128+
}
129+
130+
_, _ = fmt.Fprintf(buf, "%*s", indentLevel*numSpacesPerLevel, "")
131+
switch a.Value.Kind() {
132+
case slog.KindString:
133+
_, _ = fmt.Fprintf(buf, " %s %s%s %s\n", attrPrefix, gray(a.Key), kvd, a.Value.String())
134+
case slog.KindTime:
135+
// Write times in the same layout as the built-in time attribute.
136+
_, _ = fmt.Fprintf(buf, " %s %s%s %s\n", attrPrefix, gray(a.Key), kvd, a.Value.Time().Format(time.TimeOnly))
137+
case slog.KindGroup:
138+
attrs := a.Value.Group()
139+
140+
// From slog handler docs:
141+
// If a group has no Attrs (even if it has a non-empty key), ignore it.
142+
if len(attrs) == 0 {
143+
return
144+
}
145+
146+
// If the key is non-empty, write it out and indent the rest of the attrs.
147+
// Otherwise, inline the attrs.
148+
if a.Key != "" {
149+
_, _ = fmt.Fprintf(buf, " %s %s:\n", attrPrefix, gray(a.Key))
150+
indentLevel++
151+
}
152+
153+
for _, ga := range attrs {
154+
h.appendAttr(buf, ga, indentLevel)
155+
}
156+
default:
157+
_, _ = fmt.Fprintf(buf, " %s %s%s %s\n", attrPrefix, gray(a.Key), kvd, a.Value)
158+
}
159+
}
160+
161+
// withGroupOrAttrs is for use in the Handler's WithAttrs or WithGroup methods.
162+
// The slog.Handler docs say that those methods must return a new Handler. So
163+
// this method clones the handler state but makes a deep copy of the goas field
164+
// with a new value at the end. The goal is to avoid potentially shared state
165+
// with another handler instance, should either of them append to the same
166+
// underlying array variable. So avoid that situation by making a deep copy.
167+
func (h *Handler) withGroupOrAttrs(goa groupOrAttrs) *Handler {
168+
out := *h
169+
out.goas = make([]groupOrAttrs, len(h.goas)+1)
170+
copy(out.goas, h.goas)
171+
out.goas[len(out.goas)-1] = goa
172+
return &out
173+
}
174+
175+
// groupOrAttrs holds either a group name or a list of slog.Attrs.
176+
// It is lifted from the slog-handler-guide at:
177+
// https://github.com/golang/example/blob/master/slog-handler-guide
178+
type groupOrAttrs struct {
179+
group string // group name if non-empty
180+
attrs []slog.Attr // attrs if non-empty
181+
}
182+
93183
// SetDefault is syntactic sugar for constructing a new devslog handler
94184
// and setting it as the default [slog.Logger]. The top-level slog
95185
// functions [slog.Info], [slog.Debug], etc will all use this handler

0 commit comments

Comments
 (0)