[slog] customize defaultHandler

1,390 views
Skip to first unread message

vl...@mailbox.org

unread,
Aug 28, 2023, 9:06:37 AM8/28/23
to golan...@googlegroups.com
Hi,

When reading trough the log/slog documentation, it seems one can create
a logger with a different handler, which is either NewTextHandler or
NewJSONHandler.

Why can't I configure the defaultHandler? Let's say I want my logger to
behave exactly like the defaultHandler, but output to a logfile or
Stdout instead.

The defaultHandler's output is different compared to the NewTextHandler:

slog.Info("ok"), gives me:

INFO ok

The NextTextHandler gives me:

level=INFO msg="ok"


Regards,

Tamás Gulácsi

unread,
Aug 28, 2023, 12:50:50 PM8/28/23
to golang-nuts
slog.SetDefault(slog.New(myHandler{Handler:slog.Default().Handler}))

Mike Schinkel

unread,
Aug 28, 2023, 9:00:01 PM8/28/23
to golang-nuts
Hi Tamás,

Have you actually tried that and gotten it to work? It does not compile
for me but this does (note method call vs. property reference):

slog.SetDefault(slog.New(myHandler{Handler:slog.Default().Handler()}))

However, when delegating the Handle() method it seems to cause an infinite
loop:

func (m MyHandler) Handle(ctx context.Context, r slog.Record) error {
    return m.Handler.Handle(ctx, r)
}

See https://goplay.tools/snippet/qw07m0YflLd

I know about this because just this past weekend I was trying to write a
TeeHandler to output the default to the screen and JSON to a file just this
past weekend and ran into an infinite loop problem with the default handler.

I tried my best to figure out why it needed to be structured the way it was
in that it seems to call itself recursively. I wanted to post a question to
this list to see if there was a workaround, or if not to see if there might
be interest in allowing it to work, but I could not get my head around it so
eventually gave up and just used the TextHandler instead.

Shame though. It would be nice to be able to reuse the default handler but
AFACT it is not possible (though if I am wrong I would love for someone to
show me how to get it to work.)

-Mike

Marcello H

unread,
Aug 29, 2023, 2:53:34 AM8/29/23
to golang-nuts
Yesterday, I came up with the same question and found this:
"github.com/lmittmann/tint"

(This solution still uses os.Stdout, but I think this can do what you need.)

An example:
logOptions := &tint.Options{
NoColor: true,
Level: slog.LevelError,
TimeFormat: time.DateTime,
}
logHandler := tint.NewHandler(os.Stdout, logOptions)
logger := slog.New(logHandler)
slog.SetDefault(logger)
slog.Info("this does not show")
slog.Debug("this debug info does not show")
logOptions.Level = slog.LevelInfo
slog.Info("this is now visible")
slog.Debug("this debug info still does not show")
logOptions.Level = slog.LevelDebug
slog.Info("this is still visible")
slog.Debug("this debug info also shows")
Op dinsdag 29 augustus 2023 om 03:00:01 UTC+2 schreef Mike Schinkel:

Sean Liao

unread,
Aug 29, 2023, 3:16:54 AM8/29/23
to golang-nuts

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/df136f1e-0283-46fa-a0b7-ce17a0722fd9n%40googlegroups.com.

Marcello H

unread,
Aug 29, 2023, 3:59:55 AM8/29/23
to Sean Liao, golang-nuts
After playing with it some more:

This also does the trick

h := NewHandler(os.Stdout, &MyOptions{Level: slog.LevelDebug, TimeFormat: time.DateTime})
l := slog.New(h)
slog.SetDefault(l)

slog.Debug("hello")
slog.Info("hello")
slog.Warn("hello")
slog.Error("hello")





type MyOptions struct {
// Enable source code location (Default: false)
AddSource bool

// Minimum level to log (Default: slog.LevelInfo)
Level slog.Leveler

// ReplaceAttr is called to rewrite each non-group attribute before it is logged.
ReplaceAttr func(groups []string, attr slog.Attr) slog.Attr

// Time format (Default: time.StampMilli)
TimeFormat string
}

