// @tag logging // @tag core package logger import ( "fmt" "log" "os" "sync" "time" ) // Level represents the minimum severity for log messages. type Level int // Log severity levels from most verbose to most critical. const ( LevelDebug Level = iota // Debug logs everything including diagnostic messages LevelInfo // Info logs informational messages and above (default) LevelWarn // Warn logs warnings, errors, and critical messages LevelError // LevelError logs only errors and critical messages ) var ( currentLevel = LevelInfo logger = log.New(os.Stdout, "", 0) logFile *os.File logPath string pid int mu sync.Once loggerMu sync.Mutex // Protects logger and logFile ) // initPID gets the process ID for log messages func initPID() { pid = os.Getpid() } // formatLog formats a message with timestamp, log level, and pid. // Format: timestamp level pekoe[pid]: message func formatLog(level string, message string) string { mu.Do(initPID) // Timestamp format: "YYYY Mmm dd hh:mm:ss" // e.g., "2024 Apr 2 14:30:15" now := time.Now() timestamp := now.Format("2006 Jan _2 15:04:05") // Format: timestamp level pekoe[pid]: message // e.g., "2024 Apr 2 14:30:15 INFO pekoe[12345]: event=startup" return fmt.Sprintf("%s %s pekoe[%d]: %s", timestamp, level, pid, message) } // SetOutput configures the logger to write to the specified file. // An empty path resets to stdout. // Returns an error if the file cannot be opened/created. // nolint: gosec func SetOutput(path string) error { loggerMu.Lock() defer loggerMu.Unlock() // Close existing log file if any if logFile != nil { _ = logFile.Close() logFile = nil } if path == "" { logPath = "" logger = log.New(os.Stdout, "", 0) return nil } file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } logPath = path logFile = file logger = log.New(file, "", 0) return nil } // Reopen reopens the log file. // This is useful for log rotation - after the log file is moved/renamed, // sending SIGUSR1 will cause the server to reopen the log file. // Returns an error if the log file cannot be opened. func Reopen() error { loggerMu.Lock() defer loggerMu.Unlock() // If logging to stdout, nothing to reopen if logPath == "" { return nil } // Close existing file if logFile != nil { _ = logFile.Close() logFile = nil } // Open the file again (create if doesn't exist) //nolint: gosec // logPath is configured by admin, not user input file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { // Fall back to stdout if we can't reopen logger = log.New(os.Stdout, "", 0) return fmt.Errorf("failed to reopen log file %s: %w", logPath, err) } logFile = file logger = log.New(file, "", 0) return nil } // SetLevel configures the minimum log level from a string name. // Accepted values: "debug", "info", "warn", "error". func SetLevel(level string) { switch level { case "debug": currentLevel = LevelDebug case "info": currentLevel = LevelInfo case "warn": currentLevel = LevelWarn case "error": currentLevel = LevelError } } // Debug logs a message at debug level. Uses fmt.Sprintf formatting. func Debug(format string, v ...any) { if currentLevel <= LevelDebug { msg := fmt.Sprintf(format, v...) logger.Println(formatLog("DEBUG", msg)) } } // Info logs a message at info level. Uses fmt.Sprintf formatting. func Info(format string, v ...any) { if currentLevel <= LevelInfo { msg := fmt.Sprintf(format, v...) logger.Println(formatLog("INFO", msg)) } } // Warn logs a message at warning level. Uses fmt.Sprintf formatting. func Warn(format string, v ...any) { if currentLevel <= LevelWarn { msg := fmt.Sprintf(format, v...) logger.Println(formatLog("WARN", msg)) } } // Error logs a message at error level. Uses fmt.Sprintf formatting. func Error(format string, v ...any) { if currentLevel <= LevelError { msg := fmt.Sprintf(format, v...) logger.Println(formatLog("ERROR", msg)) } } // Fatal logs a message at critical level and terminates the process with exit code 1. func Fatal(format string, v ...any) { msg := fmt.Sprintf(format, v...) logger.Println(formatLog("CRITICAL", msg)) os.Exit(1) }