Add goroutine numbers to log lines

This commit is contained in:
2025-03-27 19:19:39 +01:00
parent e847e5c3ce
commit 66bcf21d79
4 changed files with 201 additions and 11 deletions

View File

@@ -1,6 +1,7 @@
package logger package logger
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"log" "log"
@@ -57,6 +58,7 @@ type Logger struct {
useColors bool useColors bool
callerOffset int callerOffset int
defaultFields map[string]interface{} defaultFields map[string]interface{}
showGoroutine bool
} }
var ( var (
@@ -104,6 +106,7 @@ func New(out io.Writer, prefix string, flag int) *Logger {
useColors: true, useColors: true,
callerOffset: 0, callerOffset: 0,
defaultFields: make(map[string]interface{}), defaultFields: make(map[string]interface{}),
showGoroutine: true,
} }
} }
@@ -139,6 +142,20 @@ func (l *Logger) SetCallerOffset(offset int) {
l.callerOffset = offset 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 // WithField adds a field to the logger's context
func (l *Logger) WithField(key string, value interface{}) *Logger { func (l *Logger) WithField(key string, value interface{}) *Logger {
newLogger := &Logger{ newLogger := &Logger{
@@ -149,6 +166,7 @@ func (l *Logger) WithField(key string, value interface{}) *Logger {
useColors: l.useColors, useColors: l.useColors,
callerOffset: l.callerOffset, callerOffset: l.callerOffset,
defaultFields: make(map[string]interface{}), defaultFields: make(map[string]interface{}),
showGoroutine: l.showGoroutine,
} }
// Copy existing fields // Copy existing fields
@@ -171,6 +189,7 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
useColors: l.useColors, useColors: l.useColors,
callerOffset: l.callerOffset, callerOffset: l.callerOffset,
defaultFields: make(map[string]interface{}), defaultFields: make(map[string]interface{}),
showGoroutine: l.showGoroutine,
} }
// Copy existing fields // Copy existing fields
@@ -185,6 +204,17 @@ func (l *Logger) WithFields(fields map[string]interface{}) *Logger {
return newLogger 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 // formatMessage formats a log message with level, time, file, and line information
func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{}) string { func (l *Logger) formatMessage(level LogLevel, format string, args ...interface{}) string {
var msg 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", // Add goroutine ID if enabled
l.prefix, timeStr, caller, levelColor, levelNames[level], resetColor, fields, resetColor, msg) 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 // log logs a message at the specified level
@@ -341,6 +378,16 @@ func Trace(format string, args ...interface{}) {
DefaultLogger.Trace(format, args...) 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 // SetLevel sets the log level for the default logger
func SetLevel(level LogLevel) { func SetLevel(level LogLevel) {
if DefaultLogger == nil { if DefaultLogger == nil {
@@ -373,3 +420,19 @@ func WithFields(fields map[string]interface{}) *Logger {
} }
return DefaultLogger.WithFields(fields) 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()
}

49
logger/panic_handler.go Normal file
View File

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

19
main.go
View File

@@ -184,31 +184,32 @@ func main() {
// Process each file // Process each file
for _, file := range files { for _, file := range files {
wg.Add(1) wg.Add(1)
go func(file string) { logger.SafeGoWithArgs(func(args ...interface{}) {
defer wg.Done() 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 // 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 { if err != nil {
logger.Error("Failed to process file %s: %v", file, err) logger.Error("Failed to process file %s: %v", fileToProcess, err)
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err) fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", fileToProcess, err)
stats.FailedFiles++ stats.FailedFiles++
} else { } else {
if modCount > 0 { if modCount > 0 {
logger.Info("Successfully processed file %s: %d modifications from %d matches", logger.Info("Successfully processed file %s: %d modifications from %d matches",
file, modCount, matchCount) fileToProcess, modCount, matchCount)
} else if matchCount > 0 { } else if matchCount > 0 {
logger.Info("Found %d matches in file %s but made no modifications", logger.Info("Found %d matches in file %s but made no modifications",
matchCount, file) matchCount, fileToProcess)
} else { } else {
logger.Debug("No matches found in file: %s", file) logger.Debug("No matches found in file: %s", fileToProcess)
} }
stats.ProcessedFiles++ stats.ProcessedFiles++
stats.TotalMatches += matchCount stats.TotalMatches += matchCount
stats.TotalModifications += modCount stats.TotalModifications += modCount
} }
}(file) }, file)
} }
wg.Wait() wg.Wait()

View File

@@ -81,3 +81,80 @@ func TestTalentsMechanicOutOfRange(t *testing.T) {
t.Errorf("expected %s, got %s", actual, result) t.Errorf("expected %s, got %s", actual, result)
} }
} }
func TestIndexExplosions(t *testing.T) {
given := `<Talent identifier="quickfixer">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
<Description tag="talentdescription.quickfixer">
<Replace tag="[amount]" value="20" color="gui.green"/>
<Replace tag="[duration]" value="10" color="gui.green"/>
</Description>
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="1"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<Conditions>
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
</Conditions>
<Abilities>
<CharacterAbilityApplyStatusEffects>
<StatusEffects>
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
<Affliction identifier="quickfixer" amount="10.0"/>
</StatusEffect>
</StatusEffects>
</CharacterAbilityApplyStatusEffects>
</Abilities>
</AbilityGroupEffect>
</Talent>`
actual := `<Talent identifier="quickfixer">
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
<Description tag="talentdescription.quickfixer">
<Replace tag="[amount]" value="30" color="gui.green"/>
<Replace tag="[duration]" value="20" color="gui.green"/>
</Description>
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
<AbilityGroupEffect abilityeffecttype="None">
<Abilities>
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="2"/>
</Abilities>
</AbilityGroupEffect>
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
<Conditions>
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
</Conditions>
<Abilities>
<CharacterAbilityApplyStatusEffects>
<StatusEffects>
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
<Affliction identifier="quickfixer" amount="20"/>
</StatusEffect>
</StatusEffects>
</CharacterAbilityApplyStatusEffects>
</Abilities>
</AbilityGroupEffect>
</Talent>`
p := &processor.RegexProcessor{}
result, mods, matches, err := p.ProcessContent(given, `<Talent identifier="quickfixer">!anyvalue="(?<movementspeed>!num)"!anyvalue="(?<duration>!num)"!anyvalue="(?<repairspeed>!num)"!anyamount="(?<durationv>!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)
}
}