136 lines
3.0 KiB
Go
136 lines
3.0 KiB
Go
|
package loggers
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"io"
|
||
|
"log/slog"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"go.elara.ws/loggers/internal/buffer"
|
||
|
"go.elara.ws/loggers/internal/colors"
|
||
|
)
|
||
|
|
||
|
// Pretty is an slog handler with a human-readable output
|
||
|
type Pretty struct {
|
||
|
mtx *sync.Mutex
|
||
|
goa []groupOrAttr
|
||
|
|
||
|
// Colorize indicates whether colors will be used
|
||
|
// in log output.
|
||
|
Colorize bool
|
||
|
|
||
|
// Out is where log output will be written.
|
||
|
Out io.Writer
|
||
|
|
||
|
Options
|
||
|
}
|
||
|
|
||
|
// NewPretty creates and returns a new [Pretty] handler.
|
||
|
// If opts doesn't specify a time format, [time.Kitchen] will be used.
|
||
|
func NewPretty(wr io.Writer, opts Options) *Pretty {
|
||
|
if opts.TimeFormat == "" {
|
||
|
opts.TimeFormat = time.Kitchen
|
||
|
}
|
||
|
|
||
|
return &Pretty{
|
||
|
mtx: &sync.Mutex{},
|
||
|
Colorize: opts.ForceColors || isTerm(wr),
|
||
|
Out: wr,
|
||
|
Options: opts,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Enabled reports whether the handler handles records at the given level.
|
||
|
// The handler ignores records whose level is lower.
|
||
|
func (p *Pretty) Enabled(_ context.Context, level slog.Level) bool {
|
||
|
return level >= p.Level
|
||
|
}
|
||
|
|
||
|
// WithGroup returns a new [Pretty] handler with the given group name.
|
||
|
func (p *Pretty) WithGroup(name string) slog.Handler {
|
||
|
return &Pretty{
|
||
|
mtx: p.mtx,
|
||
|
Out: p.Out,
|
||
|
Options: p.Options,
|
||
|
Colorize: p.Colorize,
|
||
|
goa: append(p.goa, groupOrAttr{group: name}),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// WithAttrs returns a new [Pretty] handler whose attributes
|
||
|
// consists of p's attributes followed by attrs.
|
||
|
func (p *Pretty) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||
|
goa := make([]groupOrAttr, len(attrs))
|
||
|
for i, attr := range attrs {
|
||
|
goa[i] = groupOrAttr{attr: attr}
|
||
|
}
|
||
|
return &Pretty{
|
||
|
mtx: p.mtx,
|
||
|
Out: p.Out,
|
||
|
Options: p.Options,
|
||
|
Colorize: p.Colorize,
|
||
|
goa: append(p.goa, goa...),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Handle formats the given [log/slog.Record] as a human-readable string on a single line.
|
||
|
func (p *Pretty) Handle(_ context.Context, rec slog.Record) error {
|
||
|
buf := buffer.Alloc()
|
||
|
defer buffer.Free(buf)
|
||
|
|
||
|
colors.WriteString(p.Colorize, buf, colors.Grey, rec.Time.Format(p.TimeFormat))
|
||
|
buf.WriteByte(' ')
|
||
|
|
||
|
switch rec.Level {
|
||
|
case slog.LevelInfo:
|
||
|
colors.WriteString(p.Colorize, buf, colors.Green, "INF")
|
||
|
case slog.LevelError:
|
||
|
colors.WriteString(p.Colorize, buf, colors.Red, "ERR")
|
||
|
case slog.LevelWarn:
|
||
|
colors.WriteString(p.Colorize, buf, colors.Yellow, "WRN")
|
||
|
case slog.LevelDebug:
|
||
|
colors.WriteString(p.Colorize, buf, colors.Magenta, "DBG")
|
||
|
}
|
||
|
buf.WriteByte(' ')
|
||
|
|
||
|
buf.WriteString(rec.Message)
|
||
|
|
||
|
lastGroup := ""
|
||
|
for _, goa := range p.goa {
|
||
|
switch {
|
||
|
case goa.group == "":
|
||
|
if lastGroup != "" {
|
||
|
goa.attr.Key = lastGroup + goa.attr.Key
|
||
|
}
|
||
|
buf.WriteByte(' ')
|
||
|
writeAttr(p.Colorize, buf, goa.attr)
|
||
|
default:
|
||
|
lastGroup += goa.group + "."
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if rec.NumAttrs() > 0 {
|
||
|
rec.Attrs(func(attr slog.Attr) bool {
|
||
|
if lastGroup != "" {
|
||
|
attr.Key = lastGroup + attr.Key
|
||
|
}
|
||
|
buf.WriteByte(' ')
|
||
|
writeAttr(p.Colorize, buf, attr)
|
||
|
return true
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if p.ShowCaller {
|
||
|
writeCaller(p.Colorize, buf, rec.PC)
|
||
|
}
|
||
|
|
||
|
buf.WriteByte('\n')
|
||
|
|
||
|
p.mtx.Lock()
|
||
|
defer p.mtx.Unlock()
|
||
|
|
||
|
_, err := buf.WriteTo(p.Out)
|
||
|
return err
|
||
|
}
|