check / check (push) Waiting to run
Takes .golangci.yml, .dockerignore, .gitignore, .editorconfig, the workflow and REPO_POLICIES.md from sneak/prompts dd4027b, keeping this repository's own entries after the shared content. Lint (golangci-lint v2.14.0) and test become Dockerfile phases that script/lint and script/test build alone and uncached; the image build depends on both, and script/cibuild bootstraps, runs script/check, then builds the image. The agent guidance file moves to AGENTS.md. The Go code is fixed for what the new configuration finds; reads that iterate rows now also report an iteration error. Model: opus-5-5
177 lines
5.2 KiB
Go
177 lines
5.2 KiB
Go
// Package logger provides a structured logger with source location tracking.
|
|
// It wraps the standard library's log/slog package and automatically enriches
|
|
// log messages with the file name, line number, and function name of the caller.
|
|
// The output format is automatically selected based on the runtime environment:
|
|
// human-readable text for terminals, JSON for non-terminal output.
|
|
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// Logger wraps slog.Logger to add automatic source location information
|
|
// to all log messages. It embeds slog.Logger and provides the same logging
|
|
// methods (Debug, Info, Warn, Error) but enriches each message with the
|
|
// file name, line number, and function name of the caller.
|
|
type Logger struct {
|
|
*slog.Logger
|
|
}
|
|
|
|
// AsSlog returns the underlying slog.Logger for use with APIs that require
|
|
// a standard slog.Logger instance rather than the custom Logger type.
|
|
func (l *Logger) AsSlog() *slog.Logger {
|
|
return l.Logger
|
|
}
|
|
|
|
// New creates a new Logger with an appropriate handler based on the runtime
|
|
// environment. If stdout is a terminal, it uses a human-readable text format;
|
|
// otherwise, it outputs JSON for structured log aggregation. The log level
|
|
// defaults to Info, but can be set to Debug by including "routewatch" in the
|
|
// DEBUG environment variable.
|
|
func New() *Logger {
|
|
level := slog.LevelInfo
|
|
if debug := os.Getenv("DEBUG"); strings.Contains(debug, "routewatch") {
|
|
level = slog.LevelDebug
|
|
}
|
|
|
|
opts := &slog.HandlerOptions{
|
|
Level: level,
|
|
}
|
|
|
|
var handler slog.Handler
|
|
if term.IsTerminal(int(os.Stdout.Fd())) {
|
|
// Terminal, use text
|
|
handler = slog.NewTextHandler(os.Stdout, opts)
|
|
} else {
|
|
// Not a terminal, use JSON
|
|
handler = slog.NewJSONHandler(os.Stdout, opts)
|
|
}
|
|
|
|
return &Logger{Logger: slog.New(handler)}
|
|
}
|
|
|
|
// sourceSkipLevel defines the number of call stack frames to skip when
|
|
// determining the caller's source location. This accounts for the logger
|
|
// method itself and the getSourceAttrs helper function.
|
|
const sourceSkipLevel = 2
|
|
|
|
// getSourceAttrs returns attributes for the calling source location
|
|
func getSourceAttrs() []slog.Attr {
|
|
pc, file, line, ok := runtime.Caller(sourceSkipLevel)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
// Get just the filename without the full path
|
|
file = filepath.Base(file)
|
|
|
|
// Get the function name
|
|
fn := runtime.FuncForPC(pc)
|
|
|
|
var funcName string
|
|
if fn != nil {
|
|
funcName = filepath.Base(fn.Name())
|
|
}
|
|
|
|
attrs := []slog.Attr{
|
|
slog.String("source", fmt.Sprintf("%s:%d", file, line)),
|
|
}
|
|
|
|
if funcName != "" {
|
|
attrs = append(attrs, slog.String("func", funcName))
|
|
}
|
|
|
|
return attrs
|
|
}
|
|
|
|
// Debug logs a message at debug level with automatic source location tracking.
|
|
// Additional structured attributes can be passed as key-value pairs in args.
|
|
// Debug messages are only output when the DEBUG environment variable contains
|
|
// "routewatch".
|
|
func (l *Logger) Debug(msg string, args ...any) {
|
|
sourceAttrs := getSourceAttrs()
|
|
allArgs := make([]any, 0, len(args)+len(sourceAttrs)*2)
|
|
|
|
// Add source attributes first
|
|
for _, attr := range sourceAttrs {
|
|
allArgs = append(allArgs, attr)
|
|
}
|
|
|
|
// Add user args
|
|
allArgs = append(allArgs, args...)
|
|
|
|
l.Logger.Debug(msg, allArgs...)
|
|
}
|
|
|
|
// Info logs a message at info level with automatic source location tracking.
|
|
// Additional structured attributes can be passed as key-value pairs in args.
|
|
func (l *Logger) Info(msg string, args ...any) {
|
|
sourceAttrs := getSourceAttrs()
|
|
allArgs := make([]any, 0, len(args)+len(sourceAttrs)*2)
|
|
|
|
// Add source attributes first
|
|
for _, attr := range sourceAttrs {
|
|
allArgs = append(allArgs, attr)
|
|
}
|
|
|
|
// Add user args
|
|
allArgs = append(allArgs, args...)
|
|
|
|
l.Logger.Info(msg, allArgs...)
|
|
}
|
|
|
|
// Warn logs a message at warn level with automatic source location tracking.
|
|
// Additional structured attributes can be passed as key-value pairs in args.
|
|
func (l *Logger) Warn(msg string, args ...any) {
|
|
sourceAttrs := getSourceAttrs()
|
|
allArgs := make([]any, 0, len(args)+len(sourceAttrs)*2)
|
|
|
|
// Add source attributes first
|
|
for _, attr := range sourceAttrs {
|
|
allArgs = append(allArgs, attr)
|
|
}
|
|
|
|
// Add user args
|
|
allArgs = append(allArgs, args...)
|
|
|
|
l.Logger.Warn(msg, allArgs...)
|
|
}
|
|
|
|
// Error logs a message at error level with automatic source location tracking.
|
|
// Additional structured attributes can be passed as key-value pairs in args.
|
|
func (l *Logger) Error(msg string, args ...any) {
|
|
sourceAttrs := getSourceAttrs()
|
|
allArgs := make([]any, 0, len(args)+len(sourceAttrs)*2)
|
|
|
|
// Add source attributes first
|
|
for _, attr := range sourceAttrs {
|
|
allArgs = append(allArgs, attr)
|
|
}
|
|
|
|
// Add user args
|
|
allArgs = append(allArgs, args...)
|
|
|
|
l.Logger.Error(msg, allArgs...)
|
|
}
|
|
|
|
// With returns a new Logger with additional structured attributes that will
|
|
// be included in all subsequent log messages. The args parameter accepts
|
|
// key-value pairs in the same format as the logging methods.
|
|
func (l *Logger) With(args ...any) *Logger {
|
|
return &Logger{Logger: l.Logger.With(args...)}
|
|
}
|
|
|
|
// WithGroup returns a new Logger that adds the specified group name as a
|
|
// prefix to all attribute keys in subsequent log messages. This is useful
|
|
// for organizing related attributes under a common namespace.
|
|
func (l *Logger) WithGroup(name string) *Logger {
|
|
return &Logger{Logger: l.Logger.WithGroup(name)}
|
|
}
|