slog: reimplement defaultHandler using commonHandler
The defaultHandler can be viewed as a variant of a TextHandler.
Re-use commonHandler to implement it.
Test groups, Marshaler and scopes for it.
Change-Id: I7600e5b82d4d8189024ae94f760848eaedf5d4db
Reviewed-on: https://go-review.googlesource.com/c/exp/+/439055
Reviewed-by: Alan Donovan <adonovan@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
diff --git a/slog/handler.go b/slog/handler.go
index 5b52559..c00e5c9 100644
--- a/slog/handler.go
+++ b/slog/handler.go
@@ -8,7 +8,6 @@
"fmt"
"io"
"strconv"
- "strings"
"sync"
"time"
@@ -61,8 +60,16 @@
}
type defaultHandler struct {
- attrs []Attr
- output func(int, string) error // log.Output, except for testing
+ ch *commonHandler
+ // log.Output, except for testing
+ output func(calldepth int, message string) error
+}
+
+func newDefaultHandler(output func(int, string) error) *defaultHandler {
+ return &defaultHandler{
+ ch: &commonHandler{json: false},
+ output: output,
+ }
}
func (*defaultHandler) Enabled(l Level) bool {
@@ -73,34 +80,23 @@
// write it with the default log.Logger.
// Let the log.Logger handle time and file/line.
func (h *defaultHandler) Handle(r Record) error {
- var b strings.Builder
- b.WriteString(r.Level().String())
- b.WriteByte(' ')
- for _, a := range h.attrs {
- h.writeAttr(&b, a)
- }
- r.Attrs(func(a Attr) {
- h.writeAttr(&b, a)
- })
- b.WriteString(r.Message())
- return h.output(4, b.String())
+ buf := buffer.New()
+ defer buf.Free()
+ buf.WriteString(r.Level().String())
+ buf.WriteByte(' ')
+ buf.WriteString(r.Message())
+ state := handleState{h: h.ch, buf: buf, sep: " "}
+ state.appendNonBuiltIns(r)
+ // 4 = log.Output depth + handlerWriter.Write + defaultHandler.Handle
+ return h.output(4, buf.String())
}
-func (h *defaultHandler) writeAttr(b *strings.Builder, a Attr) {
- b.WriteString(a.Key)
- b.WriteByte('=')
- b.WriteString(a.Value.Resolve().String())
- b.WriteByte(' ')
-}
-
-func (d *defaultHandler) With(as []Attr) Handler {
- d2 := *d
- d2.attrs = concat(d2.attrs, as)
- return &d2
+func (h *defaultHandler) With(as []Attr) Handler {
+ return &defaultHandler{h.ch.withAttrs(as), h.output}
}
func (h *defaultHandler) WithScope(name string) Handler {
- panic("unimplemented")
+ return &defaultHandler{h.ch.withScope(name), h.output}
}
// HandlerOptions are options for a TextHandler or JSONHandler.
@@ -254,28 +250,7 @@
} else {
state.appendAttr(String(key, msg))
}
- // preformatted Attrs
- if len(h.preformattedAttrs) > 0 {
- state.buf.WriteString(state.sep)
- state.buf.Write(h.preformattedAttrs)
- state.sep = h.attrSep()
- }
- // Attrs in Record -- unlike the built-in ones, they are scoped.
- state.prefix = buffer.New()
- defer state.prefix.Free()
- state.prefix.WriteString(h.scopePrefix)
- state.openScopes()
- r.Attrs(func(a Attr) {
- state.appendAttr(a)
- })
- if h.json {
- // Close all open scopes.
- for range h.scopes {
- state.buf.WriteByte('}')
- }
- // Close the top-level object.
- state.buf.WriteByte('}')
- }
+ state.appendNonBuiltIns(r)
state.buf.WriteByte('\n')
h.mu.Lock()
@@ -284,6 +259,31 @@
return err
}
+func (s *handleState) appendNonBuiltIns(r Record) {
+ // preformatted Attrs
+ if len(s.h.preformattedAttrs) > 0 {
+ s.buf.WriteString(s.sep)
+ s.buf.Write(s.h.preformattedAttrs)
+ s.sep = s.h.attrSep()
+ }
+ // Attrs in Record -- unlike the built-in ones, they are scoped.
+ s.prefix = buffer.New()
+ defer s.prefix.Free()
+ s.prefix.WriteString(s.h.scopePrefix)
+ s.openScopes()
+ r.Attrs(func(a Attr) {
+ s.appendAttr(a)
+ })
+ if s.h.json {
+ // Close all open scopes.
+ for range s.h.scopes {
+ s.buf.WriteByte('}')
+ }
+ // Close the top-level object.
+ s.buf.WriteByte('}')
+ }
+}
+
// attrSep returns the separator between attributes.
func (h *commonHandler) attrSep() string {
if h.json {
diff --git a/slog/handler_test.go b/slog/handler_test.go
index 5a0ada7..7275f9e 100644
--- a/slog/handler_test.go
+++ b/slog/handler_test.go
@@ -16,11 +16,13 @@
)
func TestDefaultHandle(t *testing.T) {
+ preAttrs := []Attr{Int("pre", 0)}
+ attrs := []Attr{Int("a", 1), String("b", "two")}
for _, test := range []struct {
- name string
- withAttrs []Attr
- attrs []Attr
- want string
+ name string
+ with func(Handler) Handler
+ attrs []Attr
+ want string
}{
{
name: "no attrs",
@@ -28,31 +30,73 @@
},
{
name: "attrs",
- attrs: []Attr{Int("a", 1), String("b", "two")},
- want: "INFO a=1 b=two message",
+ attrs: attrs,
+ want: "INFO message a=1 b=two",
},
{
- name: "pre attrs",
- withAttrs: []Attr{Int("pre", 0)},
- attrs: []Attr{Int("a", 1)},
- want: "INFO pre=0 a=1 message",
+ name: "preformatted",
+ with: func(h Handler) Handler { return h.With(preAttrs) },
+ attrs: attrs,
+ want: "INFO message pre=0 a=1 b=two",
+ },
+ {
+ name: "groups",
+ attrs: []Attr{
+ Int("a", 1),
+ Group("g",
+ Int("b", 2),
+ Group("h", Int("c", 3)),
+ Int("d", 4)),
+ Int("e", 5),
+ },
+ want: "INFO message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
+ },
+ {
+ name: "scope",
+ with: func(h Handler) Handler { return h.With(preAttrs).WithScope("s") },
+ attrs: attrs,
+ want: "INFO message pre=0 s.a=1 s.b=two",
+ },
+ {
+ name: "preformatted scopes",
+ with: func(h Handler) Handler {
+ return h.With([]Attr{Int("p1", 1)}).
+ WithScope("s1").
+ With([]Attr{Int("p2", 2)}).
+ WithScope("s2")
+ },
+ attrs: attrs,
+ want: "INFO message p1=1 s1.p2=2 s1.s2.a=1 s1.s2.b=two",
+ },
+ {
+ name: "two scopes",
+ with: func(h Handler) Handler {
+ return h.With([]Attr{Int("p1", 1)}).
+ WithScope("s1").
+ WithScope("s2")
+ },
+ attrs: attrs,
+ want: "INFO message p1=1 s1.s2.a=1 s1.s2.b=two",
},
} {
- var got string
- var d Handler = &defaultHandler{output: func(_ int, s string) error {
- got = s
- return nil
- },
- }
- d = d.With(test.withAttrs)
- r := NewRecord(time.Time{}, InfoLevel, "message", 0)
- r.AddAttrs(test.attrs...)
- if err := d.Handle(r); err != nil {
- t.Fatal(err)
- }
- if got != test.want {
- t.Errorf("\ngot %s\nwant %s", got, test.want)
- }
+ t.Run(test.name, func(t *testing.T) {
+ var got string
+ var h Handler = newDefaultHandler(func(_ int, s string) error {
+ got = s
+ return nil
+ })
+ if test.with != nil {
+ h = test.with(h)
+ }
+ r := NewRecord(time.Time{}, InfoLevel, "message", 0)
+ r.AddAttrs(test.attrs...)
+ if err := h.Handle(r); err != nil {
+ t.Fatal(err)
+ }
+ if got != test.want {
+ t.Errorf("\ngot %s\nwant %s", got, test.want)
+ }
+ })
}
}
diff --git a/slog/logger.go b/slog/logger.go
index 29eadef..a812579 100644
--- a/slog/logger.go
+++ b/slog/logger.go
@@ -14,7 +14,7 @@
func init() {
defaultLogger.Store(&Logger{
- handler: &defaultHandler{output: log.Output},
+ handler: newDefaultHandler(log.Output),
})
}
diff --git a/slog/logger_test.go b/slog/logger_test.go
index c64063f..94d9c94 100644
--- a/slog/logger_test.go
+++ b/slog/logger_test.go
@@ -60,11 +60,11 @@
log.SetFlags(log.Flags() | log.Lshortfile)
Info("msg", "a", 1)
checkLogOutput(t, logbuf.String(),
- `\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} logger_test.go:\d\d: INFO a=1 msg`)
+ `\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} logger_test.go:\d\d: INFO msg a=1`)
logbuf.Reset()
Warn("msg", "b", 2)
checkLogOutput(t, logbuf.String(),
- `\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} logger_test.go:\d\d: WARN b=2 msg`)
+ `\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} logger_test.go:\d\d: WARN msg b=2`)
// Levels below Info are not printed.
logbuf.Reset()
Debug("msg", "c", 3)