mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 20:02:23 +03:00
fix(logger): fix data race in InitLogger
Replace the package logger variable with an atomic.Pointer so InitLogger swapping the handle no longer races with concurrent Debug/Info/Warning/Error calls from other goroutines. Also guard fileRotate with a mutex, and add a regression test that reproduces the race under concurrent logging.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
@@ -30,10 +31,13 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// Initialized to a usable default so logging never nil-derefs before InitLogger
|
||||
// runs — the "migrate" and "setting" CLI subcommands log without calling it.
|
||||
logger = logging.MustGetLogger("x-ui")
|
||||
fileRotate *lumberjack.Logger // nil when file backend disabled
|
||||
// InitLogger swaps the handle while other goroutines are logging, so it is
|
||||
// published atomically — a plain assignment is an unsafe publication.
|
||||
logger atomic.Pointer[logging.Logger]
|
||||
|
||||
// fileRotateMu guards fileRotate against a concurrent InitLogger/CloseLogger.
|
||||
fileRotateMu sync.Mutex
|
||||
fileRotate *lumberjack.Logger // nil when file backend disabled
|
||||
|
||||
// logBuffer maintains recent log entries in memory for web UI retrieval;
|
||||
// logBufferMu guards it — written from many goroutines, read by the web UI.
|
||||
@@ -45,6 +49,12 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// A usable default so logging never nil-derefs before InitLogger runs — the
|
||||
// "migrate" and "setting" CLI subcommands log without calling it.
|
||||
func init() {
|
||||
logger.Store(logging.MustGetLogger("x-ui"))
|
||||
}
|
||||
|
||||
// InitLogger initializes dual logging backends: console/syslog and file.
|
||||
// Console logging uses the specified level, file logging always uses DEBUG level.
|
||||
func InitLogger(level logging.Level) {
|
||||
@@ -66,7 +76,7 @@ func InitLogger(level logging.Level) {
|
||||
|
||||
multiBackend := logging.MultiLogger(backends...)
|
||||
newLogger.SetBackend(multiBackend)
|
||||
logger = newLogger
|
||||
logger.Store(newLogger)
|
||||
}
|
||||
|
||||
// initDefaultBackend creates the console/syslog logging backend.
|
||||
@@ -104,7 +114,7 @@ func initFileBackend() logging.Backend {
|
||||
}
|
||||
|
||||
logPath := filepath.Join(logDir, logFileName)
|
||||
fileRotate = &lumberjack.Logger{
|
||||
rotate := &lumberjack.Logger{
|
||||
Filename: logPath,
|
||||
MaxSize: maxLogFileMB,
|
||||
MaxBackups: maxLogBackups,
|
||||
@@ -112,8 +122,11 @@ func initFileBackend() logging.Backend {
|
||||
LocalTime: true,
|
||||
Compress: compressRotated,
|
||||
}
|
||||
fileRotateMu.Lock()
|
||||
fileRotate = rotate
|
||||
fileRotateMu.Unlock()
|
||||
|
||||
backend := logging.NewLogBackend(fileRotate, "", 0)
|
||||
backend := logging.NewLogBackend(rotate, "", 0)
|
||||
return logging.NewBackendFormatter(backend, newFormatter(true))
|
||||
}
|
||||
|
||||
@@ -129,6 +142,8 @@ func newFormatter(withTime bool) logging.Formatter {
|
||||
// CloseLogger closes the rotating log writer and cleans up resources.
|
||||
// Should be called during application shutdown.
|
||||
func CloseLogger() {
|
||||
fileRotateMu.Lock()
|
||||
defer fileRotateMu.Unlock()
|
||||
if fileRotate != nil {
|
||||
_ = fileRotate.Close()
|
||||
fileRotate = nil
|
||||
@@ -137,61 +152,61 @@ func CloseLogger() {
|
||||
|
||||
// Debug logs a debug message and adds it to the log buffer.
|
||||
func Debug(args ...any) {
|
||||
logger.Debug(args...)
|
||||
logger.Load().Debug(args...)
|
||||
addToBuffer("DEBUG", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Debugf logs a formatted debug message and adds it to the log buffer.
|
||||
func Debugf(format string, args ...any) {
|
||||
logger.Debugf(format, args...)
|
||||
logger.Load().Debugf(format, args...)
|
||||
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Info logs an info message and adds it to the log buffer.
|
||||
func Info(args ...any) {
|
||||
logger.Info(args...)
|
||||
logger.Load().Info(args...)
|
||||
addToBuffer("INFO", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Infof logs a formatted info message and adds it to the log buffer.
|
||||
func Infof(format string, args ...any) {
|
||||
logger.Infof(format, args...)
|
||||
logger.Load().Infof(format, args...)
|
||||
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Notice logs a notice message and adds it to the log buffer.
|
||||
func Notice(args ...any) {
|
||||
logger.Notice(args...)
|
||||
logger.Load().Notice(args...)
|
||||
addToBuffer("NOTICE", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Noticef logs a formatted notice message and adds it to the log buffer.
|
||||
func Noticef(format string, args ...any) {
|
||||
logger.Noticef(format, args...)
|
||||
logger.Load().Noticef(format, args...)
|
||||
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Warning logs a warning message and adds it to the log buffer.
|
||||
func Warning(args ...any) {
|
||||
logger.Warning(args...)
|
||||
logger.Load().Warning(args...)
|
||||
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Warningf logs a formatted warning message and adds it to the log buffer.
|
||||
func Warningf(format string, args ...any) {
|
||||
logger.Warningf(format, args...)
|
||||
logger.Load().Warningf(format, args...)
|
||||
addToBuffer("WARNING", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Error logs an error message and adds it to the log buffer.
|
||||
func Error(args ...any) {
|
||||
logger.Error(args...)
|
||||
logger.Load().Error(args...)
|
||||
addToBuffer("ERROR", fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Errorf logs a formatted error message and adds it to the log buffer.
|
||||
func Errorf(format string, args ...any) {
|
||||
logger.Errorf(format, args...)
|
||||
logger.Load().Errorf(format, args...)
|
||||
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
golog "github.com/op/go-logging"
|
||||
)
|
||||
|
||||
// TestGetLogs_ReturnsAtMostC guards the documented "up to c entries" contract.
|
||||
@@ -28,3 +31,30 @@ func TestGetLogs_ReturnsAtMostC(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitLogger replaces the package logger while other goroutines are already
|
||||
// logging — CI caught that as a data race between InitLogger and Warningf.
|
||||
func TestInitLoggerConcurrentWithLogging(t *testing.T) {
|
||||
t.Setenv("XUI_LOG_FOLDER", t.TempDir())
|
||||
|
||||
stop := make(chan struct{})
|
||||
var logging sync.WaitGroup
|
||||
logging.Add(1)
|
||||
go func() {
|
||||
defer logging.Done()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
Warningf("concurrent %s", "log")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for range 10 {
|
||||
InitLogger(golog.CRITICAL)
|
||||
}
|
||||
close(stop)
|
||||
logging.Wait()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user