package logger import ( "context" "fmt" "io" "log/slog" "os" "strconv" "strings" "sync" "time" ) // isTerminal reports whether f is connected to a character device (terminal). func isTerminal(f *os.File) bool { info, err := f.Stat() return err == nil && info.Mode()&os.ModeCharDevice != 0 } // humanHandler writes human-readable log lines to w with no timestamp. // // Format: // // INFO: message key=val … (no prefix — the program's normal voice) // WARN: warning: message key=val … // ERROR: error: message key=val … // DEBUG: debug: message key=val … type humanHandler struct { w io.Writer level slog.Level mu sync.Mutex attrs []slog.Attr prefix string // dot-separated group prefix for attribute keys } func newHumanHandler(w io.Writer, level slog.Level) *humanHandler { return &humanHandler{w: w, level: level} } func (h *humanHandler) Enabled(_ context.Context, level slog.Level) bool { return level >= h.level } func (h *humanHandler) Handle(_ context.Context, r slog.Record) error { var b strings.Builder b.WriteString(humanLevel(r.Level)) b.WriteString(r.Message) for _, a := range h.attrs { appendAttr(&b, h.prefix, a) } r.Attrs(func(a slog.Attr) bool { appendAttr(&b, h.prefix, 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 { merged := make([]slog.Attr, len(h.attrs)+len(attrs)) copy(merged, h.attrs) copy(merged[len(h.attrs):], attrs) return &humanHandler{w: h.w, level: h.level, attrs: merged, prefix: h.prefix} } func (h *humanHandler) WithGroup(name string) slog.Handler { if name == "" { return h } prefix := h.prefix if prefix != "" { prefix += "." } return &humanHandler{w: h.w, level: h.level, attrs: h.attrs, prefix: prefix + name} } // multiHandler fans a single log record out to multiple handlers. // Each sub-handler's own Enabled filter is respected independently, // so different handlers can capture different level ranges. type multiHandler []slog.Handler func (m multiHandler) Enabled(ctx context.Context, level slog.Level) bool { for _, h := range m { if h.Enabled(ctx, level) { return true } } return false } func (m multiHandler) Handle(ctx context.Context, r slog.Record) error { var firstErr error for _, h := range m { if h.Enabled(ctx, r.Level) { if err := h.Handle(ctx, r); err != nil && firstErr == nil { firstErr = err } } } return firstErr } func (m multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler { handlers := make(multiHandler, len(m)) for i, h := range m { handlers[i] = h.WithAttrs(attrs) } return handlers } func (m multiHandler) WithGroup(name string) slog.Handler { handlers := make(multiHandler, len(m)) for i, h := range m { handlers[i] = h.WithGroup(name) } return handlers } // ── formatting helpers ──────────────────────────────────────────────────────── func humanLevel(level slog.Level) string { switch { case level >= slog.LevelError: return "error: " case level >= slog.LevelWarn: return "warning: " case level >= slog.LevelInfo: return "" // INFO needs no label — it is the program's normal voice default: return "debug: " } } func appendAttr(b *strings.Builder, prefix string, a slog.Attr) { a.Value = a.Value.Resolve() if a.Value.Kind() == slog.KindGroup { // inline unnamed groups; named groups become a key prefix sub := prefix if a.Key != "" { if sub != "" { sub += "." } sub += a.Key } for _, ga := range a.Value.Group() { appendAttr(b, sub, ga) } return } if a.Key == "" { return } key := a.Key if prefix != "" { key = prefix + "." + key } b.WriteByte(' ') b.WriteString(key) b.WriteByte('=') appendValue(b, a.Value) } func appendValue(b *strings.Builder, v slog.Value) { switch v.Kind() { case slog.KindString: s := v.String() if strings.ContainsAny(s, " \t\n\"=") { b.WriteString(strconv.Quote(s)) } else { b.WriteString(s) } case slog.KindTime: b.WriteString(v.Time().Format(time.RFC3339)) default: fmt.Fprint(b, v.Any()) } }