Files
simplelog/console_handler.go
T
sneak 86436449c5
check / check (push) Successful in 34s
check / check (pull_request) Successful in 32s
Emit slog attributes from every handler (closes #19, closes #24)
Handle never read the record's attributes, and WithAttrs and WithGroup
returned the receiver unchanged. The JSON and webhook handlers also marshaled
slog.Record itself, whose attributes are unexported.

Each handler now carries a handlerAttrs value, copied rather than mutated, so
sibling loggers cannot leak attributes into each other. JSON and webhook
output nest groups as objects; the console appends key=value pairs with dotted
group keys, quoted as slog.NewTextHandler quotes them, invalid UTF-8
included. Logged values are never written to. In JSON the record's own fields
win a key collision, and a repeated key keeps its last value unless both are
groups, which merge; the README documents both.

Deviation: DEL is quoted on the console; the stdlib leaves it bare.

Model: opus-5-5
2026-09-28 10:49:46 +00:00

103 lines
2.3 KiB
Go

package simplelog
import (
"context"
"fmt"
"log/slog"
"os"
"runtime"
"time"
"github.com/fatih/color"
)
// callerSkipFrames is the number of stack frames between runtime.Caller
// and the slog call site that produced the record.
const callerSkipFrames = 4
// ConsoleHandler writes human-readable, colored log lines to stdout.
type ConsoleHandler struct {
attrs handlerAttrs
}
// NewConsoleHandler returns a new ConsoleHandler.
func NewConsoleHandler() *ConsoleHandler {
return &ConsoleHandler{}
}
// Handle writes the record to stdout as a colored, timestamped line
// including the caller file and line, followed by the attributes as
// key=value pairs.
func (c *ConsoleHandler) Handle(
_ context.Context,
record slog.Record,
) error {
timestamp := time.Now().UTC().Format("2006-01-02T15:04:05.000Z07:00")
var colorFunc func(format string, a ...any) string
switch record.Level {
case slog.LevelDebug:
colorFunc = color.New(color.FgWhite).SprintfFunc()
case slog.LevelInfo:
colorFunc = color.New(color.FgBlue).SprintfFunc()
case slog.LevelWarn:
colorFunc = color.New(color.FgYellow).SprintfFunc()
case slog.LevelError:
colorFunc = color.New(color.FgRed).SprintfFunc()
default:
colorFunc = color.New(color.FgWhite).SprintfFunc()
}
// Get the caller information
_, file, line, ok := runtime.Caller(callerSkipFrames)
if !ok {
file = "???"
line = 0
}
_, _ = fmt.Fprintln(
os.Stdout,
colorFunc(
"%s [%s] %s:%d: %s%s",
timestamp,
record.Level,
file,
line,
record.Message,
attrsToText(c.attrs.forRecord(record)),
),
)
return nil
}
// Enabled reports whether the handler processes records at the given
// level; it always returns true.
func (c *ConsoleHandler) Enabled(
_ context.Context,
_ slog.Level,
) bool {
return true
}
// WithAttrs returns a new handler that also emits attrs, qualified by the
// groups open now. The receiver is not modified.
func (c *ConsoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(attrs) == 0 {
return c
}
return &ConsoleHandler{attrs: c.attrs.withAttrs(attrs)}
}
// WithGroup returns a new handler that qualifies later attributes with
// the group name. An empty name returns the receiver.
func (c *ConsoleHandler) WithGroup(name string) slog.Handler {
if name == "" {
return c
}
return &ConsoleHandler{attrs: c.attrs.withGroup(name)}
}