diff --git a/config.yml b/config.yml index 916ef0a..a47f2ca 100644 --- a/config.yml +++ b/config.yml @@ -1,3 +1,7 @@ +logging: + log_level: "debug" # Can be debug, info, warn, error, or fatal + log_file: "/var/log/backea/app.log" # Path to log file (optional) + defaults: retention: keep_latest: 10 @@ -31,28 +35,4 @@ services: type: "kopia" provider: "local" destination: - path: "/local/backup/path" - 2: - backup_strategy: - type: "kopia" - provider: "b2" - # Credentials are in .env - 3: - backup_strategy: - <<: *sftp_strategy # This uses the common sftp strategy defined above - - # Example of another service - another_service: - source: - host: "local" - path: "/path/to/another/service" - hooks: - before_hook: "systemctl stop service-name" - after_hook: "systemctl start service-name" - backup_configs: - 1: - backup_strategy: - type: "kopia" - provider: "local" - destination: - path: "/local/backup/path" \ No newline at end of file + path: "/tmp" diff --git a/internal/logging/logging.go b/internal/logging/logging.go index be32369..bf52e7f 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -14,7 +14,7 @@ import ( var ( // Global logger instance logger *Logger - once sync.Once + mu sync.Mutex // Mutex to protect logger initialization and replacement ) // LogLevel represents the severity of a log message @@ -28,59 +28,104 @@ const ( FATAL ) +// String returns the string representation of the log level +func (l LogLevel) String() string { + switch l { + case DEBUG: + return "DEBUG" + case INFO: + return "INFO" + case WARN: + return "WARN" + case ERROR: + return "ERROR" + case FATAL: + return "FATAL" + default: + return "UNKNOWN" + } +} + // Logger provides logging functionality type Logger struct { logLevel LogLevel output io.Writer stdLog *log.Logger errLog *log.Logger + file *os.File // Store file reference for closing mu sync.Mutex } -// Singleton -func InitLogger(level LogLevel, logFile string) { - once.Do(func() { - var output io.Writer = os.Stdout +// InitLogger initializes or reinitializes the global logger instance +func InitLogger(level LogLevel, logFilePath string) { + mu.Lock() + defer mu.Unlock() - if logFile != "" { - logDir := filepath.Dir(logFile) - if err := os.MkdirAll(logDir, 0755); err != nil { - fmt.Fprintf(os.Stderr, "Failed to create log directory: %v\n", err) - os.Exit(1) - } + // Close the existing logger if there is one + if logger != nil { + logger.Close() + } - file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to open log file: %v\n", err) - os.Exit(1) - } + fmt.Printf("Initializing logger with level %s and log file: %s\n", level.String(), logFilePath) - output = io.MultiWriter(os.Stdout, file) + var output io.Writer = os.Stdout + var fileHandle *os.File + + // Set up log file if specified + if logFilePath != "" { + logDir := filepath.Dir(logFilePath) + if err := os.MkdirAll(logDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create log directory %s: %v\n", logDir, err) + os.Exit(1) } - logger = &Logger{ - logLevel: level, - output: output, - stdLog: log.New(output, "", log.LstdFlags), - errLog: log.New(output, "ERROR: ", log.LstdFlags), + file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to open log file %s: %v\n", logFilePath, err) + os.Exit(1) } - }) + + fmt.Printf("Successfully opened log file: %s\n", logFilePath) + fileHandle = file + output = io.MultiWriter(os.Stdout, file) + } + + logger = &Logger{ + logLevel: level, + output: output, + stdLog: log.New(output, "", log.LstdFlags), + errLog: log.New(output, "ERROR: ", log.LstdFlags), + file: fileHandle, + } + + // Log initialization message through the logger itself + logger.Info("Logger initialized with level %s and log file %s", level.String(), logFilePath) } -// Singleton +// GetLogger returns the global logger instance, initializing it if necessary func GetLogger() *Logger { + mu.Lock() + defer mu.Unlock() + if logger == nil { + // Initialize with default settings if not already initialized + // We need to unlock before calling InitLogger since it will lock mu + mu.Unlock() InitLogger(INFO, "") + mu.Lock() } return logger } +// SetLogLevel changes the current log level func (l *Logger) SetLogLevel(level LogLevel) { l.mu.Lock() defer l.mu.Unlock() l.logLevel = level + l.Info("Log level set to %s", level.String()) } +// log is the internal logging function func (l *Logger) log(level LogLevel, format string, v ...interface{}) { if level < l.logLevel { return @@ -89,20 +134,22 @@ func (l *Logger) log(level LogLevel, format string, v ...interface{}) { l.mu.Lock() defer l.mu.Unlock() - // Get caller information + // Get caller information (skip 2 levels to get the actual calling function) _, file, line, ok := runtime.Caller(2) if !ok { file = "unknown" line = 0 } - // Format message with source location - message := fmt.Sprintf("%s:%d: %s", filepath.Base(file), line, fmt.Sprintf(format, v...)) + // Format message with source location and level + prefix := fmt.Sprintf("[%s] %s:%d: ", level.String(), filepath.Base(file), line) + message := fmt.Sprintf(format, v...) + fullMessage := prefix + message if level >= ERROR { - l.errLog.Println(message) + l.errLog.Println(fullMessage) } else { - l.stdLog.Println(message) + l.stdLog.Println(fullMessage) } } @@ -129,6 +176,7 @@ func (l *Logger) Error(format string, v ...interface{}) { // Fatal logs a fatal message and exits the program func (l *Logger) Fatal(format string, v ...interface{}) { l.log(FATAL, format, v...) + l.Close() // Ensure logs are flushed before exit os.Exit(1) } @@ -139,9 +187,30 @@ func (l *Logger) ErrorWithStack(err error, format string, v ...interface{}) { } message := fmt.Sprintf(format, v...) - - buf := make([]byte, 1024) + buf := make([]byte, 4096) // Larger buffer for more stack trace info n := runtime.Stack(buf, false) - l.Error("%s: %v\n%s", message, err, buf[:n]) } + +// Close flushes and closes the log file if one is being used +func (l *Logger) Close() { + l.mu.Lock() + defer l.mu.Unlock() + + if l.file != nil { + l.file.Sync() // Flush any buffered data + l.file.Close() // Close the file + l.file = nil // Clear the reference + } +} + +// Shutdown properly closes the logger +func Shutdown() { + mu.Lock() + defer mu.Unlock() + + if logger != nil { + logger.Close() + logger = nil + } +}