mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-11-02 06:54:07 +01:00
Updated log rotate and viewer design
- Moved log rotation flags to in-system settings - New log tab for log related configs in util page - Added line number limit in log view - Fixed bug for using zip for .gz log compression - Fixed panic for loading legacy log formats
This commit is contained in:
@@ -386,7 +386,9 @@ 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)
|
||||
authRouter.HandleFunc("/api/log/rotate/trigger", SystemWideLogger.HandleDebugTriggerLogRotation)
|
||||
authRouter.HandleFunc("/api/logger/config", handleLoggerConfig)
|
||||
|
||||
//Debug
|
||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy"
|
||||
"imuslab.com/zoraxy/mod/dynamicproxy/loadbalance"
|
||||
"imuslab.com/zoraxy/mod/info/logger"
|
||||
"imuslab.com/zoraxy/mod/tlscert"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
@@ -366,3 +367,13 @@ func ImportConfigFromZip(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handleLoggerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
logger.HandleGetLogConfig(CONF_LOG_CONFIG)(w, r)
|
||||
} else if r.Method == http.MethodPost {
|
||||
logger.HandleUpdateLogConfig(CONF_LOG_CONFIG, SystemWideLogger)(w, r)
|
||||
} else {
|
||||
utils.SendErrorResponse(w, "Method not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ const (
|
||||
CONF_PATH_RULE = CONF_FOLDER + "/rules/pathrules"
|
||||
CONF_PLUGIN_GROUPS = CONF_FOLDER + "/plugin_groups.json"
|
||||
CONF_GEODB_PATH = CONF_FOLDER + "/geodb"
|
||||
CONF_LOG_CONFIG = CONF_FOLDER + "/log_conf.json"
|
||||
)
|
||||
|
||||
/* System Startup Flags */
|
||||
@@ -95,9 +96,9 @@ var (
|
||||
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.String("logrotate", "0", "Enable log rotation and set the maximum log file size in KB, also support K, M, G suffix (e.g. 200M), set to 0 to disable")
|
||||
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.String("logrotate", "0", "Enable log rotation and set the maximum log file size in KB, also support K, M, G suffix (e.g. 200M), set to 0 to disable")
|
||||
|
||||
/* Default Configuration Flags */
|
||||
defaultInboundPort = flag.Int("default_inbound_port", 443, "Default web server listening port")
|
||||
|
||||
164
src/mod/info/logger/handle.go
Normal file
164
src/mod/info/logger/handle.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
)
|
||||
|
||||
// LogConfig represents the log rotation configuration
|
||||
type LogConfig struct {
|
||||
Enabled bool `json:"enabled"` // Whether log rotation is enabled
|
||||
MaxSize string `json:"maxSize"` // Maximum size as string (e.g., "200M", "10K")
|
||||
MaxBackups int `json:"maxBackups"` // Maximum number of backup files to keep
|
||||
Compress bool `json:"compress"` // Whether to compress rotated logs
|
||||
}
|
||||
|
||||
// LoadLogConfig loads the log configuration from the config file
|
||||
func LoadLogConfig(configPath string) (*LogConfig, error) {
|
||||
// Ensure config directory exists
|
||||
configDir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Default config
|
||||
defaultConfig := &LogConfig{
|
||||
Enabled: false,
|
||||
MaxSize: "0",
|
||||
MaxBackups: 16,
|
||||
Compress: true,
|
||||
}
|
||||
|
||||
// Try to read existing config
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// File doesn't exist, save default config
|
||||
if saveErr := SaveLogConfig(configPath, defaultConfig); saveErr != nil {
|
||||
return nil, saveErr
|
||||
}
|
||||
return defaultConfig, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var config LogConfig
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&config); err != nil {
|
||||
// If decode fails, use default
|
||||
return defaultConfig, nil
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SaveLogConfig saves the log configuration to the config file
|
||||
func SaveLogConfig(configPath string, config *LogConfig) error {
|
||||
// Ensure config directory exists
|
||||
configDir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Create(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(config)
|
||||
}
|
||||
|
||||
// ApplyLogConfig applies the log configuration to the logger
|
||||
func (l *Logger) ApplyLogConfig(config *LogConfig) error {
|
||||
maxSizeBytes, err := utils.SizeStringToBytes(config.MaxSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if maxSizeBytes == 0 {
|
||||
// Use default value of 25MB
|
||||
maxSizeBytes = 25 * 1024 * 1024
|
||||
}
|
||||
|
||||
rotateOption := &RotateOption{
|
||||
Enabled: config.Enabled,
|
||||
MaxSize: int64(maxSizeBytes),
|
||||
MaxBackups: config.MaxBackups,
|
||||
Compress: config.Compress,
|
||||
BackupDir: "",
|
||||
}
|
||||
|
||||
l.SetRotateOption(rotateOption)
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleGetLogConfig handles GET /api/logger/config
|
||||
func HandleGetLogConfig(configPath string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := LoadLogConfig(configPath)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Failed to load log config: "+err.Error())
|
||||
return
|
||||
}
|
||||
js, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, "Failed to marshal config: "+err.Error())
|
||||
return
|
||||
}
|
||||
utils.SendJSONResponse(w, string(js))
|
||||
}
|
||||
}
|
||||
|
||||
// HandleUpdateLogConfig handles POST /api/logger/config
|
||||
func HandleUpdateLogConfig(configPath string, logger *Logger) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
utils.SendErrorResponse(w, "Method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
var config LogConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate MaxSize
|
||||
if _, err := utils.SizeStringToBytes(config.MaxSize); err != nil {
|
||||
utils.SendErrorResponse(w, "Invalid maxSize: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate MaxBackups
|
||||
if config.MaxBackups < 1 {
|
||||
utils.SendErrorResponse(w, "maxBackups must be at least 1")
|
||||
return
|
||||
}
|
||||
|
||||
// Save config
|
||||
if err := SaveLogConfig(configPath, &config); err != nil {
|
||||
utils.SendErrorResponse(w, "Failed to save config: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Apply to logger
|
||||
if err := logger.ApplyLogConfig(&config); err != nil {
|
||||
utils.SendErrorResponse(w, "Failed to apply config: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Pretty print config as key: value pairs
|
||||
configStr := fmt.Sprintf("enabled=%t, maxSize=%s, maxBackups=%d, compress=%t", config.Enabled, config.MaxSize, config.MaxBackups, config.Compress)
|
||||
logger.PrintAndLog("logger", "Updated log rotation setting: "+configStr, nil)
|
||||
utils.SendOK(w)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -176,17 +176,17 @@ func (l *Logger) RotateLog() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// compressFile compresses the given file using zip format and creates a .gz file.
|
||||
// compressFile compresses the given file using gzip format and creates a .gz file.
|
||||
func compressFile(filename string) error {
|
||||
zipFilename := filename + ".gz"
|
||||
outFile, err := os.Create(zipFilename)
|
||||
gzipFilename := filename + ".gz"
|
||||
outFile, err := os.Create(gzipFilename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
zipWriter := zip.NewWriter(outFile)
|
||||
defer zipWriter.Close()
|
||||
gzipWriter := gzip.NewWriter(outFile)
|
||||
defer gzipWriter.Close()
|
||||
|
||||
fileToCompress, err := os.Open(filename)
|
||||
if err != nil {
|
||||
@@ -194,11 +194,6 @@ func compressFile(filename string) error {
|
||||
}
|
||||
defer fileToCompress.Close()
|
||||
|
||||
w, err := zipWriter.Create(filepath.Base(filename))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(w, fileToCompress)
|
||||
_, err = io.Copy(gzipWriter, fileToCompress)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package logviewer
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -15,7 +18,6 @@ import (
|
||||
|
||||
type ViewerOption struct {
|
||||
RootFolder string //The root folder to scan for log
|
||||
Extension string //The extension the root files use, include the . in your ext (e.g. .log)
|
||||
}
|
||||
|
||||
type Viewer struct {
|
||||
@@ -72,6 +74,11 @@ func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
|
||||
filter = ""
|
||||
}
|
||||
|
||||
linesParam, err := utils.GetPara(r, "lines")
|
||||
if err != nil {
|
||||
linesParam = "all"
|
||||
}
|
||||
|
||||
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(filename)))
|
||||
if err != nil {
|
||||
utils.SendErrorResponse(w, err.Error())
|
||||
@@ -107,6 +114,18 @@ func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
|
||||
content = strings.Join(filteredLines, "\n")
|
||||
}
|
||||
|
||||
// Apply lines limit after filtering
|
||||
if linesParam != "all" {
|
||||
if lineLimit, err := strconv.Atoi(linesParam); err == nil && lineLimit > 0 {
|
||||
allLines := strings.Split(content, "\n")
|
||||
if len(allLines) > lineLimit {
|
||||
// Keep only the last lineLimit lines
|
||||
allLines = allLines[len(allLines)-lineLimit:]
|
||||
content = strings.Join(allLines, "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.SendTextResponse(w, content)
|
||||
}
|
||||
|
||||
@@ -158,7 +177,7 @@ func (v *Viewer) HandleLogErrorSummary(w http.ResponseWriter, r *http.Request) {
|
||||
line = line[strings.LastIndex(line, "]")+1:]
|
||||
fields := strings.Fields(strings.TrimSpace(line))
|
||||
|
||||
if len(fields) > 0 {
|
||||
if len(fields) >= 3 {
|
||||
statusStr := fields[2]
|
||||
if len(statusStr) == 3 && (statusStr[0] != '1' && statusStr[0] != '2' && statusStr[0] != '3') {
|
||||
fieldsWithTimestamp := append([]string{timestamp}, strings.Fields(strings.TrimSpace(line))...)
|
||||
@@ -179,7 +198,7 @@ func (v *Viewer) HandleLogErrorSummary(w http.ResponseWriter, r *http.Request) {
|
||||
func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
|
||||
result := map[string][]*LogFile{}
|
||||
filepath.WalkDir(v.option.RootFolder, func(path string, di fs.DirEntry, err error) error {
|
||||
if filepath.Ext(path) == v.option.Extension {
|
||||
if filepath.Ext(path) == ".log" || strings.HasSuffix(path, ".log.gz") {
|
||||
catergory := filepath.Base(filepath.Dir(path))
|
||||
logList, ok := result[catergory]
|
||||
if !ok {
|
||||
@@ -197,9 +216,12 @@ func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
|
||||
return nil
|
||||
}
|
||||
|
||||
filename := filepath.Base(path)
|
||||
filename = strings.TrimSuffix(filename, ".log") //to handle cases where the filename ends of .log.gz
|
||||
|
||||
logList = append(logList, &LogFile{
|
||||
Title: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)),
|
||||
Filename: filepath.Base(path),
|
||||
Filename: filename,
|
||||
Fullpath: fullpath,
|
||||
Filesize: st.Size(),
|
||||
})
|
||||
@@ -212,13 +234,78 @@ func (v *Viewer) ListLogFiles(showFullpath bool) map[string][]*LogFile {
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *Viewer) LoadLogFile(filename string) (string, error) {
|
||||
// readLogFileContent reads a log file, handling both compressed (.gz) and uncompressed files
|
||||
func (v *Viewer) readLogFileContent(filepath string) ([]byte, error) {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Check if file is compressed
|
||||
if strings.HasSuffix(filepath, ".gz") {
|
||||
gzipReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
// Try zip reader for older logs that use zip compression despite .gz extension
|
||||
zipReader, err := zip.OpenReader(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer zipReader.Close()
|
||||
if len(zipReader.File) == 0 {
|
||||
return nil, errors.New("zip file is empty")
|
||||
}
|
||||
zipFile := zipReader.File[0]
|
||||
rc, err := zipFile.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rc.Close()
|
||||
return io.ReadAll(rc)
|
||||
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
|
||||
return io.ReadAll(gzipReader)
|
||||
}
|
||||
|
||||
// Regular file
|
||||
return io.ReadAll(file)
|
||||
}
|
||||
|
||||
func (v *Viewer) senatizeLogFilenameInput(filename string) string {
|
||||
filename = strings.TrimSuffix(filename, ".log.gz")
|
||||
filename = strings.TrimSuffix(filename, ".log")
|
||||
filename = filepath.ToSlash(filename)
|
||||
filename = strings.ReplaceAll(filename, "../", "")
|
||||
logFilepath := filepath.Join(v.option.RootFolder, filename)
|
||||
//Check if .log.gz or .log exists
|
||||
if utils.FileExists(filepath.Join(v.option.RootFolder, filename+".log")) {
|
||||
return filepath.Join(v.option.RootFolder, filename+".log")
|
||||
}
|
||||
if utils.FileExists(filepath.Join(v.option.RootFolder, filename+".log.gz")) {
|
||||
return filepath.Join(v.option.RootFolder, filename+".log.gz")
|
||||
}
|
||||
return filepath.Join(v.option.RootFolder, filename)
|
||||
}
|
||||
|
||||
func (v *Viewer) LoadLogFile(filename string) (string, error) {
|
||||
// filename might be in (no extension), .log or .log.gz format
|
||||
// so we trim those first before proceeding
|
||||
logFilepath := v.senatizeLogFilenameInput(filename)
|
||||
if utils.FileExists(logFilepath) {
|
||||
//Load it
|
||||
content, err := os.ReadFile(logFilepath)
|
||||
content, err := v.readLogFileContent(logFilepath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
//Also check .log.gz
|
||||
logFilepathGz := logFilepath + ".gz"
|
||||
if utils.FileExists(logFilepathGz) {
|
||||
content, err := v.readLogFileContent(logFilepathGz)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -230,12 +317,10 @@ func (v *Viewer) LoadLogFile(filename string) (string, error) {
|
||||
}
|
||||
|
||||
func (v *Viewer) LoadLogSummary(filename string) (string, error) {
|
||||
filename = filepath.ToSlash(filename)
|
||||
filename = strings.ReplaceAll(filename, "../", "")
|
||||
logFilepath := filepath.Join(v.option.RootFolder, filename)
|
||||
logFilepath := v.senatizeLogFilenameInput(filename)
|
||||
if utils.FileExists(logFilepath) {
|
||||
//Load it
|
||||
content, err := os.ReadFile(logFilepath)
|
||||
content, err := v.readLogFileContent(logFilepath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
30
src/start.go
30
src/start.go
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
"imuslab.com/zoraxy/mod/auth/sso/oauth2"
|
||||
"imuslab.com/zoraxy/mod/eventsystem"
|
||||
"imuslab.com/zoraxy/mod/utils"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"imuslab.com/zoraxy/mod/access"
|
||||
@@ -77,30 +76,33 @@ func startupSequence() {
|
||||
SystemWideLogger = l
|
||||
SystemWideLogger.Println("System wide logging is disabled, all logs will be printed to STDOUT only")
|
||||
} else {
|
||||
logRotateSize, err := utils.SizeStringToBytes(*logRotate)
|
||||
// Load log configuration from file
|
||||
logConfig, err := logger.LoadLogConfig(CONF_LOG_CONFIG)
|
||||
if err != nil {
|
||||
//Default disable
|
||||
logRotateSize = 0
|
||||
SystemWideLogger.Println("Failed to load log config, using defaults: " + err.Error())
|
||||
logConfig = &logger.LogConfig{
|
||||
Enabled: false,
|
||||
MaxSize: "0",
|
||||
Compress: true,
|
||||
}
|
||||
}
|
||||
l.SetRotateOption(&logger.RotateOption{
|
||||
Enabled: logRotateSize != 0,
|
||||
MaxSize: int64(logRotateSize),
|
||||
MaxBackups: 10,
|
||||
Compress: *enableLogCompression,
|
||||
BackupDir: "",
|
||||
})
|
||||
|
||||
// Apply the configuration
|
||||
if err := l.ApplyLogConfig(logConfig); err != nil {
|
||||
SystemWideLogger.Println("Failed to apply log config: " + err.Error())
|
||||
}
|
||||
|
||||
SystemWideLogger = l
|
||||
if logRotateSize == 0 {
|
||||
if !logConfig.Enabled {
|
||||
SystemWideLogger.Println("Log rotation is disabled")
|
||||
} else {
|
||||
SystemWideLogger.Println("Log rotation is enabled, max log file size " + utils.BytesToHumanReadable(int64(logRotateSize)))
|
||||
SystemWideLogger.Println("Log rotation is enabled, max log file size " + logConfig.MaxSize)
|
||||
}
|
||||
SystemWideLogger.Println("System wide logging is enabled")
|
||||
}
|
||||
|
||||
LogViewer = logviewer.NewLogViewer(&logviewer.ViewerOption{
|
||||
RootFolder: *path_logFile,
|
||||
Extension: LOG_EXTENSION,
|
||||
})
|
||||
|
||||
//Create database
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
</div>
|
||||
<div class="ui top attached tabular menu">
|
||||
<a class="utils item active" data-tab="utiltab1"><i class="ui user circle blue icon"></i> Accounts</a>
|
||||
<a class="utils item" data-tab="utiltab2">Toolbox</a>
|
||||
<a class="utils item" data-tab="utiltab3">System</a>
|
||||
<a class="utils item" data-tab="utiltab2"><i class="ui grey file alternate outline icon"></i> Logger</a>
|
||||
<a class="utils item" data-tab="utiltab3">Toolbox</a>
|
||||
<a class="utils item" data-tab="utiltab4">System</a>
|
||||
</div>
|
||||
|
||||
<div class="ui bottom attached tab segment utilitiesTabs active" data-tab="utiltab1">
|
||||
@@ -88,6 +89,60 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment utilitiesTabs" data-tab="utiltab2">
|
||||
<!-- Log Settings -->
|
||||
<h3>Log Settings</h3>
|
||||
<p>Configure log rotation settings for the system logger</p>
|
||||
<div class="ui basic segment">
|
||||
<form id="log-settings-form" class="ui form">
|
||||
<div class="field">
|
||||
<div class="ui toggle checkbox">
|
||||
<input type="checkbox" id="logRotationEnabled" name="enabled">
|
||||
<label>Enable Log Rotation</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Maximum Log File Size</label>
|
||||
<div class="ui right labeled input">
|
||||
<input type="text" id="logMaxSize" name="maxSize" placeholder="e.g. 200M, 10K, 500" value="0">
|
||||
<div class="ui basic label">bytes</div>
|
||||
</div>
|
||||
<small>Enter size with suffix (K, M, G) or plain number. Set to 0 to disable.</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Maximum Backup Files</label>
|
||||
<input type="number" id="logMaxBackups" name="maxBackups" placeholder="10" min="1" value="16">
|
||||
<small>Maximum number of rotated log files to keep.</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" id="logCompressionEnabled" name="compress">
|
||||
<label>Enable Log Compression</label>
|
||||
</div>
|
||||
<small>When enabled, rotated log files will be compressed using ZIP format.</small>
|
||||
</div>
|
||||
<button class="ui basic button" type="submit">
|
||||
<i class="green save icon"></i> Save Settings
|
||||
</button>
|
||||
<button class="ui basic button" id="rotateLogBtn" onclick="triggerLogRotation(event)">
|
||||
<i class="yellow archive icon" ></i> Rotate Log Now
|
||||
</button>
|
||||
<div id="logSettingsSuccessMsg" class="ui green message" style="display:none;">
|
||||
<i class="checkmark icon"></i> Log settings updated successfully
|
||||
</div>
|
||||
<div id="logSettingsErrorMsg" class="ui red message" style="display:none;">
|
||||
<i class="exclamation triangle icon"></i> <span id="logSettingsErrorText"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<!-- Log Viewer -->
|
||||
<h3>System Log Viewer</h3>
|
||||
<p>View and download Zoraxy log</p>
|
||||
<a class="ui basic button" href="snippet/logview.html" target="_blank"><i class="ui blue file icon"></i> Open Log Viewer</a>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment utilitiesTabs" data-tab="utiltab3">
|
||||
<h3> IP Address to CIDR</h3>
|
||||
<p>No experience with CIDR notations? Here are some tools you can use to make setting up easier.</p>
|
||||
<div class="ui basic segment">
|
||||
@@ -116,17 +171,13 @@
|
||||
</div>
|
||||
<div class="ui divider"></div>
|
||||
</div>
|
||||
<div class="ui bottom attached tab segment utilitiesTabs" data-tab="utiltab3">
|
||||
<div class="ui bottom attached tab segment utilitiesTabs" data-tab="utiltab4">
|
||||
<!-- Config Tools -->
|
||||
<h3>System Backup & Restore</h3>
|
||||
<p>Options related to system backup, migrate and restore.</p>
|
||||
<button class="ui basic button" onclick="showSideWrapper('snippet/configTools.html');"><i class="ui green undo icon icon"></i> Open Config Tools</button>
|
||||
<div class="ui divider"></div>
|
||||
<!-- Log Viewer -->
|
||||
<h3>System Log Viewer</h3>
|
||||
<p>View and download Zoraxy log</p>
|
||||
<a class="ui basic button" href="snippet/logview.html" target="_blank"><i class="ui blue file icon"></i> Open Log Viewer</a>
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<!-- System Information -->
|
||||
<div id="zoraxyinfo">
|
||||
<h3 class="ui header">
|
||||
@@ -307,6 +358,7 @@
|
||||
});
|
||||
}
|
||||
initSMTPSettings();
|
||||
loadLogSettings();
|
||||
|
||||
function sendTestEmail(btn){
|
||||
$(btn).addClass("loading").addClass("disabled");
|
||||
@@ -472,4 +524,104 @@
|
||||
}
|
||||
return [ip >>> 24 & 0xFF, ip >>> 16 & 0xFF, ip >>> 8 & 0xFF, ip & 0xFF].join('.')
|
||||
}
|
||||
|
||||
/*
|
||||
Log Settings
|
||||
*/
|
||||
function loadLogSettings() {
|
||||
$.get("/api/logger/config", function(data) {
|
||||
if (data.error) {
|
||||
console.error("Failed to load log settings:", data.error);
|
||||
return;
|
||||
}
|
||||
$("#logRotationEnabled").prop("checked", data.enabled);
|
||||
$("#logMaxSize").val(data.maxSize);
|
||||
$("#logMaxBackups").val(data.maxBackups || 16);
|
||||
$("#logCompressionEnabled").prop("checked", data.compress);
|
||||
// Re-initialize checkboxes after setting values
|
||||
$('.ui.checkbox').checkbox();
|
||||
}).fail(function(xhr, status, error) {
|
||||
console.error("Failed to load log settings:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function saveLogSettings() {
|
||||
const settings = {
|
||||
enabled: $("#logRotationEnabled").is(":checked"),
|
||||
maxSize: $("#logMaxSize").val().trim(),
|
||||
maxBackups: parseInt($("#logMaxBackups").val()) || 16,
|
||||
compress: $("#logCompressionEnabled").is(":checked")
|
||||
};
|
||||
|
||||
// Basic validation
|
||||
if (settings.maxSize === "") {
|
||||
showLogSettingsError("Max size cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (settings.maxBackups < 1) {
|
||||
showLogSettingsError("Max backups must be at least 1");
|
||||
return;
|
||||
}
|
||||
|
||||
$.cjax({
|
||||
type: "POST",
|
||||
url: "/api/logger/config",
|
||||
data: JSON.stringify(settings),
|
||||
success: function(data) {
|
||||
if (data.error) {
|
||||
showLogSettingsError(data.error);
|
||||
} else {
|
||||
showLogSettingsSuccess();
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
showLogSettingsError("Failed to save settings: " + error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showLogSettingsSuccess() {
|
||||
$("#logSettingsErrorMsg").hide();
|
||||
$("#logSettingsSuccessMsg").stop().finish().slideDown("fast").delay(3000).slideUp("fast");
|
||||
}
|
||||
|
||||
function showLogSettingsError(message) {
|
||||
$("#logSettingsSuccessMsg").hide();
|
||||
$("#logSettingsErrorText").text(message);
|
||||
$("#logSettingsErrorMsg").stop().finish().slideDown("fast").delay(5000).slideUp("fast");
|
||||
}
|
||||
|
||||
function triggerLogRotation(event) {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
// Show loading state on button
|
||||
const rotateBtn = $('#rotateLogBtn');
|
||||
const originalText = rotateBtn.html();
|
||||
rotateBtn.html('<i class="spinner loading icon"></i> Rotating...').addClass('loading disabled');
|
||||
|
||||
$.get("/api/log/rotate/trigger", function(data) {
|
||||
if (data.error) {
|
||||
showLogSettingsError("Failed to rotate log: " + data.error);
|
||||
} else {
|
||||
showLogSettingsSuccess();
|
||||
// Update success message for rotation
|
||||
$("#logSettingsSuccessMsg").html('<i class="archive icon"></i> Log rotation completed successfully');
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
showLogSettingsError("Failed to rotate log: " + error);
|
||||
}).always(function() {
|
||||
// Restore button state
|
||||
rotateBtn.html(originalText).removeClass('loading disabled');
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize Semantic UI checkboxes
|
||||
$('.ui.checkbox').checkbox();
|
||||
|
||||
// Bind form submission
|
||||
$("#log-settings-form").submit(function(e) {
|
||||
e.preventDefault();
|
||||
saveLogSettings();
|
||||
});
|
||||
</script>
|
||||
@@ -10,7 +10,7 @@
|
||||
<link rel="stylesheet" href="../script/semantic/semantic.min.css">
|
||||
<script type="text/javascript" src="../script/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript" src="../script/semantic/semantic.min.js"></script>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<link rel="stylesheet" href="../main.css">
|
||||
<style>
|
||||
.clickable{
|
||||
cursor: pointer;
|
||||
@@ -149,6 +149,20 @@
|
||||
<!-- Log files will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lines Dropdown -->
|
||||
<div class="ui selection dropdown" id="linesDropdown" style="margin-left: 0.4em; margin-top: 0.4em; height: 2.8em;">
|
||||
<div class="text">Last 100 Lines</div>
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="menu">
|
||||
<div class="item selected" data-value="100">Last 100 Lines</div>
|
||||
<div class="item" data-value="300">Last 300 Lines</div>
|
||||
<div class="item" data-value="500">Last 500 Lines</div>
|
||||
<div class="item" data-value="1000">Last 1000 Lines</div>
|
||||
<div class="item" data-value="2000">Last 2000 Lines</div>
|
||||
<div class="item" data-value="all">All Lines</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download Button -->
|
||||
<button class="ui icon basic button logfile_menu_btn" id="downloadLogBtn" title="Download Current Log File">
|
||||
@@ -263,11 +277,13 @@ Pick a log file from the menu to start debugging
|
||||
<script>
|
||||
//LogView Implementation
|
||||
var currentFilter = "all";
|
||||
var currentLines = "100";
|
||||
var currentOpenedLogURL = "";
|
||||
var currentLogFile = "";
|
||||
var autoscroll = false;
|
||||
|
||||
$(".checkbox").checkbox();
|
||||
$(".dropdown").dropdown();
|
||||
|
||||
/* Menu Subpanel Switch */
|
||||
$(".subpanel").hide();
|
||||
@@ -289,7 +305,7 @@ Pick a log file from the menu to start debugging
|
||||
/* Refresh Button */
|
||||
$("#refreshLogBtn").on("click", function() {
|
||||
if (currentLogFile) {
|
||||
openLog(null, null, currentLogFile, currentFilter);
|
||||
openLog(null, null, currentLogFile, currentFilter, currentLines);
|
||||
loadLogSummary(currentLogFile);
|
||||
} else {
|
||||
alert('Please select a log file first.');
|
||||
@@ -299,6 +315,7 @@ Pick a log file from the menu to start debugging
|
||||
/* Log file dropdown */
|
||||
function populateLogfileDropdown() {
|
||||
$.get("/api/log/list", function(data){
|
||||
console.log(data);
|
||||
let $menu = $("#logfileDropdownMenu");
|
||||
$menu.html("");
|
||||
for (let [key, value] of Object.entries(data)) {
|
||||
@@ -310,17 +327,12 @@ Pick a log file from the menu to start debugging
|
||||
});
|
||||
}
|
||||
$('#logfileDropdown').dropdown('refresh');
|
||||
|
||||
//let firstItem = $menu.find('.item').first();
|
||||
//if (firstItem.length) {
|
||||
// $('#logfileDropdown').dropdown('set selected', firstItem.data('value'));
|
||||
//}
|
||||
});
|
||||
}
|
||||
$('#logfileDropdown').dropdown({
|
||||
onChange: function(value, text, $choice) {
|
||||
if (value) {
|
||||
openLog(null, $choice.data('category'), value, currentFilter || "all");
|
||||
openLog(null, $choice.data('category'), value, currentFilter, currentLines);
|
||||
loadLogSummary(value);
|
||||
}
|
||||
}
|
||||
@@ -336,7 +348,7 @@ Pick a log file from the menu to start debugging
|
||||
}
|
||||
$(".filterbtn.active").removeClass("active");
|
||||
$(`.filterbtn[filter="${value}"]`).addClass("active");
|
||||
openLog(null, null, currentLogFile, currentFilter);
|
||||
openLog(null, null, currentLogFile, currentFilter, currentLines);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -344,6 +356,21 @@ Pick a log file from the menu to start debugging
|
||||
$('#filterDropdown').dropdown('set selected', 'all');
|
||||
currentFilter = "all";
|
||||
|
||||
/* Lines dropdown */
|
||||
$('#linesDropdown').dropdown({
|
||||
onChange: function(value) {
|
||||
currentLines = value;
|
||||
if (!currentLogFile) {
|
||||
return;
|
||||
}
|
||||
openLog(null, null, currentLogFile, currentFilter, currentLines);
|
||||
}
|
||||
});
|
||||
|
||||
// Set default lines to 100
|
||||
$('#linesDropdown').dropdown('set selected', '100');
|
||||
currentLines = "100";
|
||||
|
||||
/* Log download button */
|
||||
$("#downloadLogBtn").on("click", function() {
|
||||
if (!currentLogFile) {
|
||||
@@ -566,11 +593,11 @@ Pick a log file from the menu to start debugging
|
||||
}
|
||||
|
||||
|
||||
function openLog(object, catergory, filename, filter="all"){
|
||||
function openLog(object, catergory, filename, filter="all", lines="100"){
|
||||
$(".logfile.active").removeClass('active');
|
||||
$(object).addClass("active");
|
||||
currentLogFile = filename;
|
||||
currentOpenedLogURL = "/api/log/read?file=" + filename + "&filter=" + filter;
|
||||
currentOpenedLogURL = "/api/log/read?file=" + filename + "&filter=" + filter + "&lines=" + lines;
|
||||
$.get(currentOpenedLogURL, function(data){
|
||||
if (data.error !== undefined){
|
||||
alert(data.error);
|
||||
|
||||
Reference in New Issue
Block a user