Files
Muse f887f47bb9 P7: restore agent node type, agent heartbeat expiry, build-version injection, remove log/history/xray-metrics modules, optional sub port
- nodes: port node-type (traditional/agent) picker back from web-src into
  the embedded frontend; backend annotates NodeView.nodeType from
  agent_nodes bindings; fix agent-node update path (skip reachability
  probe and apiToken requirement for agent-managed nodes); restore i18n
  keys in all 13 locales; install command now uses the Gitea mirror
- jobs: expire agent-bound nodes whose agent_nodes.last_seen is older
  than max(3x heartbeat interval, 120s) = 180s; last_seen=0 (never
  registered) is left untouched; transitions reuse UpdateHeartbeat +
  node event broadcast
- config: GetBaseVersion honors ldflags-injected buildVersion; release
  builds inject 3.7.0-agent.1 so the panel no longer reports a phantom
  update against the Gitea release tag
- logger/server: remove the three log viewers (panel/xray/amneziawg),
  their API endpoints and the 10240-entry in-memory log buffer; file
  logging via lumberjack unchanged; tests assert via a new optional
  logger hook instead of the removed buffer
- server: remove system history and xray metrics modules end to end
  (endpoints, sampling appends in RefreshStatus, per-minute persist
  cron, gob snapshot/restore, XrayMetricsService); realtime status and
  per-node history are untouched
- settings: subPort may be 0/unset - subscription links are generated
  without a port while the subscription service keeps binding the
  default 2096; the settings form declares the cleared state via
  onNumber's new onClear option instead of snapping back to 1
2026-10-21 06:44:00 +00:00

214 lines
6.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package logger provides logging functionality for the 3x-ui panel with
// dual-backend logging (console/syslog and file). The in-memory log buffer
// that fed the web UI's log viewer was removed together with that feature;
// logs live on disk (rotating file under the log folder) and in syslog.
package logger
import (
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"github.com/op/go-logging"
"github.com/mhsanaei/3x-ui/v3/internal/config"
"gopkg.in/natefinch/lumberjack.v2"
)
const (
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 (
// 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
// logHook, when set, additionally receives every log entry. Production
// never sets it; it exists so tests can assert on emitted logs now that
// the web UI's in-memory buffer is gone.
logHookMu sync.RWMutex
logHook func(level, msg string)
)
// SetLogHook installs a sink receiving every log entry (level name and
// formatted message) in addition to the normal backends, and returns the
// previously installed hook (nil in production). Passing nil clears it.
func SetLogHook(h func(level, msg string)) func(level, msg string) {
logHookMu.Lock()
defer logHookMu.Unlock()
prev := logHook
logHook = h
return prev
}
func emitLogHook(level, msg string) {
logHookMu.RLock()
h := logHook
logHookMu.RUnlock()
if h != nil {
h(level, msg)
}
}
// 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 = 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
includeTime := false
if runtime.GOOS == "windows" {
// Windows: Use stderr directly (no syslog support)
backend = logging.NewLogBackend(os.Stderr, "", 0)
includeTime = true
} else {
// 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
}
}
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
}
logPath := filepath.Join(logDir, logFileName)
fileRotate = &lumberjack.Logger{
Filename: logPath,
MaxSize: maxLogFileMB,
MaxBackups: maxLogBackups,
MaxAge: maxLogAgeDays,
LocalTime: true,
Compress: compressRotated,
}
backend := logging.NewLogBackend(fileRotate, "", 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 rotating log writer and cleans up resources.
// Should be called during application shutdown.
func CloseLogger() {
if fileRotate != nil {
_ = fileRotate.Close()
fileRotate = nil
}
}
// Debug logs a debug message and emits it to the log hook.
func Debug(args ...any) {
logger.Debug(args...)
emitLogHook("DEBUG", fmt.Sprint(args...))
}
// Debugf logs a formatted debug message and emits it to the log hook.
func Debugf(format string, args ...any) {
logger.Debugf(format, args...)
emitLogHook("DEBUG", fmt.Sprintf(format, args...))
}
// Info logs an info message and emits it to the log hook.
func Info(args ...any) {
logger.Info(args...)
emitLogHook("INFO", fmt.Sprint(args...))
}
// Infof logs a formatted info message and emits it to the log hook.
func Infof(format string, args ...any) {
logger.Infof(format, args...)
emitLogHook("INFO", fmt.Sprintf(format, args...))
}
// Notice logs a notice message and emits it to the log hook.
func Notice(args ...any) {
logger.Notice(args...)
emitLogHook("NOTICE", fmt.Sprint(args...))
}
// Noticef logs a formatted notice message and emits it to the log hook.
func Noticef(format string, args ...any) {
logger.Noticef(format, args...)
emitLogHook("NOTICE", fmt.Sprintf(format, args...))
}
// Warning logs a warning message and emits it to the log hook.
func Warning(args ...any) {
logger.Warning(args...)
emitLogHook("WARNING", fmt.Sprint(args...))
}
// Warningf logs a formatted warning message and emits it to the log hook.
func Warningf(format string, args ...any) {
logger.Warningf(format, args...)
emitLogHook("WARNING", fmt.Sprintf(format, args...))
}
// Error logs an error message and emits it to the log hook.
func Error(args ...any) {
logger.Error(args...)
emitLogHook("ERROR", fmt.Sprint(args...))
}
// Errorf logs a formatted error message and emits it to the log hook.
func Errorf(format string, args ...any) {
logger.Errorf(format, args...)
emitLogHook("ERROR", fmt.Sprintf(format, args...))
}