// internal/logging/logging.go package logging import ( "fmt" "io" "log" "os" "path/filepath" "runtime" "sync" ) var ( // Global logger instance logger *Logger mu sync.Mutex // Mutex to protect logger initialization and replacement ) // LogLevel represents the severity of a log message type LogLevel int const ( DEBUG LogLevel = iota INFO WARN ERROR 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 } // InitLogger initializes or reinitializes the global logger instance func InitLogger(level LogLevel, logFilePath string) { mu.Lock() defer mu.Unlock() // Close the existing logger if there is one if logger != nil { logger.Close() } fmt.Printf("Initializing logger with level %s and log file: %s\n", level.String(), logFilePath) 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) } 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) } // 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 } l.mu.Lock() defer l.mu.Unlock() // 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 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(fullMessage) } else { l.stdLog.Println(fullMessage) } } // Debug logs a debug message func (l *Logger) Debug(format string, v ...interface{}) { l.log(DEBUG, format, v...) } // Info logs an info message func (l *Logger) Info(format string, v ...interface{}) { l.log(INFO, format, v...) } // Warn logs a warning message func (l *Logger) Warn(format string, v ...interface{}) { l.log(WARN, format, v...) } // Error logs an error message func (l *Logger) Error(format string, v ...interface{}) { l.log(ERROR, format, v...) } // 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) } // ErrorWithStack logs an error with its stack trace func (l *Logger) ErrorWithStack(err error, format string, v ...interface{}) { if err == nil { return } message := fmt.Sprintf(format, v...) 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 } }