Files
ResolveSpec/pkg/logger/logger.go
T
Hein c7b4530689 fix(race): make make test-race pass across ./pkg/...
Add a test-race target and run test-unit over ./pkg/...

Production races:
- logger: guard Logger/errorTracker with an RWMutex
- security: copy UserContext for the async session-activity goroutine
  and track it with a WaitGroup

Test fixes:
- eventbroker, websocketspec: use atomics for state shared with workers
- security: wait for async activity updates before touching sqlmock
- mqttspec: build full HookContext, set SubscriptionID, pin the
  in-memory SQLite to one connection

Update the cross-cutting audit (X1) with status and findings.
2026-09-30 13:07:47 +02:00

248 lines
5.9 KiB
Go

package logger
import (
"context"
"fmt"
"log"
"os"
"runtime/debug"
"sync"
"go.uber.org/zap"
errortracking "github.com/bitechdev/ResolveSpec/pkg/errortracking"
)
// Logger is the active logger. It is kept exported for compatibility, but
// inside this package it must only be accessed through getLogger/setLogger.
var Logger *zap.SugaredLogger
var errorTracker errortracking.Provider
// stateMu guards Logger and errorTracker, which may be replaced while other
// goroutines are logging.
var stateMu sync.RWMutex
func getLogger() *zap.SugaredLogger {
stateMu.RLock()
defer stateMu.RUnlock()
return Logger
}
func setLogger(l *zap.SugaredLogger) {
stateMu.Lock()
defer stateMu.Unlock()
Logger = l
}
func getErrorTracker() errortracking.Provider {
stateMu.RLock()
defer stateMu.RUnlock()
return errorTracker
}
func Init(dev bool) {
if dev {
cfg := zap.NewDevelopmentConfig()
UpdateLogger(&cfg)
} else {
cfg := zap.NewProductionConfig()
UpdateLogger(&cfg)
}
}
func UpdateLoggerPath(path string, dev bool) {
defaultConfig := zap.NewProductionConfig()
if dev {
defaultConfig = zap.NewDevelopmentConfig()
}
defaultConfig.OutputPaths = []string{path}
UpdateLogger(&defaultConfig)
}
func UpdateLogger(config *zap.Config) {
defaultConfig := zap.NewProductionConfig()
defaultConfig.OutputPaths = []string{"resolvespec.log"}
if config == nil {
config = &defaultConfig
}
logger, err := config.Build()
if err != nil {
log.Print(err)
return
}
setLogger(logger.Sugar())
Info("ResolveSpec Logger initialized")
}
// InitErrorTracking initializes the error tracking provider
func InitErrorTracking(provider errortracking.Provider) {
stateMu.Lock()
errorTracker = provider
stateMu.Unlock()
if provider != nil {
Info("Error tracking initialized")
}
}
// GetErrorTracker returns the current error tracking provider
func GetErrorTracker() errortracking.Provider {
return getErrorTracker()
}
// CloseErrorTracking flushes and closes the error tracking provider
func CloseErrorTracking() error {
if tracker := getErrorTracker(); tracker != nil {
tracker.Flush(5)
return tracker.Close()
}
return nil
}
// extractContext attempts to find a context.Context in the given arguments.
// It returns the found context (or context.Background() if not found) and
// the remaining arguments without the context.
func extractContext(args ...interface{}) (ctx context.Context, filteredArgs []interface{}) {
ctx = context.Background()
var newArgs []interface{}
found := false
for _, arg := range args {
if c, ok := arg.(context.Context); ok {
if !found {
ctx = c
found = true
}
// Ignore any additional context.Context arguments after the first one.
continue
}
newArgs = append(newArgs, arg)
}
return ctx, newArgs
}
func Info(template string, args ...interface{}) {
lg := getLogger()
if lg == nil {
log.Printf(template, args...)
return
}
lg.Infow(fmt.Sprintf(template, args...), "process_id", os.Getpid())
}
func Warn(template string, args ...interface{}) {
lg := getLogger()
tracker := getErrorTracker()
ctx, remainingArgs := extractContext(args...)
message := fmt.Sprintf(template, remainingArgs...)
if lg == nil {
log.Printf("%s", message)
} else {
lg.Warnw(message, "process_id", os.Getpid())
}
// Send to error tracker
if tracker != nil {
tracker.CaptureMessage(ctx, message, errortracking.SeverityWarning, map[string]interface{}{
"process_id": os.Getpid(),
})
}
}
func Error(template string, args ...interface{}) {
lg := getLogger()
tracker := getErrorTracker()
ctx, remainingArgs := extractContext(args...)
message := fmt.Sprintf(template, remainingArgs...)
if lg == nil {
log.Printf("%s", message)
} else {
lg.Errorw(message, "process_id", os.Getpid())
}
// Send to error tracker
if tracker != nil {
tracker.CaptureMessage(ctx, message, errortracking.SeverityError, map[string]interface{}{
"process_id": os.Getpid(),
})
}
}
func Debug(template string, args ...interface{}) {
lg := getLogger()
if lg == nil {
log.Printf(template, args...)
return
}
lg.Debugw(fmt.Sprintf(template, args...), "process_id", os.Getpid())
}
// CatchPanic - Handle panic
// Returns a function that should be deferred to catch panics
// Example usage: defer CatchPanicCallback("MyFunction", func(err any) { /* cleanup */ })()
func CatchPanicCallback(location string, cb func(err any), args ...interface{}) func() {
ctx, _ := extractContext(args...)
return func() {
if err := recover(); err != nil {
callstack := debug.Stack()
lg := getLogger()
tracker := getErrorTracker()
if lg != nil {
Error("Panic in %s : %v", location, err, ctx) // Pass context implicitly
} else {
fmt.Printf("%s:PANIC->%+v", location, err)
debug.PrintStack()
}
// Send to error tracker
if tracker != nil {
tracker.CapturePanic(ctx, err, callstack, map[string]interface{}{
"location": location,
"process_id": os.Getpid(),
})
}
if cb != nil {
cb(err)
}
}
}
}
// CatchPanic - Handle panic
// Returns a function that should be deferred to catch panics
// Example usage: defer CatchPanic("MyFunction")()
func CatchPanic(location string, args ...interface{}) func() {
return CatchPanicCallback(location, nil, args...)
}
// HandlePanic logs a panic and returns it as an error
// This should be called with the result of recover() from a deferred function
// Example usage:
//
// defer func() {
// if r := recover(); r != nil {
// err = logger.HandlePanic("MethodName", r)
// }
// }()
func HandlePanic(methodName string, r any, args ...interface{}) error {
tracker := getErrorTracker()
ctx, _ := extractContext(args...)
stack := debug.Stack()
Error("Panic in %s: %v\nStack trace:\n%s", methodName, r, string(stack), ctx) // Pass context implicitly
// Send to error tracker
if tracker != nil {
tracker.CapturePanic(ctx, r, stack, map[string]interface{}{
"method": methodName,
"process_id": os.Getpid(),
})
}
return fmt.Errorf("panic in %s: %v", methodName, r)
}