11package devslog
22
33import (
4+ "bytes"
45 "context"
56 "fmt"
67 "io"
@@ -11,11 +12,10 @@ import (
1112
1213// A Handler handles log records produced by a Logger.
1314type 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.
4646func (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.
5856func (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.
7066func (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