// Package logger provides a structured logger with source location tracking package logger import ( "fmt" "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", fmt.Sprintf("%s:%d", file, 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)} }