package logging
import (
"context"
"fmt"
"io"
"log/slog"
"strings"
"sync"
"time"
)
func Setup(format string, w io.Writer, level slog.Level) {
var handler slog.Handler
switch format {
case "json":
handler = slog.NewJSONHandler(w, &slog.HandlerOptions{Level: level})
default:
handler = newHumanHandler(w, level)
}
slog.SetDefault(slog.New(handler))
}
// humanHandler emits "<RFC3339> <LVL>: <msg> k=v k=v\n".
type humanHandler struct {
mu *sync.Mutex
w io.Writer
level slog.Level
attrs []slog.Attr
group string
}
func newHumanHandler(w io.Writer, level slog.Level) *humanHandler {
return &humanHandler{mu: &sync.Mutex{}, w: w, level: level}
}
func (h *humanHandler) Enabled(_ context.Context, l slog.Level) bool { return l >= h.level }
func (h *humanHandler) Handle(_ context.Context, r slog.Record) error {
var b strings.Builder
b.WriteString(r.Time.UTC().Format(time.RFC3339))
b.WriteByte(' ')
b.WriteString(shortLevel(r.Level))
b.WriteString(": ")
b.WriteString(r.Message)
for _, a := range h.attrs {
writeAttr(&b, h.group, a)
}
r.Attrs(func(a slog.Attr) bool {
writeAttr(&b, h.group, a)
return true
})
b.WriteByte('\n')
h.mu.Lock()
defer h.mu.Unlock()
_, err := io.WriteString(h.w, b.String())
return err
}
func (h *humanHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
out := *h
out.attrs = append(append([]slog.Attr{}, h.attrs...), attrs...)
return &out
}
func (h *humanHandler) WithGroup(name string) slog.Handler {
out := *h
if h.group == "" {
out.group = name
} else {
out.group = h.group + "." + name
}
return &out
}
func shortLevel(l slog.Level) string {
switch {
case l < slog.LevelInfo:
return "DBG"
case l < slog.LevelWarn:
return "INF"
case l < slog.LevelError:
return "WRN"
default:
return "ERR"
}
}
func writeAttr(b *strings.Builder, group string, a slog.Attr) {
if a.Equal(slog.Attr{}) {
return
}
b.WriteByte(' ')
if group != "" {
b.WriteString(group)
b.WriteByte('.')
}
b.WriteString(a.Key)
b.WriteByte('=')
v := a.Value.String()
if strings.ContainsAny(v, " \t\"") {
fmt.Fprintf(b, "%q", v)
} else {
b.WriteString(v)
}
}