139 lines
2.8 KiB
Go
139 lines
2.8 KiB
Go
package logging
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"slices"
|
|
"sync"
|
|
)
|
|
|
|
// ANSI color codes
|
|
const (
|
|
dim = "\033[2m"
|
|
red = "\033[31m"
|
|
yellow = "\033[33m"
|
|
cyan = "\033[36m"
|
|
reset = "\033[0m"
|
|
)
|
|
|
|
// ConsoleHandler formats log output for human readability on terminals.
|
|
// It produces compact, color-coded lines:
|
|
//
|
|
// 20:15:54 INF daemon started addr=:5055
|
|
// 20:15:54 ERR backup failed component=backup error="connection refused"
|
|
type ConsoleHandler struct {
|
|
w io.Writer
|
|
level slog.Leveler
|
|
attrs []slog.Attr
|
|
mu *sync.Mutex
|
|
}
|
|
|
|
// NewConsoleHandler creates a handler that writes human-friendly colored logs.
|
|
func NewConsoleHandler(w io.Writer, opts *slog.HandlerOptions) *ConsoleHandler {
|
|
level := slog.LevelInfo
|
|
if opts != nil && opts.Level != nil {
|
|
level = opts.Level.Level()
|
|
}
|
|
return &ConsoleHandler{
|
|
w: w,
|
|
level: level,
|
|
mu: &sync.Mutex{},
|
|
}
|
|
}
|
|
|
|
func (h *ConsoleHandler) Enabled(_ context.Context, level slog.Level) bool {
|
|
return level >= h.level.Level()
|
|
}
|
|
|
|
func (h *ConsoleHandler) Handle(_ context.Context, r slog.Record) error {
|
|
// Time
|
|
buf := []byte(dim + r.Time.Format("15:04:05") + reset + " ")
|
|
|
|
// Level badge
|
|
switch {
|
|
case r.Level >= slog.LevelError:
|
|
buf = append(buf, red+"ERR"+reset+" "...)
|
|
case r.Level >= slog.LevelWarn:
|
|
buf = append(buf, yellow+"WRN"+reset+" "...)
|
|
default:
|
|
buf = append(buf, cyan+"INF"+reset+" "...)
|
|
}
|
|
|
|
// Message
|
|
buf = append(buf, r.Message...)
|
|
|
|
// Pre-set attrs (from slog.With)
|
|
for _, a := range h.attrs {
|
|
buf = appendAttr(buf, a)
|
|
}
|
|
|
|
// Inline attrs
|
|
r.Attrs(func(a slog.Attr) bool {
|
|
buf = appendAttr(buf, a)
|
|
return true
|
|
})
|
|
|
|
buf = append(buf, '\n')
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
_, err := h.w.Write(buf)
|
|
return err
|
|
}
|
|
|
|
func (h *ConsoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
return &ConsoleHandler{
|
|
w: h.w,
|
|
level: h.level,
|
|
attrs: append(slices.Clone(h.attrs), attrs...),
|
|
mu: h.mu,
|
|
}
|
|
}
|
|
|
|
func (h *ConsoleHandler) WithGroup(name string) slog.Handler {
|
|
// Groups are rare in this codebase; treat as a prefixed attr set
|
|
return &ConsoleHandler{
|
|
w: h.w,
|
|
level: h.level,
|
|
attrs: append(slices.Clone(h.attrs), slog.String("group", name)),
|
|
mu: h.mu,
|
|
}
|
|
}
|
|
|
|
func appendAttr(buf []byte, a slog.Attr) []byte {
|
|
if a.Equal(slog.Attr{}) {
|
|
return buf
|
|
}
|
|
v := a.Value.Resolve()
|
|
buf = append(buf, ' ')
|
|
buf = append(buf, dim...)
|
|
buf = append(buf, a.Key...)
|
|
buf = append(buf, '=')
|
|
buf = append(buf, reset...)
|
|
|
|
s := v.String()
|
|
if needsQuote(s) {
|
|
buf = append(buf, fmt.Sprintf("%q", s)...)
|
|
} else {
|
|
buf = append(buf, s...)
|
|
}
|
|
return buf
|
|
}
|
|
|
|
func needsQuote(s string) bool {
|
|
if s == "" {
|
|
return true
|
|
}
|
|
for _, c := range s {
|
|
if c <= ' ' || c == '"' || c == '\\' {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Verify interface compliance at compile time.
|
|
var _ slog.Handler = (*ConsoleHandler)(nil)
|