blob: bf96c5dc7f60076236d551bcc7c4c17c6b0dc923 [file] [log] [blame]
//go:build go1.21
package indenthandler
import (
"context"
"fmt"
"io"
"log/slog"
"runtime"
"slices"
"strconv"
"sync"
"time"
)
// !+IndentHandler
type IndentHandler struct {
opts Options
preformatted []byte // data from WithGroup and WithAttrs
unopenedGroups []string // groups from WithGroup that haven't been opened
indentLevel int // same as number of opened groups so far
mu *sync.Mutex
out io.Writer
}
//!-IndentHandler
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
}
func (h *IndentHandler) Enabled(ctx context.Context, level slog.Level) bool {
return level >= h.opts.Level.Level()
}
// !+WithGroup
func (h *IndentHandler) WithGroup(name string) slog.Handler {
if name == "" {
return h
}
h2 := *h
// Add an unopened group to h2 without modifying h.
h2.unopenedGroups = make([]string, len(h.unopenedGroups)+1)
copy(h2.unopenedGroups, h.unopenedGroups)
h2.unopenedGroups[len(h2.unopenedGroups)-1] = name
return &h2
}
//!-WithGroup
// !+WithAttrs
func (h *IndentHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return h
}
h2 := *h
// Force an append to copy the underlying array.
pre := slices.Clip(h.preformatted)
// Add all groups from WithGroup that haven't already been added.
h2.preformatted = h2.appendUnopenedGroups(pre, h2.indentLevel)
// Each of those groups increased the indent level by 1.
h2.indentLevel += len(h2.unopenedGroups)
// Now all groups have been opened.
h2.unopenedGroups = nil
// Pre-format the attributes.
for _, a := range attrs {
h2.preformatted = h2.appendAttr(h2.preformatted, a, h2.indentLevel)
}
return &h2
}
func (h *IndentHandler) appendUnopenedGroups(buf []byte, indentLevel int) []byte {
for _, g := range h.unopenedGroups {
buf = fmt.Appendf(buf, "%*s%s:\n", indentLevel*4, "", g)
indentLevel++
}
return buf
}
//!-WithAttrs
// !+Handle
func (h *IndentHandler) Handle(ctx context.Context, r slog.Record) error {
bufp := allocBuf()
buf := *bufp
defer func() {
*bufp = buf
freeBuf(bufp)
}()
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()
// Optimize to minimize allocation.
srcbufp := allocBuf()
defer freeBuf(srcbufp)
*srcbufp = append(*srcbufp, f.File...)
*srcbufp = append(*srcbufp, ':')
*srcbufp = strconv.AppendInt(*srcbufp, int64(f.Line), 10)
buf = h.appendAttr(buf, slog.String(slog.SourceKey, string(*srcbufp)), 0)
}
buf = h.appendAttr(buf, slog.String(slog.MessageKey, r.Message), 0)
// Insert preformatted attributes just after built-in ones.
buf = append(buf, h.preformatted...)
if r.NumAttrs() > 0 {
buf = h.appendUnopenedGroups(buf, h.indentLevel)
r.Attrs(func(a slog.Attr) bool {
buf = h.appendAttr(buf, a, h.indentLevel+len(h.unopenedGroups))
return true
})
}
buf = append(buf, "---\n"...)
h.mu.Lock()
defer h.mu.Unlock()
_, err := h.out.Write(buf)
return err
}
//!-Handle
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 = append(buf, a.Key...)
buf = append(buf, ": "...)
buf = strconv.AppendQuote(buf, a.Value.String())
buf = append(buf, '\n')
case slog.KindTime:
// Write times in a standard way, without the monotonic time.
buf = append(buf, a.Key...)
buf = append(buf, ": "...)
buf = a.Value.Time().AppendFormat(buf, time.RFC3339Nano)
buf = append(buf, '\n')
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 = append(buf, a.Key...)
buf = append(buf, ": "...)
buf = append(buf, a.Value.String()...)
buf = append(buf, '\n')
}
return buf
}
// !+pool
var bufPool = sync.Pool{
New: func() any {
b := make([]byte, 0, 1024)
return &b
},
}
func allocBuf() *[]byte {
return bufPool.Get().(*[]byte)
}
func freeBuf(b *[]byte) {
// To reduce peak allocation, return only smaller buffers to the pool.
const maxBufferSize = 16 << 10
if cap(*b) <= maxBufferSize {
*b = (*b)[:0]
bufPool.Put(b)
}
}
//!-pool