| //go:build go1.21 |
| |
| package indenthandler |
| |
| import ( |
| "context" |
| "fmt" |
| "io" |
| "log/slog" |
| "runtime" |
| "sync" |
| "time" |
| ) |
| |
| // !+IndentHandler |
| type IndentHandler struct { |
| opts Options |
| goas []groupOrAttrs |
| 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 |
| } |
| |
| // !+gora |
| // groupOrAttrs holds either a group name or a list of slog.Attrs. |
| type groupOrAttrs struct { |
| group string // group name if non-empty |
| attrs []slog.Attr // attrs if non-empty |
| } |
| |
| //!-gora |
| |
| 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() |
| } |
| |
| // !+withs |
| func (h *IndentHandler) WithGroup(name string) slog.Handler { |
| if name == "" { |
| return h |
| } |
| return h.withGroupOrAttrs(groupOrAttrs{group: name}) |
| } |
| |
| func (h *IndentHandler) WithAttrs(attrs []slog.Attr) slog.Handler { |
| if len(attrs) == 0 { |
| return h |
| } |
| return h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) |
| } |
| |
| //!-withs |
| |
| // !+withgora |
| func (h *IndentHandler) withGroupOrAttrs(goa groupOrAttrs) *IndentHandler { |
| h2 := *h |
| h2.goas = make([]groupOrAttrs, len(h.goas)+1) |
| copy(h2.goas, h.goas) |
| h2.goas[len(h2.goas)-1] = goa |
| return &h2 |
| } |
| |
| //!-withgora |
| |
| // !+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 |
| // Handle state from WithGroup and WithAttrs. |
| goas := h.goas |
| if r.NumAttrs() == 0 { |
| // If the record has no Attrs, remove groups at the end of the list; they are empty. |
| for len(goas) > 0 && goas[len(goas)-1].group != "" { |
| goas = goas[:len(goas)-1] |
| } |
| } |
| for _, goa := range goas { |
| if goa.group != "" { |
| buf = fmt.Appendf(buf, "%*s%s:\n", indentLevel*4, "", goa.group) |
| indentLevel++ |
| } else { |
| for _, a := range goa.attrs { |
| buf = h.appendAttr(buf, a, indentLevel) |
| } |
| } |
| } |
| 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 |
| |
| 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 |
| } |