| //go:build go1.21 | |
| package indenthandler | |
| import ( | |
| "context" | |
| "fmt" | |
| "io" | |
| "log/slog" | |
| "runtime" | |
| "slices" | |
| "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 { | |
| 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) | |
| // 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 = 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 | |
| } |