check / check (push) Successful in 35s
The console and JSON handlers threw away the error from their write to stdout, so a lost log line looked delivered. They now return it, wrapped. The webhook handler also returns an error for a status outside 2xx, and no longer follows redirects, which could resend the request without the record. The multiplex handler passes the record to every handler and returns their errors joined with errors.Join instead of stopping at the first. Both stdout handlers gain an unexported writer, nil meaning os.Stdout at write time, so tests can supply a sink that fails. The README says what each handler returns and that the slog.Logger methods discard a handler's error. Model: opus-5-5
115 lines
2.7 KiB
Go
115 lines
2.7 KiB
Go
package simplelog
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"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 {
|
|
// 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()
|
|
}
|
|
|
|
// Get the caller information
|
|
_, file, line, ok := runtime.Caller(callerSkipFrames)
|
|
if !ok {
|
|
file = "???"
|
|
line = 0
|
|
}
|
|
|
|
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)}
|
|
}
|