commit bf79762d572627bf5ffd4e61757829a908dd8a8b Author: PhatPhuckDave Date: Fri Apr 18 12:19:58 2025 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45784b0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.qodo diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8baf6bb --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module cylogger + +go 1.24.2 diff --git a/main.go b/main.go new file mode 100644 index 0000000..c32b087 --- /dev/null +++ b/main.go @@ -0,0 +1,575 @@ +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() +}