routewatch/internal/logger/logger.go
sneak 95bbb655ab Add godoc documentation and README with code structure
Add comprehensive godoc comments to all exported types, functions,
and constants throughout the codebase. Create README.md documenting
the project architecture, execution flow, database schema, and
component relationships.
2025-12-27 12:30:46 +07:00

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