package logger import ( "bytes" "fmt" "io" "log" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" ) // 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 ) var levelNames = map[LogLevel]string{ LevelError: "ERROR", LevelWarning: "WARNING", LevelInfo: "INFO", LevelDebug: "DEBUG", LevelTrace: "TRACE", LevelLua: "LUA", } 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 } // 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 flag int useColors bool callerOffset int defaultFields map[string]interface{} showGoroutine bool } var ( // DefaultLogger is the global logger instance DefaultLogger *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, flag: flag, useColors: true, callerOffset: 0, defaultFields: make(map[string]interface{}), showGoroutine: true, } } // Init initializes the DefaultLogger func Init(level LogLevel) { initMutex.Lock() defer initMutex.Unlock() if DefaultLogger == nil { DefaultLogger = New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile) } DefaultLogger.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, 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, 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 } // 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, resetColor string if l.useColors { levelColor = levelColors[level] resetColor = ResetColor } 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 with both brackets colored levelStr := fmt.Sprintf("%s[%s]%s", levelColor, levelNames[level], levelColor) // Add a space after the level and before the reset color levelColumn := fmt.Sprintf("%s %s", levelStr, resetColor) return fmt.Sprintf("%s%s%s%s%s%s%s\n", l.prefix, timeStr, caller, goroutineStr, levelColumn, msg, fields) } // 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() msg := l.formatMessage(level, format, args...) fmt.Fprint(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 DefaultLogger == nil { Init(defaultLogLevel) } DefaultLogger.Error(format, args...) } // Warning logs a warning message using the default logger func Warning(format string, args ...interface{}) { if DefaultLogger == nil { Init(defaultLogLevel) } DefaultLogger.Warning(format, args...) } // Info logs an informational message using the default logger func Info(format string, args ...interface{}) { if DefaultLogger == nil { Init(defaultLogLevel) } DefaultLogger.Info(format, args...) } // Debug logs a debug message using the default logger func Debug(format string, args ...interface{}) { if DefaultLogger == nil { Init(defaultLogLevel) } DefaultLogger.Debug(format, args...) } // Trace logs a trace message using the default logger func Trace(format string, args ...interface{}) { if DefaultLogger == nil { Init(defaultLogLevel) } DefaultLogger.Trace(format, args...) } // Lua logs a Lua message using the default logger func Lua(format string, args ...interface{}) { if DefaultLogger == nil { Init(defaultLogLevel) } DefaultLogger.Lua(format, args...) } // LogPanic logs a panic error and its stack trace func LogPanic(r interface{}) { if DefaultLogger == nil { Init(defaultLogLevel) } stack := make([]byte, 4096) n := runtime.Stack(stack, false) DefaultLogger.Error("PANIC: %v\n%s", r, stack[:n]) } // SetLevel sets the log level for the default logger func SetLevel(level LogLevel) { if DefaultLogger == nil { Init(level) return } DefaultLogger.SetLevel(level) } // GetLevel gets the log level for the default logger func GetLevel() LogLevel { if DefaultLogger == nil { Init(defaultLogLevel) } return DefaultLogger.GetLevel() } // WithField returns a new logger with the field added to the default logger's context func WithField(key string, value interface{}) *Logger { if DefaultLogger == nil { Init(defaultLogLevel) } return DefaultLogger.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 DefaultLogger == nil { Init(defaultLogLevel) } return DefaultLogger.WithFields(fields) } // SetShowGoroutine enables or disables goroutine ID display in the default logger func SetShowGoroutine(show bool) { if DefaultLogger == nil { Init(defaultLogLevel) } DefaultLogger.SetShowGoroutine(show) } // ShowGoroutine returns whether goroutine ID is included in default logger's messages func ShowGoroutine() bool { if DefaultLogger == nil { Init(defaultLogLevel) } return DefaultLogger.ShowGoroutine() }