package logger import ( "fmt" "io" "os" "sync" "time" ) // ANSI color codes const ( colorReset = "\033[0m" colorRed = "\033[31m" colorGreen = "\033[32m" colorYellow = "\033[33m" colorBlue = "\033[34m" colorPurple = "\033[35m" colorCyan = "\033[36m" colorWhite = "\033[37m" ) // Log levels const ( LevelDebug = iota LevelInfo LevelWarning LevelError LevelFatal ) // Level names and colors var levelProps = map[int]struct { tag string color string }{ LevelDebug: {" DBG", colorCyan}, LevelInfo: {"INFO", colorBlue}, LevelWarning: {"WARN", colorYellow}, LevelError: {" ERR", colorRed}, LevelFatal: {"FATL", colorPurple}, } // Time format for log messages const timeFormat = "15:04:05" // logMessage represents a message to be logged type logMessage struct { level int message string rawMode bool // Indicates if raw formatting should be used } // Logger handles logging operations type Logger struct { writer io.Writer messages chan logMessage wg sync.WaitGroup level int useColors bool done chan struct{} timeFormat string mu sync.Mutex // Mutex for thread-safe writing } // New creates a new logger func New(minLevel int, useColors bool) *Logger { l := &Logger{ writer: os.Stdout, messages: make(chan logMessage, 100), // Buffer 100 messages level: minLevel, useColors: useColors, done: make(chan struct{}), timeFormat: timeFormat, } l.wg.Add(1) go l.processLogs() return l } // SetOutput changes the output destination func (l *Logger) SetOutput(w io.Writer) { l.mu.Lock() defer l.mu.Unlock() l.writer = w } // TimeFormat returns the current time format func (l *Logger) TimeFormat() string { return l.timeFormat } // SetTimeFormat changes the time format string func (l *Logger) SetTimeFormat(format string) { l.timeFormat = format } // SetLevel changes the minimum log level func (l *Logger) SetLevel(level int) { l.level = level } // EnableColors enables ANSI color codes in the output func (l *Logger) EnableColors() { l.useColors = true } // DisableColors disables ANSI color codes in the output func (l *Logger) DisableColors() { l.useColors = false } // processLogs processes incoming log messages func (l *Logger) processLogs() { defer l.wg.Done() for { select { case msg := <-l.messages: if msg.level >= l.level { l.writeMessage(msg) } case <-l.done: // Process remaining messages for { select { case msg := <-l.messages: if msg.level >= l.level { l.writeMessage(msg) } default: return } } } } } // writeMessage writes a formatted log message func (l *Logger) writeMessage(msg logMessage) { var logLine string if msg.rawMode { // Raw mode - message is already formatted, just append newline logLine = msg.message + "\n" } else { // Standard format with timestamp, level tag, and message now := time.Now().Format(l.timeFormat) props := levelProps[msg.level] if l.useColors { logLine = fmt.Sprintf("%s %s[%s]%s %s\n", now, props.color, props.tag, colorReset, msg.message) } else { logLine = fmt.Sprintf("%s [%s] %s\n", now, props.tag, msg.message) } } // Synchronize writing l.mu.Lock() _, _ = fmt.Fprint(l.writer, logLine) l.mu.Unlock() // Auto-flush for fatal errors if msg.level == LevelFatal { if f, ok := l.writer.(*os.File); ok { _ = f.Sync() } } } // log sends a message to the logger goroutine func (l *Logger) log(level int, format string, args ...any) { if level < l.level { return } var message string if len(args) > 0 { message = fmt.Sprintf(format, args...) } else { message = format } // Don't block if channel is full select { case l.messages <- logMessage{level: level, message: message, rawMode: false}: // Message sent default: // Channel full, write directly l.writeMessage(logMessage{level: level, message: message, rawMode: false}) } // Exit on fatal errors if level == LevelFatal { l.Close() os.Exit(1) } } // LogRaw logs a message with raw formatting, bypassing the standard format func (l *Logger) LogRaw(format string, args ...any) { // Use info level for filtering if LevelInfo < l.level { return } var message string if len(args) > 0 { message = fmt.Sprintf(format, args...) } else { message = format } // Don't apply colors if disabled if !l.useColors { // Strip ANSI color codes if colors are disabled // Simple approach to strip common ANSI codes message = removeAnsiColors(message) } // Don't block if channel is full select { case l.messages <- logMessage{level: LevelInfo, message: message, rawMode: true}: // Message sent default: // Channel full, write directly l.writeMessage(logMessage{level: LevelInfo, message: message, rawMode: true}) } } // Simple helper to remove ANSI color codes func removeAnsiColors(s string) string { result := "" inEscape := false for _, c := range s { if inEscape { if c == 'm' { inEscape = false } continue } if c == '\033' { inEscape = true continue } result += string(c) } return result } // Debug logs a debug message func (l *Logger) Debug(format string, args ...any) { l.log(LevelDebug, format, args...) } // Info logs an informational message func (l *Logger) Info(format string, args ...any) { l.log(LevelInfo, format, args...) } // Warning logs a warning message func (l *Logger) Warning(format string, args ...any) { l.log(LevelWarning, format, args...) } // Error logs an error message func (l *Logger) Error(format string, args ...any) { l.log(LevelError, format, args...) } // Fatal logs a fatal error message and exits func (l *Logger) Fatal(format string, args ...any) { l.log(LevelFatal, format, args...) // No need for os.Exit here as it's handled in log() } // Close shuts down the logger goroutine func (l *Logger) Close() { close(l.done) l.wg.Wait() close(l.messages) } // Default global logger var defaultLogger = New(LevelInfo, true) // Debug logs a debug message to the default logger func Debug(format string, args ...any) { defaultLogger.Debug(format, args...) } // Info logs an informational message to the default logger func Info(format string, args ...any) { defaultLogger.Info(format, args...) } // Warning logs a warning message to the default logger func Warning(format string, args ...any) { defaultLogger.Warning(format, args...) } // Error logs an error message to the default logger func Error(format string, args ...any) { defaultLogger.Error(format, args...) } // Fatal logs a fatal error message to the default logger and exits func Fatal(format string, args ...any) { defaultLogger.Fatal(format, args...) } // LogRaw logs a raw message to the default logger func LogRaw(format string, args ...any) { defaultLogger.LogRaw(format, args...) } // SetLevel changes the minimum log level of the default logger func SetLevel(level int) { defaultLogger.SetLevel(level) } // SetOutput changes the output destination of the default logger func SetOutput(w io.Writer) { defaultLogger.SetOutput(w) } // Close shuts down the default logger func Close() { defaultLogger.Close() }