diff --git a/src/api.go b/src/api.go index ea6dbc9..27fc773 100644 --- a/src/api.go +++ b/src/api.go @@ -384,6 +384,7 @@ func initAPIs(targetMux *http.ServeMux) { authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog) authRouter.HandleFunc("/api/log/summary", LogViewer.HandleReadLogSummary) authRouter.HandleFunc("/api/log/errors", LogViewer.HandleLogErrorSummary) + authRouter.HandleFunc("/api/log/rotate/debug.trigger", SystemWideLogger.HandleDebugTriggerLogRotation) //Debug authRouter.HandleFunc("/api/info/pprof", pprof.Index) } diff --git a/src/def.go b/src/def.go index a2d6897..35b58eb 100644 --- a/src/def.go +++ b/src/def.go @@ -94,6 +94,11 @@ var ( allowWebFileManager = flag.Bool("webfm", true, "Enable web file manager for static web server root folder") enableAutoUpdate = flag.Bool("cfgupgrade", true, "Enable auto config upgrade if breaking change is detected") + /* Logging Configuration Flags */ + enableLog = flag.Bool("enablelog", true, "Enable system wide logging, set to false for writing log to STDOUT only") + enableLogCompression = flag.Bool("enablelogcompress", true, "Enable log compression for rotated log files") + logRotate = flag.Int("logrotate", 0, "Enable log rotation and set the maximum log file size in KB (e.g. 25 for 25KB), set to 0 for disable") + /* Default Configuration Flags */ defaultInboundPort = flag.Int("default_inbound_port", 443, "Default web server listening port") defaultEnableInboundTraffic = flag.Bool("default_inbound_enabled", true, "If web server is enabled by default") diff --git a/src/mod/info/logger/logger.go b/src/mod/info/logger/logger.go index 06b0e45..0474486 100644 --- a/src/mod/info/logger/logger.go +++ b/src/mod/info/logger/logger.go @@ -18,12 +18,15 @@ import ( */ type Logger struct { - Prefix string //Prefix for log files - LogFolder string //Folder to store the log file - CurrentLogFile string //Current writing filename - RotateOption RotateOption //Options for log rotation, see rotate.go - logger *log.Logger - file *os.File + Prefix string //Prefix for log files + LogFolder string //Folder to store the log file + CurrentLogFile string //Current writing filename + RotateOption *RotateOption //Options for log rotation, see rotate.go + + //Internal + logRotateTicker *time.Ticker + logger *log.Logger + file *os.File } // Create a new logger that log to files @@ -47,6 +50,17 @@ func NewLogger(logFilePrefix string, logFolder string) (*Logger, error) { thisLogger.CurrentLogFile = logFilePath thisLogger.file = f + //Initiate the log rotation ticker + thisLogger.logRotateTicker = time.NewTicker(1 * time.Hour) + go func() { + for range thisLogger.logRotateTicker.C { + err := thisLogger.RotateLog() + if err != nil { + log.Println("Log rotation error: ", err.Error()) + } + } + }() + //Start the logger logger := log.New(f, "", log.Flags()&^(log.Ldate|log.Ltime)) logger.SetFlags(0) @@ -66,6 +80,11 @@ func NewFmtLogger() (*Logger, error) { }, nil } +// SetRotateOption will set the log rotation option +func (l *Logger) SetRotateOption(option *RotateOption) { + l.RotateOption = option +} + func (l *Logger) getLogFilepath() string { year, month, _ := time.Now().Date() return filepath.Join(l.LogFolder, l.Prefix+"_"+strconv.Itoa(year)+"-"+strconv.Itoa(int(month))+".log") @@ -119,6 +138,12 @@ func (l *Logger) ValidateAndUpdateLogFilepath() { l.file.Close() l.file = nil + //Archive the old log file + err := l.ArchiveLog(l.CurrentLogFile) + if err != nil { + log.Println("Unable to archive old log file: ", err.Error()) + } + //Create a new log file f, err := os.OpenFile(expectedCurrentLogFilepath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755) if err != nil { @@ -136,5 +161,8 @@ func (l *Logger) ValidateAndUpdateLogFilepath() { } func (l *Logger) Close() { - l.file.Close() + if l.file != nil { + l.file.Close() + } + l.StopLogRotateTicker() } diff --git a/src/mod/info/logger/rotate.go b/src/mod/info/logger/rotate.go index c27edcb..0562dba 100644 --- a/src/mod/info/logger/rotate.go +++ b/src/mod/info/logger/rotate.go @@ -4,20 +4,31 @@ import ( "archive/zip" "fmt" "io" + "net/http" "os" "path/filepath" "sort" "time" + + "imuslab.com/zoraxy/mod/utils" ) type RotateOption struct { - Enabled bool //Whether log rotation is enabled + Enabled bool //Whether log rotation is enabled, default false MaxSize int64 //Maximum size of the log file in bytes before rotation (e.g. 10 * 1024 * 1024 for 10MB) MaxBackups int //Maximum number of backup files to keep Compress bool //Whether to compress the rotated files BackupDir string //Directory to store backup files, if empty, use the same directory as the log file } +// Stop the log rotation ticker +func (l *Logger) StopLogRotateTicker() { + if l.logRotateTicker != nil { + l.logRotateTicker.Stop() + } +} + +// Check if the log file needs rotation func (l *Logger) LogNeedRotate(filename string) bool { if !l.RotateOption.Enabled { return false @@ -29,19 +40,82 @@ func (l *Logger) LogNeedRotate(filename string) bool { return info.Size() >= l.RotateOption.MaxSize } +// Handle web request trigger log ratation +func (l *Logger) HandleDebugTriggerLogRotation(w http.ResponseWriter, r *http.Request) { + err := l.RotateLog() + if err != nil { + utils.SendErrorResponse(w, "Log rotation error: "+err.Error()) + return + } + l.PrintAndLog("logger", "Log rotation triggered via REST API", nil) + utils.SendOK(w) +} + +// ArchiveLog will archive the given log file, use during month change +func (l *Logger) ArchiveLog(filename string) error { + if l.RotateOption == nil || !l.RotateOption.Enabled { + return nil + } + + // Determine backup directory + backupDir := l.RotateOption.BackupDir + if backupDir == "" { + backupDir = filepath.Dir(filename) + } + + // Ensure backup directory exists + if err := os.MkdirAll(backupDir, 0755); err != nil { + return err + } + + // Generate archived filename with timestamp + timestamp := time.Now().Format("20060102-150405") + baseName := filepath.Base(filename) + baseName = baseName[:len(baseName)-len(filepath.Ext(baseName))] + archivedName := fmt.Sprintf("%s.%s.log", baseName, timestamp) + archivedPath := filepath.Join(backupDir, archivedName) + + // Rename current log file to archived file + if err := os.Rename(filename, archivedPath); err != nil { + return err + } + + // Optionally compress the archived file + if l.RotateOption.Compress { + if err := compressFile(archivedPath); err != nil { + return err + } + os.Remove(archivedPath) + } + + return nil +} + +// Execute log rotation func (l *Logger) RotateLog() error { - if !l.RotateOption.Enabled { + if l.RotateOption == nil || !l.RotateOption.Enabled { return nil } needRotate := l.LogNeedRotate(l.CurrentLogFile) + l.PrintAndLog("logger", fmt.Sprintf("Log rotation check: need rotate = %v", needRotate), nil) if !needRotate { return nil } - //Close current file + // Close current file with retry on failure if l.file != nil { - l.file.Close() + var closeErr error + for i := 0; i < 5; i++ { + closeErr = l.file.Close() + if closeErr == nil { + break + } + time.Sleep(1 * time.Second) + } + if closeErr != nil { + return closeErr + } } // Determine backup directory @@ -58,7 +132,8 @@ func (l *Logger) RotateLog() error { // Generate rotated filename with timestamp timestamp := time.Now().Format("20060102-150405") baseName := filepath.Base(l.CurrentLogFile) - rotatedName := fmt.Sprintf("%s.%s", baseName, timestamp) + baseName = baseName[:len(baseName)-len(filepath.Ext(baseName))] + rotatedName := fmt.Sprintf("%s.%s.log", baseName, timestamp) rotatedPath := filepath.Join(backupDir, rotatedName) // Rename current log file to rotated file @@ -95,7 +170,9 @@ func (l *Logger) RotateLog() error { return err } l.file = file - + if l.logger != nil { + l.logger.SetOutput(file) + } return nil } diff --git a/src/start.go b/src/start.go index b280871..e5e5c83 100644 --- a/src/start.go +++ b/src/start.go @@ -65,6 +65,27 @@ func startupSequence() { } else { panic(err) } + + if !*enableLog { + //Disable file logging, use fmt logger instead + l, err = logger.NewFmtLogger() + if err != nil { + panic(err) + } + SystemWideLogger = l + SystemWideLogger.Println("System wide logging is disabled, all logs will be printed to STDOUT only") + } else { + l.SetRotateOption(&logger.RotateOption{ + Enabled: *logRotate != 0, + MaxSize: int64(*logRotate) * 1024, //Convert to bytes + MaxBackups: 10, + Compress: *enableLogCompression, + BackupDir: "", + }) + SystemWideLogger = l + SystemWideLogger.Println("System wide logging is enabled") + } + LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{ RootFolder: *path_logFile, Extension: LOG_EXTENSION, @@ -72,9 +93,10 @@ func startupSequence() { //Create database backendType := database.GetRecommendedBackendType() - if *databaseBackend == "leveldb" { + switch *databaseBackend { + case "leveldb": backendType = dbinc.BackendLevelDB - } else if *databaseBackend == "boltdb" { + case "boltdb": backendType = dbinc.BackendBoltDB } l.PrintAndLog("database", "Using "+backendType.String()+" as the database backend", nil) diff --git a/src/web/snippet/logview.html b/src/web/snippet/logview.html index 17ffe1c..35a6beb 100644 --- a/src/web/snippet/logview.html +++ b/src/web/snippet/logview.html @@ -121,9 +121,9 @@ Summary - +
@@ -159,6 +159,11 @@ + + +


@@ -281,6 +286,16 @@ Pick a log file from the menu to start debugging } }); + /* Refresh Button */ + $("#refreshLogBtn").on("click", function() { + if (currentLogFile) { + openLog(null, null, currentLogFile, currentFilter); + loadLogSummary(currentLogFile); + } else { + alert('Please select a log file first.'); + } + }); + /* Log file dropdown */ function populateLogfileDropdown() { $.get("/api/log/list", function(data){ @@ -610,10 +625,7 @@ Pick a log file from the menu to start debugging function renderErrorHighlights(errors) { if (!Array.isArray(errors) || errors.length === 0) { $("#errorHighlightWrapper").html( - `
-
No errors found
-

This log file contains no errors.

-
` + `

No error found

` ); return; }