Files
simplelog/console_handler.go
T
clawbot 9710d3f05f
check / check (push) Successful in 30s
check / check (pull_request) Successful in 22s
Take the console location from the record (closes #36)
ConsoleHandler found the file and line it prints by walking a fixed
number of stack frames up from itself, which only fit a record logged
through a slog.Logger and delivered through MultiplexHandler. It now
reads them from the record's PC, the call site slog stores, so a
ConsoleHandler used with slog.New and a record passed to Handle
directly print the right location too. A record whose PC is zero
prints ???:0 as before. The README drops its warning about the wrong
location and says how to set PC instead.

Model: opus-5-5
2026-10-06 12:02:43 +00:00

113 lines
2.6 KiB
Go

package simplelog
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"runtime"
"time"
"github.com/fatih/color"
)
// ConsoleHandler writes human-readable, colored log lines to stdout.
type ConsoleHandler struct {
// out is where records are written. Nil means os.Stdout, looked up on
// every write so that a reassigned os.Stdout is followed.
out io.Writer
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. A failed write is returned as an error.
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()
}
// The file and line come from the record's PC, the call site; a record
// without one prints a placeholder.
file, line := "???", 0
if record.PC != 0 {
frame, _ := runtime.CallersFrames([]uintptr{record.PC}).Next()
file, line = frame.File, frame.Line
}
out := c.out
if out == nil {
out = os.Stdout
}
_, err := fmt.Fprintln(
out,
colorFunc(
"%s [%s] %s:%d: %s%s",
timestamp,
record.Level,
file,
line,
record.Message,
attrsToText(c.attrs.forRecord(record)),
),
)
if err != nil {
return fmt.Errorf("error writing log record: %w", err)
}
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{out: c.out, 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{out: c.out, attrs: c.attrs.withGroup(name)}
}