// package cylogger package main 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 ) // LevelStyle defines the visual style for a log level type LevelStyle struct { Tag string // e.g., "ERROR", "INFO" TagColor string // ANSI code for tag text TagBackgroundColor string // ANSI code for tag background MessageColor string // ANSI code for message text MessageBackgroundColor string // ANSI code for message background } // levelStyles maps LogLevel to its display style var levelStyles = map[LogLevel]LevelStyle{ LevelError: { Tag: "ERROR", TagColor: BIRed, // Bold Intense Red TagBackgroundColor: On_White, // White background MessageColor: White, // Bold White text MessageBackgroundColor: On_IRed, // Intense Red background }, LevelWarning: { Tag: "WARNING", TagColor: BIOrange, // Bold Intense Orange TagBackgroundColor: On_White, // White background MessageColor: White, // Bold White text MessageBackgroundColor: On_IOrange, // Intense Orange background }, LevelInfo: { Tag: "INFO", TagColor: BGreen, // Bold Green }, LevelDebug: { Tag: "DEBUG", TagColor: BCyan, // Bold Cyan }, LevelTrace: { Tag: "TRACE", TagColor: BPurple, // Bold Purple }, LevelLua: { Tag: "LUA", TagColor: BBlue, // Bold Blue }, LevelPrefix: { Tag: "PREFIX", // Used for coloring the user prefix TagColor: BIBlack, // Bold Intense Black (Dark Grey) }, } // 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 := levelStyles[l]; ok { return name.Tag } 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 tagFgColor, tagBgColor, messageBgColor string useSpecialFormatting := false // Flag for levels with custom message background/foreground if l.useColors { // Check if a message background color is defined for this level style if levelStyles[level].MessageBackgroundColor != "" { useSpecialFormatting = true // Retrieve all style components from the map tagFgColor = levelStyles[level].TagColor // Assign directly tagBgColor = levelStyles[level].TagBackgroundColor // messageFgColor = levelStyles[level].MessageColor messageBgColor = levelStyles[level].MessageBackgroundColor } else { // For other levels (INFO, DEBUG, etc.), only TagColor is guaranteed tagFgColor = levelStyles[level].TagColor // Use the defined tag color // tagBgColor, messageFgColor, messageBgColor remain empty (use terminal defaults) } } var caller string if l.flag&log.Lshortfile != 0 || l.flag&log.Llongfile != 0 { 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 } // Check if the caller is within this logger package itself if !strings.Contains(file, "main.go") && !strings.Contains(file, "colors.go") { break } } if !ok { file = "???" line = 0 } if l.flag&log.Lshortfile != 0 { file = filepath.Base(file) } // Caller string - no background color applied here caller = fmt.Sprintf("%-25s ", file+":"+strconv.Itoa(line)) } // Format the timestamp with fixed width - no background color applied here 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 - no background color applied here var goroutineStr string if l.showGoroutine { goroutineID := GetGoroutineID() goroutineStr = fmt.Sprintf("[g:%-4s] ", goroutineID) } // --- Level Tag Formatting and Padding --- levelStr := levelStyles[level].Tag visibleTagContent := fmt.Sprintf("[%s]", levelStr) visibleTagLen := len(visibleTagContent) paddingWidth := 10 // Target width for the level column (tag + padding) numSpaces := paddingWidth - visibleTagLen if numSpaces < 0 { numSpaces = 1 // Ensure at least one space } padding := strings.Repeat(" ", numSpaces) var levelTagFormatted string if useSpecialFormatting { // ERROR/WARNING: Tag has specific background and foreground levelTagFormatted = fmt.Sprintf("%s%s%s%s", tagBgColor, tagFgColor, visibleTagContent, Reset) } else { // Other levels: Tag has standard foreground color only levelTagFormatted = fmt.Sprintf("%s%s%s", tagFgColor, visibleTagContent, Reset) } levelColumn := levelTagFormatted + padding // Combine formatted tag and padding // --- User Prefix Formatting (part of message content for coloring purposes) --- userPrefixStr := "" if l.userPrefix != "" { // Format the string part here, colors applied later if needed userPrefixStr = fmt.Sprintf("[%s] ", l.userPrefix) } // --- Message Content --- messageContent := fmt.Sprintf("%s%s", msg, fields) // --- Assemble Final String --- var finalMsg strings.Builder // Part 1: Timestamp, Caller, Goroutine ID (always default colors) finalMsg.WriteString(l.prefix) finalMsg.WriteString(timeStr) finalMsg.WriteString(caller) finalMsg.WriteString(goroutineStr) // Part 2: Level Column (already formatted with tag colors and padding) finalMsg.WriteString(levelColumn) // Part 3: User Prefix + Message Content (apply special formatting if needed) if useSpecialFormatting { // ERROR/WARNING: Apply message background and foreground to User Prefix + Message finalMsg.WriteString(messageBgColor) // finalMsg.WriteString(messageFgColor) // This doesn't work...? For some reason? finalMsg.WriteString(userPrefixStr) // Write user prefix inside the colored block finalMsg.WriteString(messageContent) finalMsg.WriteString(Reset) } else { // Other levels: User Prefix and Message content use default colors // Apply specific color to user prefix if it exists if l.userPrefix != "" { prefixColor := levelStyles[LevelPrefix].TagColor finalMsg.WriteString(fmt.Sprintf("%s%s%s", prefixColor, userPrefixStr, Reset)) } // No else needed, if userPrefix is empty, userPrefixStr is "" anyway finalMsg.WriteString(messageContent) // Append message with default colors } return finalMsg.String() } // 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() } func main() { Init(LevelDebug) // Test basic logging Debug("This is a debug message") Info("This is an info message") Warning("This is a warning message") Error("This is an error message") // Test logging with fields logger := WithField("user", "testuser") logger.Info("User logged in") // Test logging with multiple fields fields := map[string]interface{}{ "user": "testuser", "role": "admin", "id": 12345, } WithFields(fields).Info("User details") // Test error logging with fields WithField("error", "connection failed").Error("Database error") logger.WithPrefix("CUSTOM").Info("This is a message with a custom prefix") // Test goroutine ID display SetShowGoroutine(true) Info("This message should show goroutine ID") // Test different log levels SetLevel(LevelInfo) Debug("This debug message should not appear") Info("This info message should appear") // Test with custom prefix WithField("prefix", "custom").Info("Message with custom prefix") Lua("This is a Lua message") }