package logger import ( "fmt" "io" "os" "strings" "sync" "sync/atomic" "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" colorGray = "\033[90m" ) // Log levels const ( LevelDebug = iota LevelInfo LevelWarning LevelError LevelServer LevelFatal ) // Level names and colors var levelProps = map[int]struct { tag string color string }{ LevelDebug: {"D", colorCyan}, LevelInfo: {"I", colorBlue}, LevelWarning: {"W", colorYellow}, LevelError: {"E", colorRed}, LevelServer: {"S", colorGreen}, LevelFatal: {"F", colorPurple}, } // Time format for log messages const timeFormat = "15:04:05" // Single global logger instance with mutex for safe initialization var ( globalLogger *Logger globalLoggerOnce sync.Once ) // Logger handles logging operations type Logger struct { writer io.Writer level int useColors bool timeFormat string showTimestamp bool // Whether to show timestamp mu sync.Mutex // Mutex for thread-safe writing debugMode atomic.Bool // Force debug logging regardless of level } // GetLogger returns the global logger instance, creating it if needed func GetLogger() *Logger { globalLoggerOnce.Do(func() { globalLogger = newLogger(LevelInfo, true, true) }) return globalLogger } // InitGlobalLogger initializes the global logger with custom settings func InitGlobalLogger(minLevel int, useColors bool, showTimestamp bool) { // Reset the global logger instance globalLogger = newLogger(minLevel, useColors, showTimestamp) } // newLogger creates a new logger instance (internal use) func newLogger(minLevel int, useColors bool, showTimestamp bool) *Logger { logger := &Logger{ writer: os.Stdout, level: minLevel, useColors: useColors, timeFormat: timeFormat, showTimestamp: showTimestamp, } return logger } // New creates a new logger (deprecated - use GetLogger() instead) func New(minLevel int, useColors bool, showTimestamp bool) *Logger { return newLogger(minLevel, useColors, showTimestamp) } // 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.mu.Lock() defer l.mu.Unlock() l.timeFormat = format } // EnableTimestamp enables timestamp display func (l *Logger) EnableTimestamp() { l.showTimestamp = true } // DisableTimestamp disables timestamp display func (l *Logger) DisableTimestamp() { l.showTimestamp = false } // 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 } // EnableDebug forces debug logs to be shown regardless of level func (l *Logger) EnableDebug() { l.debugMode.Store(true) } // DisableDebug stops forcing debug logs func (l *Logger) DisableDebug() { l.debugMode.Store(false) } // IsDebugEnabled returns true if debug mode is enabled func (l *Logger) IsDebugEnabled() bool { return l.debugMode.Load() } // applyColor applies color to text if colors are enabled func (l *Logger) applyColor(text, color string) string { if l.useColors { return color + text + colorReset } return text } // stripAnsiColors removes ANSI color codes from a string func stripAnsiColors(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 } // writeMessage writes a formatted log message directly to the writer func (l *Logger) writeMessage(level int, message string, rawMode bool) { var logLine string if rawMode { // Raw mode - message is already formatted, just append newline logLine = message + "\n" } else { // Standard format with level tag and optional timestamp props := levelProps[level] if l.showTimestamp { now := time.Now().Format(l.timeFormat) if l.useColors { timestamp := l.applyColor(now, colorGray) tag := l.applyColor("["+props.tag+"]", props.color) logLine = fmt.Sprintf("%s %s %s\n", timestamp, tag, message) } else { logLine = fmt.Sprintf("%s [%s] %s\n", now, props.tag, message) } } else { // No timestamp, just level tag and message if l.useColors { tag := l.applyColor("["+props.tag+"]", props.color) logLine = fmt.Sprintf("%s %s\n", tag, message) } else { logLine = fmt.Sprintf("[%s] %s\n", props.tag, message) } } } // Synchronously write the log message l.mu.Lock() _, _ = fmt.Fprint(l.writer, logLine) // For fatal errors, ensure we sync immediately if level == LevelFatal { if f, ok := l.writer.(*os.File); ok { _ = f.Sync() } } l.mu.Unlock() } // log handles the core logging logic with level filtering func (l *Logger) log(level int, format string, args ...any) { // Check if we should log this message // Either level is high enough OR (it's a debug message AND debug mode is enabled) if level < l.level && !(level == LevelDebug && l.debugMode.Load()) { return } // Format message var message string if len(args) > 0 { message = fmt.Sprintf(format, args...) } else { message = format } l.writeMessage(level, message, false) // Exit on fatal errors if level == LevelFatal { 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 message = stripAnsiColors(message) } l.writeMessage(LevelInfo, message, true) } // 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() } // Server logs a server message func (l *Logger) Server(format string, args ...any) { l.log(LevelServer, format, args...) } // Global helper functions that use the global logger // Debug logs a debug message to the global logger func Debug(format string, args ...any) { GetLogger().Debug(format, args...) } // Info logs an informational message to the global logger func Info(format string, args ...any) { GetLogger().Info(format, args...) } // Warning logs a warning message to the global logger func Warning(format string, args ...any) { GetLogger().Warning(format, args...) } // Error logs an error message to the global logger func Error(format string, args ...any) { GetLogger().Error(format, args...) } // Fatal logs a fatal error message to the global logger and exits func Fatal(format string, args ...any) { GetLogger().Fatal(format, args...) } // Server logs a server message to the global logger func Server(format string, args ...any) { GetLogger().Server(format, args...) } // LogRaw logs a raw message to the global logger func LogRaw(format string, args ...any) { GetLogger().LogRaw(format, args...) } // SetLevel changes the minimum log level of the global logger func SetLevel(level int) { GetLogger().SetLevel(level) } // SetOutput changes the output destination of the global logger func SetOutput(w io.Writer) { GetLogger().SetOutput(w) } // TimeFormat returns the current time format of the global logger func TimeFormat() string { return GetLogger().TimeFormat() } // EnableDebug enables debug messages regardless of log level func EnableDebug() { GetLogger().EnableDebug() } // DisableDebug disables forced debug messages func DisableDebug() { GetLogger().DisableDebug() } // IsDebugEnabled returns true if debug mode is enabled func IsDebugEnabled() bool { return GetLogger().IsDebugEnabled() } // EnableTimestamp enables timestamp display func EnableTimestamp() { GetLogger().EnableTimestamp() } // DisableTimestamp disables timestamp display func DisableTimestamp() { GetLogger().DisableTimestamp() } // LogSpacer adds a horizontal line separator to the log output func (l *Logger) LogSpacer() { l.mu.Lock() defer l.mu.Unlock() // Calculate spacer width tagWidth := 7 // Standard width of tag area "[DEBUG]" var spacer string if l.showTimestamp { // Format: "15:04:05 [DEBUG] ----" timeWidth := len(time.Now().Format(l.timeFormat)) tagSpacer := strings.Repeat("-", tagWidth) restSpacer := strings.Repeat("-", 20) // Fixed width for the rest if l.useColors { timeStr := l.applyColor(strings.Repeat("-", timeWidth), colorGray) tagStr := l.applyColor(tagSpacer, colorCyan) spacer = fmt.Sprintf("%s %s %s\n", timeStr, tagStr, restSpacer) } else { spacer = fmt.Sprintf("%s %s %s\n", strings.Repeat("-", timeWidth), tagSpacer, restSpacer) } } else { // No timestamp: "[DEBUG] ----" tagSpacer := strings.Repeat("-", tagWidth) restSpacer := strings.Repeat("-", 20) // Fixed width for the rest if l.useColors { tagStr := l.applyColor(tagSpacer, colorCyan) spacer = fmt.Sprintf("%s %s\n", tagStr, restSpacer) } else { spacer = fmt.Sprintf("%s %s\n", tagSpacer, restSpacer) } } _, _ = fmt.Fprint(l.writer, spacer) } // LogSpacer adds a horizontal line separator to the global logger func LogSpacer() { GetLogger().LogSpacer() }