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:
Toby Chui
2025-10-29 21:47:48 +08:00
parent 7829e5a321
commit 5dc89b812d
9 changed files with 499 additions and 60 deletions

View File

@@ -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)
}

View File

@@ -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")
}
}

View File

@@ -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")

View 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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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);