routewatch/internal/logger/logger.go
sneak 67f6b78aaa Add custom logger with source location tracking and remove verbose database logs
- Create internal/logger package with Logger wrapper around slog
- Logger automatically adds source file, line number, and function name to all log entries
- Use golang.org/x/term to properly detect if stdout is a terminal
- Replace all slog.Logger usage with logger.Logger throughout the codebase
- Remove verbose logging from database GetStats() method
- Update all constructors and dependencies to use the new logger
2025-07-28 01:14:51 +02:00

151 lines
3.4 KiB
Go

// Package logger provides a structured logger with source location tracking
package logger
import (
"log/slog"
"os"
"path/filepath"
"runtime"
"strings"
"golang.org/x/term"
)
// Logger wraps slog.Logger to add source location information
type Logger struct {
*slog.Logger
}
// AsSlog returns the underlying slog.Logger
func (l *Logger) AsSlog() *slog.Logger {
return l.Logger
}
// New creates a new logger with appropriate handler based on environment
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)}
}
const sourceSkipLevel = 2 // Skip levels for source location tracking
// 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", file),
slog.Int("line", line),
}
if funcName != "" {
attrs = append(attrs, slog.String("func", funcName))
}
return attrs
}
// Debug logs at debug level with source location
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 at info level with source location
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 at warn level with source location
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 at error level with source location
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 attributes
func (l *Logger) With(args ...any) *Logger {
return &Logger{Logger: l.Logger.With(args...)}
}
// WithGroup returns a new logger with a group prefix
func (l *Logger) WithGroup(name string) *Logger {
return &Logger{Logger: l.Logger.WithGroup(name)}
}