type MyHandler struct {
opts MyOptions
prefix string // preformatted group names followed by a dot
preformat string // preformatted Attrs, with an initial space
timeFormat string

mu sync.Mutex
w io.Writer
}

func NewHandler(w io.Writer, opts *MyOptions) *MyHandler {
h := &MyHandler{w: w}
if opts != nil {
h.opts = *opts
}
if h.opts.ReplaceAttr == nil {
h.opts.ReplaceAttr = func(_ []string, a slog.Attr) slog.Attr { return a }
}
if opts.TimeFormat != "" {
h.timeFormat = opts.TimeFormat
}

return h
}

func (h *MyHandler) Enabled(ctx context.Context, level slog.Level) bool {
minLevel := slog.LevelInfo
if h.opts.Level != nil {
minLevel = h.opts.Level.Level()
}
return level >= minLevel
}

func (h *MyHandler) WithGroup(name string) slog.Handler {
return &MyHandler{
w: h.w,
opts: h.opts,
preformat: h.preformat,
prefix: h.prefix + name + ".",
}
}

func (h *MyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
var buf []byte
for _, a := range attrs {
buf = h.appendAttr(buf, h.prefix, a)
}
return &MyHandler{
w: h.w,
opts: h.opts,
prefix: h.prefix,
preformat: h.preformat + string(buf),
}
}

func (h *MyHandler) Handle(ctx context.Context, r slog.Record) error {
var buf []byte
if !r.Time.IsZero() {
buf = r.Time.AppendFormat(buf, h.timeFormat)
buf = append(buf, ' ')
}

levText := (r.Level.String() + " ")[0:5]

buf = append(buf, levText...)
buf = append(buf, ' ')
if h.opts.AddSource && r.PC != 0 {
fs := runtime.CallersFrames([]uintptr{r.PC})
f, _ := fs.Next()
buf = append(buf, f.File...)
buf = append(buf, ':')
buf = strconv.AppendInt(buf, int64(f.Line), 10)
buf = append(buf, ' ')
}
buf = append(buf, r.Message...)
buf = append(buf, h.preformat...)
r.Attrs(func(a slog.Attr) bool {
buf = h.appendAttr(buf, h.prefix, a)
return true
})
buf = append(buf, '\n')
h.mu.Lock()
defer h.mu.Unlock()
_, err := h.w.Write(buf)
return err
}

func (h *MyHandler) appendAttr(buf []byte, prefix string, a slog.Attr) []byte {
if a.Equal(slog.Attr{}) {
return buf
}
if a.Value.Kind() != slog.KindGroup {
buf = append(buf, ' ')
buf = append(buf, prefix...)
buf = append(buf, a.Key...)
buf = append(buf, '=')
return fmt.Appendf(buf, "%v", a.Value.Any())
}
// Group
if a.Key != "" {
prefix += a.Key + "."
}
for _, a := range a.Value.Group() {
buf = h.appendAttr(buf, prefix, a)
}
return buf
}


Op di 29 aug 2023 om 09:16 schreef 'Sean Liao' via golang-nuts <golan...@googlegroups.com>:
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/aJPXT2NF-Lc/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAGabyPo9NZJBf%3Dj-W_8tUS%3DqkmwHRWVpjCbFz04jZW_P8KEOow%40mail.gmail.com.

Andrew Harris

unread,
Aug 29, 2023, 5:40:29 AM8/29/23
to golang-nuts
Piping JSON output through jq is worth exploring. This much is enough to be nicer to human eyes:

cat log.json | jq -c 'del(.time)'

There's a nice Go implementation at https://github.com/itchyny/gojq.

Tamás Gulácsi

unread,
Aug 29, 2023, 7:31:39 AM8/29/23
to golang-nuts
Sorry for my wrong answer - I thought that should work.

https://go.dev//issues/61892
explains it: the default slog handler uses the default log.Logger.
This answers my other question, too: why is the default slog.Handler (newDefaultHandler) unexported? Because it's error prone.


works as the workaround: log.SetOutput (after slog.SetDefault) breaks the cycle (but you have to set the default flags, too).
Reply all
Reply to author
Forward
0 new messages