Sfoglia il codice sorgente

feat: add file logger support (#3575)

* feat: add backend for file logger
Slava M. 6 giorni fa
parent
commit
8afa39144e
2 ha cambiato i file con 100 aggiunte e 28 eliminazioni
  1. 99 27
      logger/logger.go
  2. 1 1
      sub/sub.go

+ 99 - 27
logger/logger.go

@@ -1,21 +1,29 @@
 // Package logger provides logging functionality for the 3x-ui panel with
-// buffered log storage and multiple log levels.
+// dual-backend logging (console/syslog and file) and buffered log storage for web UI.
 package logger
 
 import (
 	"fmt"
 	"os"
+	"path/filepath"
+	"runtime"
 	"time"
 
+	"github.com/mhsanaei/3x-ui/v2/config"
 	"github.com/op/go-logging"
 )
 
+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
+)
+
 var (
-	logger *logging.Logger
+	logger  *logging.Logger
+	logFile *os.File
 
-	// addToBuffer appends a log entry into the in-memory ring buffer used for
-	// retrieving recent logs via the web UI. It keeps the buffer bounded to avoid
-	// uncontrolled growth.
+	// logBuffer maintains recent log entries in memory for web UI retrieval
 	logBuffer []struct {
 		time  string
 		level logging.Level
@@ -23,35 +31,98 @@ var (
 	}
 )
 
-func init() {
-	InitLogger(logging.INFO)
-}
-
-// InitLogger initializes the logger with the specified logging level.
+// 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")
-	var err error
+	backends := make([]logging.Backend, 0, 2)
+
+	// Console/syslog backend with configurable level
+	if consoleBackend := initDefaultBackend(); consoleBackend != nil {
+		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 = newLogger
+}
+
+// initDefaultBackend creates the console/syslog logging backend.
+// Windows: Uses stderr directly (no syslog support)
+// Unix-like: Attempts syslog, falls back to stderr
+func initDefaultBackend() logging.Backend {
 	var backend logging.Backend
-	var format logging.Formatter
-	ppid := os.Getppid()
+	includeTime := false
 
-	backend, err = logging.NewSyslogBackend("")
-	if err != nil {
-		println(err)
+	if runtime.GOOS == "windows" {
+		// Windows: Use stderr directly (no syslog support)
 		backend = logging.NewLogBackend(os.Stderr, "", 0)
-	}
-	if ppid > 0 && err != nil {
-		format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
+		includeTime = true
 	} else {
-		format = logging.MustStringFormatter(`%{level} - %{message}`)
+		// Unix-like: Try syslog, fallback to stderr
+		if syslogBackend, err := logging.NewSyslogBackend(""); err != nil {
+			fmt.Fprintf(os.Stderr, "syslog backend disabled: %v\n", err)
+			backend = logging.NewLogBackend(os.Stderr, "", 0)
+			includeTime = os.Getppid() > 0
+		} else {
+			backend = syslogBackend
+		}
 	}
 
-	backendFormatter := logging.NewBackendFormatter(backend, format)
-	backendLeveled := logging.AddModuleLevel(backendFormatter)
-	backendLeveled.SetLevel(level, "x-ui")
-	newLogger.SetBackend(backendLeveled)
+	return logging.NewBackendFormatter(backend, newFormatter(includeTime))
+}
 
-	logger = newLogger
+// initFileBackend creates the file logging backend.
+// Creates log directory and truncates log file on startup for fresh logs.
+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
+	}
+
+	logPath := filepath.Join(logDir, logFileName)
+	file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "failed to open log file %s: %v\n", logPath, err)
+		return nil
+	}
+
+	// Close previous log file if exists
+	if logFile != nil {
+		_ = logFile.Close()
+	}
+	logFile = file
+
+	backend := logging.NewLogBackend(file, "", 0)
+	return logging.NewBackendFormatter(backend, newFormatter(true))
+}
+
+// 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 log file and cleans up resources.
+// Should be called during application shutdown.
+func CloseLogger() {
+	if logFile != nil {
+		_ = logFile.Close()
+		logFile = nil
+	}
 }
 
 // Debug logs a debug message and adds it to the log buffer.
@@ -114,9 +185,10 @@ func Errorf(format string, args ...any) {
 	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()
-	if len(logBuffer) >= 10240 {
+	if len(logBuffer) >= maxLogBufferSize {
 		logBuffer = logBuffer[1:]
 	}
 
@@ -126,7 +198,7 @@ func addToBuffer(level string, newLog string) {
 		level logging.Level
 		log   string
 	}{
-		time:  t.Format("2006/01/02 15:04:05"),
+		time:  t.Format(timeFormat),
 		level: logLevel,
 		log:   newLog,
 	})

+ 1 - 1
sub/sub.go

@@ -103,7 +103,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 	if basePath != "/" && !strings.HasSuffix(basePath, "/") {
 		basePath += "/"
 	}
-	logger.Debug("sub: Setting base_path to:", basePath)
+	// logger.Debug("sub: Setting base_path to:", basePath)
 	engine.Use(func(c *gin.Context) {
 		c.Set("base_path", basePath)
 	})