mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-30 05:36:36 +08:00
249b38e156
Every InitLogger call built a new lumberjack rotator and dropped the old one without closing it, leaking a handle on 3xui.log per call, and loggers still writing through an old rotator kept it alive. On Windows the open handles block deleting the file, so TestInitLoggerConcurrentWithLogging failed its t.TempDir cleanup there. InitLogger now reuses the open rotator for the same path and closes it only when the path changes, and the test closes the logger it opened. Neither half is enough alone: with only one of them the test stays red on Windows.
247 lines
7.7 KiB
Go
247 lines
7.7 KiB
Go
// Package logger provides logging functionality for the 3x-ui panel with
|
||
// dual-backend logging (console/syslog and file) and buffered log storage for web UI.
|
||
package logger
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/op/go-logging"
|
||
|
||
"github.com/mhsanaei/3x-ui/v3/internal/config"
|
||
|
||
"gopkg.in/natefinch/lumberjack.v2"
|
||
)
|
||
|
||
const (
|
||
maxLogBufferSize = 10240 // Maximum log entries kept in memory
|
||
logFileName = "3xui.log" // Log file name
|
||
timeFormat = "2006/01/02 15:04:05" // Log timestamp format
|
||
|
||
// On-disk rotation limits — single file capped, old segments pruned automatically.
|
||
maxLogFileMB = 10 // rotate active log when larger than this
|
||
maxLogBackups = 5 // rotated files retained (beyond current segment)
|
||
maxLogAgeDays = 7 // remove rotated backups older than this (0 disables time-based pruning)
|
||
compressRotated = true
|
||
)
|
||
|
||
var (
|
||
// 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.
|
||
logBufferMu sync.Mutex
|
||
logBuffer []struct {
|
||
time string
|
||
level logging.Level
|
||
log string
|
||
}
|
||
)
|
||
|
||
// 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) {
|
||
newLogger := logging.MustGetLogger("x-ui")
|
||
backends := make([]logging.Backend, 0, 2)
|
||
|
||
// Console/syslog backend with configurable level
|
||
consoleBackend := initDefaultBackend()
|
||
leveledBackend := logging.AddModuleLevel(consoleBackend)
|
||
leveledBackend.SetLevel(level, "x-ui")
|
||
backends = append(backends, leveledBackend)
|
||
|
||
// File backend with DEBUG level for comprehensive logging
|
||
if fileBackend := initFileBackend(); fileBackend != nil {
|
||
leveledBackend := logging.AddModuleLevel(fileBackend)
|
||
leveledBackend.SetLevel(logging.DEBUG, "x-ui")
|
||
backends = append(backends, leveledBackend)
|
||
}
|
||
|
||
multiBackend := logging.MultiLogger(backends...)
|
||
newLogger.SetBackend(multiBackend)
|
||
logger.Store(newLogger)
|
||
}
|
||
|
||
// initDefaultBackend creates the console logging backend: syslog where the platform has it, else stderr.
|
||
func initDefaultBackend() logging.Backend {
|
||
backend, includeTime := newConsoleBackend()
|
||
return logging.NewBackendFormatter(backend, newFormatter(includeTime))
|
||
}
|
||
|
||
// initFileBackend creates the file logging backend with size/age‑bounded rotation
|
||
// so log volume cannot grow without limit on disk.
|
||
func initFileBackend() logging.Backend {
|
||
logDir := config.GetLogFolder()
|
||
if err := os.MkdirAll(logDir, 0o750); err != nil {
|
||
fmt.Fprintf(os.Stderr, "failed to create log folder %s: %v\n", logDir, err)
|
||
return nil
|
||
}
|
||
|
||
backend := logging.NewLogBackend(fileRotateFor(filepath.Join(logDir, logFileName)), "", 0)
|
||
return logging.NewBackendFormatter(backend, newFormatter(true))
|
||
}
|
||
|
||
// fileRotateFor reuses the open rotator for logPath: a re-init that swapped in a
|
||
// new one would leave the old one holding the file, and loggers still writing to it.
|
||
func fileRotateFor(logPath string) *lumberjack.Logger {
|
||
fileRotateMu.Lock()
|
||
defer fileRotateMu.Unlock()
|
||
if fileRotate != nil && fileRotate.Filename == logPath {
|
||
return fileRotate
|
||
}
|
||
if fileRotate != nil {
|
||
_ = fileRotate.Close()
|
||
}
|
||
fileRotate = &lumberjack.Logger{
|
||
Filename: logPath,
|
||
MaxSize: maxLogFileMB,
|
||
MaxBackups: maxLogBackups,
|
||
MaxAge: maxLogAgeDays,
|
||
LocalTime: true,
|
||
Compress: compressRotated,
|
||
}
|
||
return fileRotate
|
||
}
|
||
|
||
// newFormatter creates a log formatter with optional timestamp.
|
||
func newFormatter(withTime bool) logging.Formatter {
|
||
format := `%{level} - %{message}`
|
||
if withTime {
|
||
format = `%{time:` + timeFormat + `} %{level} - %{message}`
|
||
}
|
||
return logging.MustStringFormatter(format)
|
||
}
|
||
|
||
// 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
|
||
}
|
||
}
|
||
|
||
// Debug logs a debug message and adds it to the log buffer.
|
||
func Debug(args ...any) {
|
||
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.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.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.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.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.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.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.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.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.Load().Errorf(format, args...)
|
||
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||
}
|
||
|
||
// addToBuffer adds a log entry to the in-memory ring buffer for web UI retrieval.
|
||
func addToBuffer(level string, newLog string) {
|
||
t := time.Now()
|
||
logBufferMu.Lock()
|
||
defer logBufferMu.Unlock()
|
||
if len(logBuffer) >= maxLogBufferSize {
|
||
logBuffer = logBuffer[1:]
|
||
}
|
||
|
||
logLevel, _ := logging.LogLevel(level)
|
||
logBuffer = append(logBuffer, struct {
|
||
time string
|
||
level logging.Level
|
||
log string
|
||
}{
|
||
time: t.Format(timeFormat),
|
||
level: logLevel,
|
||
log: newLog,
|
||
})
|
||
}
|
||
|
||
// GetLogs retrieves up to c log entries from the buffer that are at or below the specified level.
|
||
func GetLogs(c int, level string) []string {
|
||
var output []string
|
||
logLevel, _ := logging.LogLevel(level)
|
||
|
||
// Snapshot (copy) under the lock, then filter/format unlocked: a UI log fetch
|
||
// must not block addToBuffer — and thus all logging — for the formatting loop.
|
||
// A copy (not a reslice) is required, since addToBuffer can append in place.
|
||
logBufferMu.Lock()
|
||
snapshot := make([]struct {
|
||
time string
|
||
level logging.Level
|
||
log string
|
||
}, len(logBuffer))
|
||
copy(snapshot, logBuffer)
|
||
logBufferMu.Unlock()
|
||
|
||
for i := len(snapshot) - 1; i >= 0 && len(output) < c; i-- {
|
||
if snapshot[i].level <= logLevel {
|
||
output = append(output, fmt.Sprintf("%s %s - %s", snapshot[i].time, snapshot[i].level, snapshot[i].log))
|
||
}
|
||
}
|
||
return output
|
||
}
|