//go:build go1.21 package indenthandler import ( "context" "fmt" "io" "log/slog" "runtime" "sync" "time" ) // !+types type IndentHandler struct { opts Options // TODO: state for WithGroup and WithAttrs mu *sync.Mutex out io.Writer } type Options struct { // Level reports the minimum level to log. // Levels with lower levels are discarded. // If nil, the Handler uses [slog.LevelInfo]. Level slog.Leveler } func New(out io.Writer, opts *Options) *IndentHandler { h := &IndentHandler{out: out, mu: &sync.Mutex{}} if opts != nil { h.opts = *opts } if h.opts.Level == nil { h.opts.Level = slog.LevelInfo } return h } //!-types // !+enabled func (h *IndentHandler) Enabled(ctx context.Context, level slog.Level) bool { return level >= h.opts.Level.Level() } //!-enabled func (h *IndentHandler) WithGroup(name string) slog.Handler { // TODO: implement. return h } func (h *IndentHandler) WithAttrs(attrs []slog.Attr) slog.Handler { // TODO: implement. return h } // !+handle func (h *IndentHandler) Handle(ctx context.Context, r slog.Record) error { buf := make([]byte, 0, 1024) if !r.Time.IsZero() { buf = h.appendAttr(buf, slog.Time(slog.TimeKey, r.Time), 0) } buf = h.appendAttr(buf, slog.Any(slog.LevelKey, r.Level), 0) if r.PC != 0 { fs := runtime.CallersFrames([]uintptr{r.PC}) f, _ := fs.Next() buf = h.appendAttr(buf, slog.String(slog.SourceKey, fmt.Sprintf("%s:%d", f.File, f.Line)), 0) } buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message), 0) indentLevel := 0 // TODO: output the Attrs and groups from WithAttrs and WithGroup. r.Attrs(func(a slog.Attr) bool { buf = h.appendAttr(buf, a, indentLevel) return true }) buf = append(buf, "---\n"...) h.mu.Lock() defer h.mu.Unlock() _, err := h.out.Write(buf) return err } //!-handle // !+appendAttr func (h *IndentHandler) appendAttr(buf []byte, a slog.Attr, indentLevel int) []byte { // Resolve the Attr's value before doing anything else. a.Value = a.Value.Resolve() // Ignore empty Attrs. if a.Equal(slog.Attr{}) { return buf } // Indent 4 spaces per level. buf = fmt.Appendf(buf, "%*s", indentLevel*4, "") switch a.Value.Kind() { case slog.KindString: // Quote string values, to make them easy to parse. buf = fmt.Appendf(buf, "%s: %q\n", a.Key, a.Value.String()) case slog.KindTime: // Write times in a standard way, without the monotonic time. buf = fmt.Appendf(buf, "%s: %s\n", a.Key, a.Value.Time().Format(time.RFC3339Nano)) case slog.KindGroup: attrs := a.Value.Group() // Ignore empty groups. if len(attrs) == 0 { return buf } // If the key is non-empty, write it out and indent the rest of the attrs. // Otherwise, inline the attrs. if a.Key != "" { buf = fmt.Appendf(buf, "%s:\n", a.Key) indentLevel++ } for _, ga := range attrs { buf = h.appendAttr(buf, ga, indentLevel) } default: buf = fmt.Appendf(buf, "%s: %s\n", a.Key, a.Value) } return buf } //!-appendAttr