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)} }