From 66bcf21d7997b3943b764a9345cbdd9a9f18a28a Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Thu, 27 Mar 2025 19:19:39 +0100 Subject: [PATCH] Add goroutine numbers to log lines --- logger/logger.go | 67 +++++++++++++++++++++++++++++- logger/panic_handler.go | 49 ++++++++++++++++++++++ main.go | 19 +++++---- regression/regression_test.go | 77 +++++++++++++++++++++++++++++++++++ 4 files changed, 201 insertions(+), 11 deletions(-) create mode 100644 logger/panic_handler.go diff --git a/logger/logger.go b/logger/logger.go index a9197ed..e59489b 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -1,6 +1,7 @@ package logger import ( + "bytes" "fmt" "io" "log" @@ -57,6 +58,7 @@ type Logger struct { useColors bool callerOffset int defaultFields map[string]interface{} + showGoroutine bool } var ( @@ -104,6 +106,7 @@ func New(out io.Writer, prefix string, flag int) *Logger { useColors: true, callerOffset: 0, defaultFields: make(map[string]interface{}), + showGoroutine: true, } } @@ -139,6 +142,20 @@ func (l *Logger) SetCallerOffset(offset int) { 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{ @@ -149,6 +166,7 @@ func (l *Logger) WithField(key string, value interface{}) *Logger { useColors: l.useColors, callerOffset: l.callerOffset, defaultFields: make(map[string]interface{}), + showGoroutine: l.showGoroutine, } // Copy existing fields @@ -171,6 +189,7 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger { useColors: l.useColors, callerOffset: l.callerOffset, defaultFields: make(map[string]interface{}), + showGoroutine: l.showGoroutine, } // Copy existing fields @@ -185,6 +204,17 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger { 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 @@ -257,8 +287,15 @@ func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{ } } - return fmt.Sprintf("%s%14s%-30s%s[%s%s%s]%s %s\n", - l.prefix, timeStr, caller, levelColor, levelNames[level], resetColor, fields, resetColor, msg) + // Add goroutine ID if enabled + var goroutineStr string + if l.showGoroutine { + goroutineID := GetGoroutineID() + goroutineStr = fmt.Sprintf("[g:%s] ", goroutineID) + } + + return fmt.Sprintf("%s%s%s%s%s[%s%s%s]%s %s\n", + l.prefix, timeStr, caller, goroutineStr, levelColor, levelNames[level], resetColor, fields, resetColor, msg) } // log logs a message at the specified level @@ -341,6 +378,16 @@ func Trace(format string, args ...interface{}) { DefaultLogger.Trace(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 { @@ -373,3 +420,19 @@ func WithFields(fields map[string]interface{}) *Logger { } 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() +} diff --git a/logger/panic_handler.go b/logger/panic_handler.go new file mode 100644 index 0000000..e393a31 --- /dev/null +++ b/logger/panic_handler.go @@ -0,0 +1,49 @@ +package logger + +import ( + "fmt" + "runtime/debug" +) + +// PanicHandler handles a panic and logs it +func PanicHandler() { + if r := recover(); r != nil { + goroutineID := GetGoroutineID() + stackTrace := debug.Stack() + Error("PANIC in goroutine %s: %v\n%s", goroutineID, r, stackTrace) + } +} + +// SafeGo launches a goroutine with panic recovery +// Usage: logger.SafeGo(func() { ... your code ... }) +func SafeGo(f func()) { + go func() { + defer PanicHandler() + f() + }() +} + +// SafeGoWithArgs launches a goroutine with panic recovery and passes arguments +// Usage: logger.SafeGoWithArgs(func(arg1, arg2 interface{}) { ... }, "value1", 42) +func SafeGoWithArgs(f func(...interface{}), args ...interface{}) { + go func() { + defer PanicHandler() + f(args...) + }() +} + +// SafeExec executes a function with panic recovery +// Useful for code that should not panic +func SafeExec(f func()) (err error) { + defer func() { + if r := recover(); r != nil { + goroutineID := GetGoroutineID() + stackTrace := debug.Stack() + Error("PANIC in goroutine %s: %v\n%s", goroutineID, r, stackTrace) + err = fmt.Errorf("panic recovered: %v", r) + } + }() + + f() + return nil +} diff --git a/main.go b/main.go index 3fed915..7f648bc 100644 --- a/main.go +++ b/main.go @@ -184,31 +184,32 @@ func main() { // Process each file for _, file := range files { wg.Add(1) - go func(file string) { + logger.SafeGoWithArgs(func(args ...interface{}) { defer wg.Done() - logger.Debug("Processing file: %s", file) + fileToProcess := args[0].(string) + logger.Debug("Processing file: %s", fileToProcess) // It's a bit fucked, maybe I could do better to call it from proc... But it'll do for now - modCount, matchCount, err := processor.Process(proc, file, pattern, luaExpr) + modCount, matchCount, err := processor.Process(proc, fileToProcess, pattern, luaExpr) if err != nil { - logger.Error("Failed to process file %s: %v", file, err) - fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err) + logger.Error("Failed to process file %s: %v", fileToProcess, err) + fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", fileToProcess, err) stats.FailedFiles++ } else { if modCount > 0 { logger.Info("Successfully processed file %s: %d modifications from %d matches", - file, modCount, matchCount) + fileToProcess, modCount, matchCount) } else if matchCount > 0 { logger.Info("Found %d matches in file %s but made no modifications", - matchCount, file) + matchCount, fileToProcess) } else { - logger.Debug("No matches found in file: %s", file) + logger.Debug("No matches found in file: %s", fileToProcess) } stats.ProcessedFiles++ stats.TotalMatches += matchCount stats.TotalModifications += modCount } - }(file) + }, file) } wg.Wait() diff --git a/regression/regression_test.go b/regression/regression_test.go index 842d8bc..d121432 100644 --- a/regression/regression_test.go +++ b/regression/regression_test.go @@ -77,6 +77,83 @@ func TestTalentsMechanicOutOfRange(t *testing.T) { t.Errorf("Expected 1 modification, got %d", mods) } + if result != actual { + t.Errorf("expected %s, got %s", actual, result) + } +} + +func TestIndexExplosions(t *testing.T) { + given := ` + + + + + + + + + + + + + + + + + + + + + + + + + + ` + + actual := ` + + + + + + + + + + + + + + + + + + + + + + + + + + ` + + p := &processor.RegexProcessor{} + result, mods, matches, err := p.ProcessContent(given, `!anyvalue="(?!num)"!anyvalue="(?!num)"!anyvalue="(?!num)"!anyamount="(?!num)"`, "movementspeed=round(movementspeed*1.5, 2) duration=round(duration*2, 2) repairspeed=round(repairspeed*2, 2) durationv=duration") + + if err != nil { + t.Fatalf("Error processing content: %v", err) + } + + if matches != 1 { + t.Errorf("Expected 1 match, got %d", matches) + } + + if mods != 1 { + t.Errorf("Expected 1 modification, got %d", mods) + } + if result != actual { t.Errorf("expected %s, got %s", actual, result) }