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)
}