package logger import ( "bytes" "flag" "fmt" "io" "log" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" ) // TODO: Enable turning colors on and off maybe even per stream? var loglevel = flag.String("loglevel", "info", "log level") // LogLevel defines the severity of log messages type LogLevel int const ( // LevelError is for critical errors that should always be displayed LevelError LogLevel = iota // LevelWarning is for important warnings LevelWarning // LevelInfo is for informational messages LevelInfo // LevelDebug is for detailed debugging information LevelDebug // LevelTrace is for very detailed tracing information LevelTrace // LevelLua is specifically for output from Lua scripts LevelLua LevelPrefix ) var levelNames = map[LogLevel]string{ LevelError: "ERROR", LevelWarning: "WARNING", LevelInfo: "INFO", LevelDebug: "DEBUG", LevelTrace: "TRACE", LevelLua: "LUA", LevelPrefix: "PREFIX", } var levelColors = map[LogLevel]string{ LevelError: "\033[1;31m", // Bold Red LevelWarning: "\033[1;33m", // Bold Yellow LevelInfo: "\033[1;32m", // Bold Green LevelDebug: "\033[1;36m", // Bold Cyan LevelTrace: "\033[1;35m", // Bold Magenta LevelLua: "\033[1;34m", // Bold Blue LevelPrefix: "\033[0;90m", // Regular Dark Grey } // ANSI Background Colors var levelBackgroundColors = map[LogLevel]string{ LevelError: "\033[41m", // Red Background LevelWarning: "\033[43m", // Yellow Background } // ANSI Foreground Colors (adjusting for readability on backgrounds) const FgWhiteBold = "\033[1;37m" // ResetColor is the ANSI code to reset text color const ResetColor = "\033[0m" // Logger is our custom logger with level support type Logger struct { mu sync.Mutex out io.Writer currentLevel LogLevel prefix string userPrefix string flag int useColors bool callerOffset int defaultFields map[string]interface{} showGoroutine bool } var ( // Default is the global logger instance Default *Logger // defaultLogLevel is the default log level if not specified defaultLogLevel = LevelInfo // Global mutex for DefaultLogger initialization initMutex sync.Mutex ) // ParseLevel converts a string log level to LogLevel func ParseLevel(levelStr string) LogLevel { switch strings.ToUpper(levelStr) { case "ERROR": return LevelError case "WARNING", "WARN": return LevelWarning case "INFO": return LevelInfo case "DEBUG": return LevelDebug case "TRACE": return LevelTrace case "LUA": return LevelLua default: return defaultLogLevel } } // String returns the string representation of the log level func (l LogLevel) String() string { if name, ok := levelNames[l]; ok { return name } return fmt.Sprintf("Level(%d)", l) } // New creates a new Logger instance func New(out io.Writer, prefix string, flag int) *Logger { return &Logger{ out: out, currentLevel: defaultLogLevel, prefix: prefix, userPrefix: "", flag: flag, useColors: true, callerOffset: 0, defaultFields: make(map[string]interface{}), showGoroutine: true, } } func InitFlag() { level := ParseLevel(*loglevel) Init(level) } // Init initializes the DefaultLogger func Init(level LogLevel) { initMutex.Lock() defer initMutex.Unlock() if Default == nil { Default = New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile) } Default.SetLevel(level) } // SetLevel sets the current log level func (l *Logger) SetLevel(level LogLevel) { l.mu.Lock() defer l.mu.Unlock() l.currentLevel = level } // GetLevel returns the current log level func (l *Logger) GetLevel() LogLevel { l.mu.Lock() defer l.mu.Unlock() return l.currentLevel } // SetCallerOffset sets the caller offset for correct file and line reporting func (l *Logger) SetCallerOffset(offset int) { l.mu.Lock() defer l.mu.Unlock() l.callerOffset = offset } // SetShowGoroutine sets whether to include goroutine ID in log messages func (l *Logger) SetShowGoroutine(show bool) { l.mu.Lock() defer l.mu.Unlock() l.showGoroutine = show } // ShowGoroutine returns whether goroutine ID is included in log messages func (l *Logger) ShowGoroutine() bool { l.mu.Lock() defer l.mu.Unlock() return l.showGoroutine } // WithField adds a field to the logger's context func (l *Logger) WithField(key string, value interface{}) *Logger { newLogger := &Logger{ out: l.out, currentLevel: l.currentLevel, prefix: l.prefix, userPrefix: l.userPrefix, flag: l.flag, useColors: l.useColors, callerOffset: l.callerOffset, defaultFields: make(map[string]interface{}), showGoroutine: l.showGoroutine, } // Copy existing fields for k, v := range l.defaultFields { newLogger.defaultFields[k] = v } // Add new field newLogger.defaultFields[key] = value return newLogger } // WithFields adds multiple fields to the logger's context func (l *Logger) WithFields(fields map[string]interface{}) *Logger { newLogger := &Logger{ out: l.out, currentLevel: l.currentLevel, prefix: l.prefix, userPrefix: l.userPrefix, flag: l.flag, useColors: l.useColors, callerOffset: l.callerOffset, defaultFields: make(map[string]interface{}), showGoroutine: l.showGoroutine, } // Copy existing fields for k, v := range l.defaultFields { newLogger.defaultFields[k] = v } // Add new fields for k, v := range fields { newLogger.defaultFields[k] = v } return newLogger } func (l *Logger) WithPrefix(prefix string) *Logger { if Default == nil { Init(defaultLogLevel) } if l == nil { l = Default } newLogger := &Logger{ out: l.out, currentLevel: l.currentLevel, prefix: l.prefix, userPrefix: strings.TrimSpace(l.userPrefix + " " + prefix), flag: l.flag, useColors: l.useColors, callerOffset: l.callerOffset, defaultFields: make(map[string]interface{}), showGoroutine: l.showGoroutine, } // Copy existing fields for k, v := range l.defaultFields { newLogger.defaultFields[k] = v } return newLogger } func (l *Logger) ToFile(filename string) *Logger { file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { log.Fatal(err) } if Default == nil { Init(defaultLogLevel) } if l == nil { l = Default } newLogger := &Logger{ out: io.MultiWriter(l.out, file), currentLevel: l.currentLevel, prefix: l.prefix, userPrefix: l.userPrefix, flag: l.flag, useColors: l.useColors, callerOffset: l.callerOffset, defaultFields: make(map[string]interface{}), showGoroutine: l.showGoroutine, } return newLogger } // GetGoroutineID extracts the goroutine ID from the runtime stack func GetGoroutineID() string { buf := make([]byte, 64) n := runtime.Stack(buf, false) // Format of first line is "goroutine N [state]:" // We only need the N part buf = buf[:n] idField := bytes.Fields(bytes.Split(buf, []byte{':'})[0])[1] return string(idField) } // formatMessage formats a log message with level, time, file, and line information func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{}) string { var msg string if len(args) > 0 { msg = fmt.Sprintf(format, args...) } else { msg = format } // Format default fields if any var fields string if len(l.defaultFields) > 0 { var pairs []string for k, v := range l.defaultFields { pairs = append(pairs, fmt.Sprintf("%s=%v", k, v)) } fields = " " + strings.Join(pairs, " ") } var levelColor, bgColor, msgFgColor, resetColor string useBgColor := false // Flag to indicate if background color should be used if l.useColors { resetColor = ResetColor levelColor = levelColors[level] // Color for the level tag text ONLY if bg, ok := levelBackgroundColors[level]; ok { // Check if this level has a background color defined (ERROR/WARNING) bgColor = bg msgFgColor = FgWhiteBold // Use bold white for the message part on colored background useBgColor = true } else { // For other levels, message part uses default terminal color. // msgFgColor remains empty, bgColor remains empty. // levelColor is still needed for the tag below. } } var caller string if l.flag&log.Lshortfile != 0 || l.flag&log.Llongfile != 0 { // Find the actual caller by scanning up the stack // until we find a function outside the logger package var file string var line int var ok bool // Start at a reasonable depth and scan up to 10 frames for depth := 4; depth < 15; depth++ { _, file, line, ok = runtime.Caller(depth) if !ok { break } // If the caller is not in the logger package, we found our caller if !strings.Contains(file, "logger/logger.go") { break } } if !ok { file = "???" line = 0 } if l.flag&log.Lshortfile != 0 { file = filepath.Base(file) } caller = fmt.Sprintf("%-25s ", file+":"+strconv.Itoa(line)) } // Format the timestamp with fixed width var timeStr string if l.flag&(log.Ldate|log.Ltime|log.Lmicroseconds) != 0 { t := time.Now() if l.flag&log.Ldate != 0 { timeStr += fmt.Sprintf("%04d/%02d/%02d ", t.Year(), t.Month(), t.Day()) } if l.flag&(log.Ltime|log.Lmicroseconds) != 0 { timeStr += fmt.Sprintf("%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second()) if l.flag&log.Lmicroseconds != 0 { timeStr += fmt.Sprintf(".%06d", t.Nanosecond()/1000) } } timeStr = fmt.Sprintf("%-15s ", timeStr) } // Add goroutine ID if enabled, with fixed width var goroutineStr string if l.showGoroutine { goroutineID := GetGoroutineID() goroutineStr = fmt.Sprintf("[g:%-4s] ", goroutineID) } // Create a colored level indicator - color only the level name text, reset locally levelStr := fmt.Sprintf("%s%s%s", levelColor, levelNames[level], resetColor) levelColumn := fmt.Sprintf("%-20s", levelStr) // Pad the tag userPrefixStr := "" if l.userPrefix != "" { // Use default foreground color for prefix (dark grey), reset locally prefixColor := levelColors[LevelPrefix] userPrefixStr = fmt.Sprintf("%s[%s]%s ", prefixColor, l.userPrefix, resetColor) } // Build the prefix part (timestamp, caller, goroutine, level tag, user prefix) // These parts will use default terminal colors or their specifically set colors (like level tag) prefixPart := fmt.Sprintf("%s%s%s%s%s%s", l.prefix, timeStr, caller, goroutineStr, levelColumn, userPrefixStr) // Build the message part messagePart := fmt.Sprintf("%s%s", msg, fields) // Combine prefix and message, applying background/foreground ONLY to the message part if useBgColor { // Apply background and specific foreground ONLY to the message part return fmt.Sprintf("%s%s%s%s%s", prefixPart, bgColor, msgFgColor, messagePart, resetColor) } else { // For non-ERROR/WARNING, just combine prefix (with its colored tag) and the uncolored message part. // No additional color codes needed here for the message itself. return fmt.Sprintf("%s%s", prefixPart, messagePart) } } // log logs a message at the specified level func (l *Logger) log(level LogLevel, format string, args ...interface{}) { // Always show LUA level logs regardless of the current log level if level != LevelLua && level > l.currentLevel { return } l.mu.Lock() defer l.mu.Unlock() // Get formatted message with potential background color msg := l.formatMessage(level, format, args...) fmt.Fprintln(l.out, msg) } // Error logs an error message func (l *Logger) Error(format string, args ...interface{}) { l.log(LevelError, format, args...) } // Warning logs a warning message func (l *Logger) Warning(format string, args ...interface{}) { l.log(LevelWarning, format, args...) } // Info logs an informational message func (l *Logger) Info(format string, args ...interface{}) { l.log(LevelInfo, format, args...) } // Debug logs a debug message func (l *Logger) Debug(format string, args ...interface{}) { l.log(LevelDebug, format, args...) } // Trace logs a trace message func (l *Logger) Trace(format string, args ...interface{}) { l.log(LevelTrace, format, args...) } // Lua logs a Lua message func (l *Logger) Lua(format string, args ...interface{}) { l.log(LevelLua, format, args...) } // Global log functions that use DefaultLogger // Error logs an error message using the default logger func Error(format string, args ...interface{}) { if Default == nil { Init(defaultLogLevel) } Default.Error(format, args...) } // Warning logs a warning message using the default logger func Warning(format string, args ...interface{}) { if Default == nil { Init(defaultLogLevel) } Default.Warning(format, args...) } // Info logs an informational message using the default logger func Info(format string, args ...interface{}) { if Default == nil { Init(defaultLogLevel) } Default.Info(format, args...) } // Debug logs a debug message using the default logger func Debug(format string, args ...interface{}) { if Default == nil { Init(defaultLogLevel) } Default.Debug(format, args...) } // Trace logs a trace message using the default logger func Trace(format string, args ...interface{}) { if Default == nil { Init(defaultLogLevel) } Default.Trace(format, args...) } // Lua logs a Lua message using the default logger func Lua(format string, args ...interface{}) { if Default == nil { Init(defaultLogLevel) } Default.Lua(format, args...) } // LogPanic logs a panic error and its stack trace func LogPanic(r interface{}) { if Default == nil { Init(defaultLogLevel) } stack := make([]byte, 4096) n := runtime.Stack(stack, false) Default.Error("PANIC: %v\n%s", r, stack[:n]) } // SetLevel sets the log level for the default logger func SetLevel(level LogLevel) { if Default == nil { Init(level) return } Default.SetLevel(level) } // GetLevel gets the log level for the default logger func GetLevel() LogLevel { if Default == nil { Init(defaultLogLevel) } return Default.GetLevel() } // WithField returns a new logger with the field added to the default logger's context func WithField(key string, value interface{}) *Logger { if Default == nil { Init(defaultLogLevel) } return Default.WithField(key, value) } // WithFields returns a new logger with the fields added to the default logger's context func WithFields(fields map[string]interface{}) *Logger { if Default == nil { Init(defaultLogLevel) } return Default.WithFields(fields) } // SetShowGoroutine enables or disables goroutine ID display in the default logger func SetShowGoroutine(show bool) { if Default == nil { Init(defaultLogLevel) } Default.SetShowGoroutine(show) } // ShowGoroutine returns whether goroutine ID is included in default logger's messages func ShowGoroutine() bool { if Default == nil { Init(defaultLogLevel) } return Default.ShowGoroutine() }