mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-09-13 07:39:37 +02:00
Added wip log rotate feature
- Added log rotate function interface - Added darwin amd64 support in make file (Intel Macs) - Added log summary and error API
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
|
# PLATFORMS := darwin/amd64 darwin/arm64 freebsd/amd64 linux/386 linux/amd64 linux/arm linux/arm64 linux/mipsle windows/386 windows/amd64 windows/arm windows/arm64
|
||||||
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 freebsd/amd64
|
PLATFORMS := linux/amd64 linux/386 linux/arm linux/arm64 linux/mipsle linux/riscv64 windows/amd64 freebsd/amd64 darwin/amd64
|
||||||
temp = $(subst /, ,$@)
|
temp = $(subst /, ,$@)
|
||||||
os = $(word 1, $(temp))
|
os = $(word 1, $(temp))
|
||||||
arch = $(word 2, $(temp))
|
arch = $(word 2, $(temp))
|
||||||
|
@@ -383,7 +383,7 @@ func initAPIs(targetMux *http.ServeMux) {
|
|||||||
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
|
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
|
||||||
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
|
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
|
||||||
authRouter.HandleFunc("/api/log/summary", LogViewer.HandleReadLogSummary)
|
authRouter.HandleFunc("/api/log/summary", LogViewer.HandleReadLogSummary)
|
||||||
|
authRouter.HandleFunc("/api/log/errors", LogViewer.HandleLogErrorSummary)
|
||||||
//Debug
|
//Debug
|
||||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||||
}
|
}
|
||||||
|
@@ -18,9 +18,10 @@ import (
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
Prefix string //Prefix for log files
|
Prefix string //Prefix for log files
|
||||||
LogFolder string //Folder to store the log file
|
LogFolder string //Folder to store the log file
|
||||||
CurrentLogFile string //Current writing filename
|
CurrentLogFile string //Current writing filename
|
||||||
|
RotateOption RotateOption //Options for log rotation, see rotate.go
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
file *os.File
|
file *os.File
|
||||||
}
|
}
|
||||||
|
127
src/mod/info/logger/rotate.go
Normal file
127
src/mod/info/logger/rotate.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RotateOption struct {
|
||||||
|
Enabled bool //Whether log rotation is enabled
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) LogNeedRotate(filename string) bool {
|
||||||
|
if !l.RotateOption.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
info, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return info.Size() >= l.RotateOption.MaxSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) RotateLog() error {
|
||||||
|
if !l.RotateOption.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
needRotate := l.LogNeedRotate(l.CurrentLogFile)
|
||||||
|
if !needRotate {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//Close current file
|
||||||
|
if l.file != nil {
|
||||||
|
l.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine backup directory
|
||||||
|
backupDir := l.RotateOption.BackupDir
|
||||||
|
if backupDir == "" {
|
||||||
|
backupDir = filepath.Dir(l.CurrentLogFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure backup directory exists
|
||||||
|
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate rotated filename with timestamp
|
||||||
|
timestamp := time.Now().Format("20060102-150405")
|
||||||
|
baseName := filepath.Base(l.CurrentLogFile)
|
||||||
|
rotatedName := fmt.Sprintf("%s.%s", baseName, timestamp)
|
||||||
|
rotatedPath := filepath.Join(backupDir, rotatedName)
|
||||||
|
|
||||||
|
// Rename current log file to rotated file
|
||||||
|
if err := os.Rename(l.CurrentLogFile, rotatedPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally compress the rotated file
|
||||||
|
if l.RotateOption.Compress {
|
||||||
|
if err := compressFile(rotatedPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Remove the uncompressed rotated file after compression
|
||||||
|
os.Remove(rotatedPath)
|
||||||
|
rotatedPath += ".gz"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old backups if exceeding MaxBackups
|
||||||
|
if l.RotateOption.MaxBackups > 0 {
|
||||||
|
files, err := filepath.Glob(filepath.Join(backupDir, baseName+".*"))
|
||||||
|
if err == nil && len(files) > l.RotateOption.MaxBackups {
|
||||||
|
sort.Slice(files, func(i, j int) bool {
|
||||||
|
return files[i] < files[j]
|
||||||
|
})
|
||||||
|
for _, old := range files[:len(files)-l.RotateOption.MaxBackups] {
|
||||||
|
os.Remove(old)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reopen a new log file
|
||||||
|
file, err := os.OpenFile(l.CurrentLogFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
l.file = file
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// compressFile compresses the given file using zip format and creates a .gz file.
|
||||||
|
func compressFile(filename string) error {
|
||||||
|
zipFilename := filename + ".gz"
|
||||||
|
outFile, err := os.Create(zipFilename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(outFile)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
fileToCompress, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fileToCompress.Close()
|
||||||
|
|
||||||
|
w, err := zipWriter.Create(filepath.Base(filename))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(w, fileToCompress)
|
||||||
|
return err
|
||||||
|
}
|
@@ -126,6 +126,52 @@ func (v *Viewer) HandleReadLogSummary(w http.ResponseWriter, r *http.Request) {
|
|||||||
utils.SendJSONResponse(w, summary)
|
utils.SendJSONResponse(w, summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Viewer) HandleLogErrorSummary(w http.ResponseWriter, r *http.Request) {
|
||||||
|
filename, err := utils.GetPara(r, "file")
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, "invalid filename given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(filename)))
|
||||||
|
if err != nil {
|
||||||
|
utils.SendErrorResponse(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Generate the error summary for log that is request and non 100 - 200 range status code
|
||||||
|
errorLines := [][]string{}
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Only process router logs with a status code not in 1xx or 2xx
|
||||||
|
if strings.Contains(line, "[router:") {
|
||||||
|
//Extract date time from the line
|
||||||
|
timestamp := ""
|
||||||
|
if strings.HasPrefix(line, "[") && strings.Contains(line, "]") {
|
||||||
|
timestamp = line[1:strings.Index(line, "]")]
|
||||||
|
}
|
||||||
|
|
||||||
|
//Trim out the request metadata
|
||||||
|
line = line[strings.LastIndex(line, "]")+1:]
|
||||||
|
fields := strings.Fields(strings.TrimSpace(line))
|
||||||
|
|
||||||
|
if len(fields) > 0 {
|
||||||
|
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))...)
|
||||||
|
errorLines = append(errorLines, fieldsWithTimestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
js, _ := json.Marshal(errorLines)
|
||||||
|
utils.SendJSONResponse(w, string(js))
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Log Access Functions
|
Log Access Functions
|
||||||
*/
|
*/
|
||||||
@@ -322,104 +368,3 @@ func (v *Viewer) LoadLogSummary(filename string) (string, error) {
|
|||||||
return "", errors.New("log file not found")
|
return "", errors.New("log file not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Log examples:
|
|
||||||
|
|
||||||
[2025-08-18 21:02:15.664246] [router:host-http] [origin:test1.localhost] [client: 127.0.0.1] [useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0] GET /system/file_system/listDirHash?dir=s2%3A%2FMusic%2FMusic%20Bank%2FYear%202025%2F08-2025%2F 200
|
|
||||||
[2025-08-18 21:02:20.682091] [router:host-http] [origin:test1.localhost] [client: 127.0.0.1] [useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0] GET /system/file_system/listDirHash?dir=s2%3A%2FMusic%2FMusic%20Bank%2FYear%202025%2F08-2025%2F 200
|
|
||||||
[2025-08-18 21:02:20.725569] [internal] [system:info] mDNS scan result updated
|
|
||||||
[2025-08-17 20:24:38.669488] [uptime-monitor] [system:info] Uptime updated - 1755433478
|
|
||||||
[2025-08-17 20:25:08.247535] [internal] [system:info] mDNS scan result updated
|
|
||||||
[2025-08-17 20:29:38.669187] [uptime-monitor] [system:info] Uptime updated - 1755433778
|
|
||||||
[2025-08-17 20:34:38.669090] [uptime-monitor] [system:info] Uptime updated - 1755434078
|
|
||||||
[2025-08-17 20:39:38.668610] [uptime-monitor] [system:info] Uptime updated - 1755434378
|
|
||||||
[2025-08-17 20:40:08.248890] [internal] [system:info] mDNS scan result updated
|
|
||||||
[2025-08-17 20:44:38.669058] [uptime-monitor] [system:info] Uptime updated - 1755434678
|
|
||||||
[2025-08-17 20:49:38.669340] [uptime-monitor] [system:info] Uptime updated - 1755434978
|
|
||||||
[2025-08-17 20:54:38.668785] [uptime-monitor] [system:info] Uptime updated - 1755435278
|
|
||||||
[2025-08-17 20:55:08.247715] [internal] [system:info] mDNS scan result updated
|
|
||||||
[2025-08-17 20:59:38.668575] [uptime-monitor] [system:info] Uptime updated - 1755435578
|
|
||||||
[2025-08-17 21:04:38.669637] [uptime-monitor] [system:info] Uptime updated - 1755435878
|
|
||||||
[2025-08-17 21:09:38.669109] [uptime-monitor] [system:info] Uptime updated - 1755436178
|
|
||||||
[2025-08-17 21:10:08.247618] [internal] [system:info] mDNS scan result updated
|
|
||||||
[2025-08-17 21:14:38.668828] [uptime-monitor] [system:info] Uptime updated - 1755436478
|
|
||||||
[2025-08-17 21:19:38.669091] [uptime-monitor] [system:info] Uptime updated - 1755436778
|
|
||||||
[2025-08-17 21:24:38.668830] [uptime-monitor] [system:info] Uptime updated - 1755437078
|
|
||||||
[2025-08-17 21:25:08.246931] [internal] [system:info] mDNS scan result updated
|
|
||||||
[2025-08-17 21:29:38.673217] [uptime-monitor] [system:info] Uptime updated - 1755437378
|
|
||||||
[2025-08-17 21:34:38.668883] [uptime-monitor] [system:info] Uptime updated - 1755437678
|
|
||||||
[2025-08-17 21:39:38.668980] [uptime-monitor] [system:info] Uptime updated - 1755437978
|
|
||||||
[2025-08-17 21:40:08.266062] [internal] [system:info] mDNS scan result updated
|
|
||||||
[2025-08-17 21:44:38.669150] [uptime-monitor] [system:info] Uptime updated - 1755438278
|
|
||||||
[2025-08-17 21:49:38.668640] [uptime-monitor] [system:info] Uptime updated - 1755438578
|
|
||||||
[2025-08-17 21:54:38.669275] [uptime-monitor] [system:info] Uptime updated - 1755438878
|
|
||||||
[2025-08-17 21:55:08.266425] [internal] [system:info] mDNS scan result updated
|
|
||||||
[2025-08-17 21:59:38.668861] [uptime-monitor] [system:info] Uptime updated - 1755439178
|
|
||||||
[2025-08-17 22:04:38.668840] [uptime-monitor] [system:info] Uptime updated - 1755439478
|
|
||||||
[2025-08-17 22:09:38.668798] [uptime-monitor] [system:info] Uptime updated - 1755439778
|
|
||||||
[2025-08-17 22:10:08.266417] [internal] [system:info] mDNS scan result updated
|
|
||||||
[2025-08-17 22:14:38.669122] [uptime-monitor] [system:info] Uptime updated - 1755440078
|
|
||||||
[2025-08-17 22:19:38.668810] [uptime-monitor] [system:info] Uptime updated - 1755440378
|
|
||||||
[2025-08-17 22:21:35.947519] [netstat] [system:info] Netstats listener stopped
|
|
||||||
[2025-08-17 22:21:35.947519] [internal] [system:info] Shutting down Zoraxy
|
|
||||||
[2025-08-17 22:21:35.947519] [internal] [system:info] Closing Netstats Listener
|
|
||||||
[2025-08-17 22:21:35.970526] [plugin-manager] [system:error] plugin com.example.restful-example encounted a fatal error. Disabling plugin...: exit status 0xc000013a
|
|
||||||
[2025-08-17 22:21:35.970526] [plugin-manager] [system:error] plugin org.aroz.zoraxy.api_call_example encounted a fatal error. Disabling plugin...: exit status 0xc000013a
|
|
||||||
[2025-08-17 22:21:36.250929] [internal] [system:info] Closing Statistic Collector
|
|
||||||
[2025-08-17 22:21:36.318808] [internal] [system:info] Stopping mDNS Discoverer (might take a few minutes)
|
|
||||||
[2025-08-17 22:21:36.319829] [internal] [system:info] Shutting down load balancer
|
|
||||||
[2025-08-17 22:21:36.319829] [internal] [system:info] Closing Certificates Auto Renewer
|
|
||||||
[2025-08-17 22:21:36.319829] [internal] [system:info] Closing Access Controller
|
|
||||||
[2025-08-17 22:21:36.319829] [internal] [system:info] Shutting down plugin manager
|
|
||||||
[2025-08-17 22:21:36.319829] [internal] [system:info] Cleaning up tmp files
|
|
||||||
[2025-08-17 22:21:36.328033] [internal] [system:info] Stopping system database
|
|
||||||
[2025-08-18 20:31:49.673182] [database] [system:info] Using BoltDB as the database backend
|
|
||||||
[2025-08-18 20:31:49.784069] [auth] [system:info] Authentication session key loaded from database
|
|
||||||
[2025-08-18 20:31:50.290804] [LoadBalancer] [system:info] Upstream state cache ticker started
|
|
||||||
[2025-08-18 20:31:50.510300] [static-webserv] [system:info] Static Web Server started. Listeing on :5487
|
|
||||||
[2025-08-18 20:31:51.017433] [internal] [system:info] Starting ACME handler
|
|
||||||
[2025-08-18 20:31:51.022545] [cert-renew] [system:info] ACME early renew set to 30 days and check interval set to 86400 seconds
|
|
||||||
[2025-08-18 20:31:51.073031] [plugin-manager] [system:info] Hot reload ticker started
|
|
||||||
[2025-08-18 20:31:51.357203] [plugin-manager] [system:info] Loaded plugin: API Call Example Plugin
|
|
||||||
[2025-08-18 20:31:51.357782] [plugin-manager] [system:info] Generated API key for plugin API Call Example Plugin
|
|
||||||
[2025-08-18 20:31:51.358293] [plugin-manager] [system:info] Starting plugin API Call Example Plugin at :5974
|
|
||||||
[2025-08-18 20:31:51.406867] [plugin-manager] [system:info] [API Call Example Plugin:13316] Starting API Call Example Plugin on 127.0.0.1:5974
|
|
||||||
[2025-08-18 20:31:51.466380] [plugin-manager] [system:info] Plugin list synced from plugin store
|
|
||||||
[2025-08-18 20:31:51.662866] [plugin-manager] [system:info] Loaded plugin: Restful Example
|
|
||||||
[2025-08-18 20:31:51.663383] [plugin-manager] [system:info] Starting plugin Restful Example at :5874
|
|
||||||
[2025-08-18 20:31:51.688641] [plugin-manager] [system:info] [Restful Example:10500] Restful-example started at http://127.0.0.1:5874
|
|
||||||
[2025-08-18 20:31:51.721309] [plugin-manager] [system:info] Plugin hash generated for: org.aroz.zoraxy.api_call_example
|
|
||||||
[2025-08-18 20:31:51.777523] [plugin-manager] [system:info] Plugin hash generated for: com.example.restful-example
|
|
||||||
[2025-08-18 20:31:51.789497] [internal] [system:info] Inbound port not set. Using default (443)
|
|
||||||
[2025-08-18 20:31:51.789497] [internal] [system:info] TLS mode enabled. Serving proxy request with TLS
|
|
||||||
[2025-08-18 20:31:51.789497] [internal] [system:info] Development mode enabled. Using no-store Cache Control policy
|
|
||||||
[2025-08-18 20:31:51.790016] [internal] [system:info] Force latest TLS mode disabled. Minimum TLS version is set to v1.0
|
|
||||||
[2025-08-18 20:31:51.790016] [internal] [system:info] Port 80 listener enabled
|
|
||||||
[2025-08-18 20:31:51.790016] [internal] [system:info] Force HTTPS mode enabled
|
|
||||||
[2025-08-18 20:31:51.825385] [proxy-config] [system:info] *.yami.localhost -> 192.168.0.16:8080 routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.833567] [proxy-config] [system:info] a.localhost -> imuslab.com routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.849358] [proxy-config] [system:info] aroz.localhost -> 192.168.0.16:8080 routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.852977] [proxy-config] [system:info] auth.localhost -> localhost:5488 routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.866792] [proxy-config] [system:info] debug.localhost -> dc.imuslab.com:8080 routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.878091] [proxy-config] [system:info] peer.localhost -> 192.168.0.16:8080 routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.887843] [proxy-config] [system:info] / -> 127.0.0.1:5487 routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.895039] [proxy-config] [system:info] test.imuslab.com -> 192.168.1.202:8443 routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.909917] [proxy-config] [system:info] test.imuslab.internal -> 127.0.0.1:80 routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.922685] [proxy-config] [system:info] test.localhost -> alanyeung.co routing rule loaded
|
|
||||||
[2025-08-18 20:31:51.937314] [proxy-config] [system:info] webdav.localhost -> 127.0.0.1:80/redirect routing rule loaded
|
|
||||||
[2025-08-18 20:31:52.239414] [dprouter] [system:info] Starting HTTP-to-HTTPS redirector (port 80)
|
|
||||||
[2025-08-18 20:31:52.239414] [internal] [system:info] Dynamic Reverse Proxy service started
|
|
||||||
[2025-08-18 20:31:52.239414] [dprouter] [system:info] Reverse proxy service started in the background (TLS mode)
|
|
||||||
[2025-08-18 20:31:52.289262] [internal] [system:info] Zoraxy started. Visit control panel at http://localhost:8000
|
|
||||||
[2025-08-18 20:31:52.289262] [internal] [system:info] Assigned temporary port:36951
|
|
||||||
[2025-08-18 20:31:54.513995] [internal] [system:info] Uptime Monitor background service started
|
|
||||||
[2025-08-18 20:32:20.725596] [internal] [system:info] mDNS Startup scan completed
|
|
||||||
[2025-08-18 20:36:52.239883] [uptime-monitor] [system:info] Uptime updated - 1755520612
|
|
||||||
[2025-08-18 20:56:14.160166] [router:host-http] [origin:test1.localhost] [client: 127.0.0.1] [useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0] GET /system/file_system/preference?key=file_explorer/theme 200
|
|
||||||
[2025-08-18 20:56:14.160166] [router:host-http] [origin:test1.localhost] [client: 127.0.0.1] [useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0] GET /system/file_system/preference?key=file_explorer/listmode 200
|
|
||||||
[2025-08-18 20:56:14.160166] [router:host-http] [origin:test1.localhost] [client: 127.0.0.1] [useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0] GET /system/file_system/preference?key=file_explorer/listmode 200
|
|
||||||
[2025-08-18 20:56:14.170270] [router:host-http] [origin:test1.localhost] [client: 127.0.0.1] [useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0] GET /system/file_system/listRoots?user=true 200
|
|
||||||
[2025-08-18 20:56:14.171752] [router:host-http] [origin:test1.localhost] [client: 127.0.0.1] [useragent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0] GET /system/id/requestInfo 200
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
@@ -11,7 +11,6 @@
|
|||||||
<script type="text/javascript" src="../script/jquery-3.6.0.min.js"></script>
|
<script type="text/javascript" src="../script/jquery-3.6.0.min.js"></script>
|
||||||
<script type="text/javascript" src="../script/semantic/semantic.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">
|
||||||
<link rel="stylesheet" href="darktheme.css">
|
|
||||||
<style>
|
<style>
|
||||||
.clickable{
|
.clickable{
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -70,59 +69,16 @@
|
|||||||
color:white;
|
color:white;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme .loglist {
|
#errorHighlightWrapper{
|
||||||
background-color: #1b1c1d;
|
background-color: #f9f9f9;
|
||||||
color: #ffffff;
|
padding: 1em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
max-height:300px;
|
||||||
|
overflow-y:auto;
|
||||||
|
margin-bottom:1em;
|
||||||
|
border: 1px solid #b3b3b3;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.header .content .sub.header {
|
|
||||||
color: #bbbbbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.divider {
|
|
||||||
border-color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.accordion .title,
|
|
||||||
body.darkTheme .loglist .ui.accordion .content {
|
|
||||||
background-color: #1b1c1d;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.accordion .title:hover {
|
|
||||||
background-color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.list .item {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.list .item .content {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.list .item .showing {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.button.filterbtn {
|
|
||||||
background-color: #333333;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.button.filterbtn:hover {
|
|
||||||
background-color: #555555;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist .ui.toggle.checkbox label {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.darkTheme .loglist small {
|
|
||||||
color: #bbbbbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
#logfileDropdown {
|
#logfileDropdown {
|
||||||
margin-left: 0.4em !important;
|
margin-left: 0.4em !important;
|
||||||
@@ -136,23 +92,35 @@
|
|||||||
margin-left: 0.4em !important;
|
margin-left: 0.4em !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
#toggleFullscreenBtn {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logrenderWrapper{
|
||||||
|
margin-left: 0em !important;
|
||||||
|
margin-right: 0em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Currently not darktheme ready -->
|
||||||
<link rel="stylesheet" href="../darktheme.css">
|
<link rel="stylesheet" href="../darktheme.css">
|
||||||
<script src="../script/darktheme.js"></script>
|
<!-- <script src="../script/darktheme.js"></script> -->
|
||||||
<br>
|
<br>
|
||||||
<div class="ui container">
|
<div class="ui container">
|
||||||
<div class="ui stackable secondary menu">
|
<div class="ui stackable secondary menu">
|
||||||
<div class="item" style="font-weight: bold;">
|
<div class="item" style="font-weight: bold;">
|
||||||
Zoraxy LogView
|
Zoraxy LogView
|
||||||
</div>
|
</div>
|
||||||
<a class="item active panel_menu_btn" id="dashboardMenu">
|
<a class="item active panel_menu_btn" id="logViewMenu">
|
||||||
Dashboard
|
|
||||||
</a>
|
|
||||||
<a class="item panel_menu_btn" id="logViewMenu">
|
|
||||||
Log View
|
Log View
|
||||||
</a>
|
</a>
|
||||||
|
<a class="item panel_menu_btn" id="summaryMenu">
|
||||||
|
Summary
|
||||||
|
</a>
|
||||||
<a class="item panel_menu_btn" id="settingsMenu">
|
<a class="item panel_menu_btn" id="settingsMenu">
|
||||||
Settings
|
Settings
|
||||||
</a>
|
</a>
|
||||||
@@ -194,32 +162,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br><br>
|
<br><br>
|
||||||
<!-- Dashboard View -->
|
<!-- Summary View -->
|
||||||
<div id="dashboard" class="ui container subpanel">
|
<div id="summary" class="ui container subpanel">
|
||||||
<h3 class="ui header">
|
<h3 class="ui header">
|
||||||
Dashboard
|
Summary
|
||||||
</h3>
|
</h3>
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
<p>Welcome to LogVPro! Use the left menu to select a log file </p>
|
<div class="ui small statistics" style="display: flex; justify-content: center;">
|
||||||
<div id="analyzer">
|
<div class="statistic">
|
||||||
|
<div class="value" id="successRequestsCount"></div>
|
||||||
|
<div class="label">Success Requests</div>
|
||||||
|
</div>
|
||||||
|
<div class="statistic">
|
||||||
|
<div class="value" id="errorRequestsCount"></div>
|
||||||
|
<div class="label">Error Requests</div>
|
||||||
|
</div>
|
||||||
|
<div class="statistic">
|
||||||
|
<div class="value" id="totalRequestsCount"></div>
|
||||||
|
<div class="label">Total Requests</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<!-- Error Highlight Section -->
|
||||||
|
<h4 class="ui header">Error Highlights</h4>
|
||||||
|
<div id="errorHighlightWrapper">
|
||||||
|
<p><i class="ui green circle check icon"></i> No error data loaded</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div id="analyzer">
|
||||||
|
<div class="ui info message">
|
||||||
|
<div class="header">
|
||||||
|
No log file selected
|
||||||
|
</div>
|
||||||
|
<p>Please select a log file from the dropdown menu to view analysis and summary.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Log Viewer -->
|
<!-- Log Viewer -->
|
||||||
<div id="logviewer" class="ui container subpanel" style="display:none;">
|
<div id="logviewer" class="subpanel" style="display:none;">
|
||||||
<textarea id="logrender" spellcheck="false" readonly="true">
|
<div id="logrenderWrapper" class="ui container" style="position:relative;">
|
||||||
|
<textarea id="logrender" spellcheck="false" readonly="true">
|
||||||
Pick a log file from the menu to start debugging
|
Pick a log file from the menu to start debugging
|
||||||
</textarea>
|
</textarea>
|
||||||
<br><br>
|
<!-- Full Screen Button -->
|
||||||
<div class="ui divider"></div>
|
<button id="toggleFullscreenBtn" class="ui icon black button" title="Toggle Full Screen" style="border: 1px solid white; position:absolute; top:0.5em; right:0.5em; z-index:999;">
|
||||||
<div class="ui toggle checkbox">
|
<i class="expand arrows alternate icon"></i>
|
||||||
<input type="checkbox" id="enableAutoScroll" onchange="handleAutoScrollTicker(event, this.checked);">
|
</button>
|
||||||
<label>Auto Refresh<br>
|
<!-- Scroll to Bottom Button -->
|
||||||
<small>Refresh the viewing log every 10 seconds</small></label>
|
<button id="scrollBottomBtn" class="ui icon black button" title="Scroll to Bottom" style="border: 1px solid white; position:absolute; bottom:0.5em; right:0.5em; z-index:999;">
|
||||||
|
<i class="angle double down icon"></i>
|
||||||
|
</button>
|
||||||
|
<script>
|
||||||
|
$("#scrollBottomBtn").on("click", function() {
|
||||||
|
var textarea = document.getElementById('logrender');
|
||||||
|
textarea.scrollTop = textarea.scrollHeight;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Toggle full screen for logrenderWrapper
|
||||||
|
$("#toggleFullscreenBtn").on("click", function() {
|
||||||
|
$("#logrenderWrapper").toggleClass("ui container");
|
||||||
|
$(this).find("i").toggleClass("expand arrows alternate compress arrows alternate");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<br>
|
||||||
|
<div class="ui container">
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui toggle checkbox">
|
||||||
|
<input type="checkbox" id="enableAutoScroll" onchange="handleAutoScrollTicker(event, this.checked);">
|
||||||
|
<label>Auto Refresh<br>
|
||||||
|
<small>Refresh the viewing log every 3 seconds</small></label>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<small>Notes: Some log files might be huge. Make sure you have checked the log file size before opening</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider"></div>
|
|
||||||
<small>Notes: Some log files might be huge. Make sure you have checked the log file size before opening</small>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
@@ -247,14 +266,14 @@ Pick a log file from the menu to start debugging
|
|||||||
|
|
||||||
/* Menu Subpanel Switch */
|
/* Menu Subpanel Switch */
|
||||||
$(".subpanel").hide();
|
$(".subpanel").hide();
|
||||||
$("#dashboard").show();
|
$("#logviewer").show();
|
||||||
$(".panel_menu_btn").on("click", function() {
|
$(".panel_menu_btn").on("click", function() {
|
||||||
var id = $(this).attr("id");
|
var id = $(this).attr("id");
|
||||||
$(".subpanel").hide();
|
$(".subpanel").hide();
|
||||||
$(".ui.menu .item").removeClass("active");
|
$(".ui.menu .item").removeClass("active");
|
||||||
$(this).addClass("active");
|
$(this).addClass("active");
|
||||||
if (id === "dashboardMenu") {
|
if (id === "summaryMenu") {
|
||||||
$("#dashboard").show();
|
$("#summary").show();
|
||||||
} else if (id === "logViewMenu") {
|
} else if (id === "logViewMenu") {
|
||||||
$("#logviewer").show();
|
$("#logviewer").show();
|
||||||
} else if (id === "settingsMenu") {
|
} else if (id === "settingsMenu") {
|
||||||
@@ -276,12 +295,18 @@ Pick a log file from the menu to start debugging
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
$('#logfileDropdown').dropdown('refresh');
|
$('#logfileDropdown').dropdown('refresh');
|
||||||
|
|
||||||
|
//let firstItem = $menu.find('.item').first();
|
||||||
|
//if (firstItem.length) {
|
||||||
|
// $('#logfileDropdown').dropdown('set selected', firstItem.data('value'));
|
||||||
|
//}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
$('#logfileDropdown').dropdown({
|
$('#logfileDropdown').dropdown({
|
||||||
onChange: function(value, text, $choice) {
|
onChange: function(value, text, $choice) {
|
||||||
if (value) {
|
if (value) {
|
||||||
openLog(null, $choice.data('category'), value, currentFilter || "all");
|
openLog(null, $choice.data('category'), value, currentFilter || "all");
|
||||||
|
loadLogSummary(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -333,6 +358,198 @@ Pick a log file from the menu to start debugging
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Analyzer */
|
||||||
|
function loadLogSummary(filename){
|
||||||
|
$.get("/api/log/summary?file=" + filename, function(data){
|
||||||
|
if (data.error !== undefined){
|
||||||
|
$("#analyzer").html("<div class='ui negative message'><div class='header'>Error</div><p>" + data.error + "</p></div>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#successRequestsCount").text(data.total_valid || 0);
|
||||||
|
$("#errorRequestsCount").text(data.total_errors || 0);
|
||||||
|
$("#totalRequestsCount").text(data.total_requests || 0);
|
||||||
|
renderSummaryToHTML(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
$.get("/api/log/errors?file=" + encodeURIComponent(filename), function(data) {
|
||||||
|
renderErrorHighlights(data);
|
||||||
|
}).fail(function() {
|
||||||
|
$("#errorHighlightWrapper").html(
|
||||||
|
`<div class="ui error message">
|
||||||
|
<div class="header">Failed to load error highlights</div>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummaryToHTML(data){
|
||||||
|
/* Render summary analysis to #analyzer */
|
||||||
|
let html = `
|
||||||
|
<div class="ui stackable grid">
|
||||||
|
<div class="eight wide column">
|
||||||
|
<h4 class="ui header">Request Methods</h4>
|
||||||
|
<canvas id="requestMethodsChart" height="180"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="eight wide column">
|
||||||
|
<h4 class="ui header">Hits Per Day</h4>
|
||||||
|
<canvas id="hitsPerDayChart" height="180"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui stackable grid">
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
<h4 class="ui header">Hits Per Site</h4>
|
||||||
|
<div id="siteCharts"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui stackable grid">
|
||||||
|
<div class="eight wide column">
|
||||||
|
<h4 class="ui header">Unique IPs</h4>
|
||||||
|
<table class="ui celled table">
|
||||||
|
<thead><tr><th>IP</th><th>Requests</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${Object.entries(data.unique_ips)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([ip, count]) => `<tr><td>${ip}</td><td>${count}</td></tr>`)
|
||||||
|
.join("")}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="eight wide column">
|
||||||
|
<h4 class="ui header">Top User Agents</h4>
|
||||||
|
<table class="ui celled table">
|
||||||
|
<thead><tr><th>User Agent</th><th>Requests</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${Object.entries(data.top_user_agents)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([ua, count]) => `<tr><td style="word-break:break-all;">${ua}</td><td>${count}</td></tr>`)
|
||||||
|
.join("")}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div class="ui stackable grid">
|
||||||
|
<div class="sixteen wide column">
|
||||||
|
<h4 class="ui header">Top Paths</h4>
|
||||||
|
<table class="ui celled table">
|
||||||
|
<thead><tr><th>Path</th><th>Requests</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${Object.entries(data.top_paths)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.map(([path, count]) => `<tr><td style="word-break:break-all;">${path}</td><td>${count}</td></tr>`)
|
||||||
|
.join("")}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$("#analyzer").html(html);
|
||||||
|
|
||||||
|
// Load Chart.js if not loaded
|
||||||
|
function loadChartJs(cb) {
|
||||||
|
if (window.Chart) { cb(); return; }
|
||||||
|
let s = document.createElement("script");
|
||||||
|
s.src = "https://cdn.jsdelivr.net/npm/chart.js";
|
||||||
|
s.onload = cb;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare data for charts
|
||||||
|
function getDateRangeLabels(hitPerDay) {
|
||||||
|
let dates = Object.keys(hitPerDay).sort();
|
||||||
|
if (dates.length === 0) return [];
|
||||||
|
let start = new Date(dates[0]);
|
||||||
|
let end = new Date(dates[dates.length - 1]);
|
||||||
|
let labels = [];
|
||||||
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
|
let y = d.getFullYear(), m = (d.getMonth() + 1).toString().padStart(2, "0"), day = d.getDate().toString().padStart(2, "0");
|
||||||
|
labels.push(`${y}-${m}-${day}`);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadChartJs(function() {
|
||||||
|
// 1. Pie chart for request methods
|
||||||
|
let reqMethods = data.request_methods || {};
|
||||||
|
let reqLabels = Object.keys(reqMethods);
|
||||||
|
let reqCounts = Object.values(reqMethods);
|
||||||
|
new Chart(document.getElementById("requestMethodsChart"), {
|
||||||
|
type: "pie",
|
||||||
|
data: {
|
||||||
|
labels: reqLabels,
|
||||||
|
datasets: [{
|
||||||
|
data: reqCounts,
|
||||||
|
backgroundColor: ["#4b75ff", "#21ba45", "#db2828", "#fbbd08", "#b5cc18", "#a333c8"]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: { responsive: true, plugins: { legend: { position: "bottom" } } }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Histogram for hit_per_day
|
||||||
|
let hitPerDay = data.hit_per_day || {};
|
||||||
|
let dayLabels = getDateRangeLabels(hitPerDay);
|
||||||
|
let dayCounts = dayLabels.map(d => hitPerDay[d] || 0);
|
||||||
|
new Chart(document.getElementById("hitsPerDayChart"), {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: dayLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: "Hits",
|
||||||
|
data: dayCounts,
|
||||||
|
backgroundColor: "#4b75ff"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: { x: { ticks: { autoSkip: false, maxRotation: 90, minRotation: 45 } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Line chart for each site in hit_per_site
|
||||||
|
let siteChartsDiv = $("#siteCharts");
|
||||||
|
let siteData = data.hit_per_site || {};
|
||||||
|
let i = 0;
|
||||||
|
for (let [site, counts] of Object.entries(siteData)) {
|
||||||
|
let canvasId = "siteChart_" + i;
|
||||||
|
siteChartsDiv.append(`
|
||||||
|
<div style="margin-bottom:2em;">
|
||||||
|
<b>${site}</b>
|
||||||
|
<canvas id="${canvasId}" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
// If counts length doesn't match dayLabels, pad left with zeros
|
||||||
|
let paddedCounts = counts.slice(-dayLabels.length);
|
||||||
|
while (paddedCounts.length < dayLabels.length) paddedCounts.unshift(0);
|
||||||
|
new Chart(document.getElementById(canvasId), {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: dayLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: "Hits",
|
||||||
|
data: paddedCounts,
|
||||||
|
fill: false,
|
||||||
|
borderColor: "#21ba45",
|
||||||
|
backgroundColor: "#21ba45",
|
||||||
|
tension: 0.2
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: { x: { ticks: { autoSkip: false, maxRotation: 90, minRotation: 45 } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function openLog(object, catergory, filename, filter="all"){
|
function openLog(object, catergory, filename, filter="all"){
|
||||||
$(".logfile.active").removeClass('active');
|
$(".logfile.active").removeClass('active');
|
||||||
@@ -347,48 +564,13 @@ Pick a log file from the menu to start debugging
|
|||||||
renderLogWithCurrentFilter(data);
|
renderLogWithCurrentFilter(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
function openLogInNewTab(){
|
function openLogInNewTab(){
|
||||||
if (currentOpenedLogURL != ""){
|
if (currentOpenedLogURL != ""){
|
||||||
window.open(currentOpenedLogURL);
|
window.open(currentOpenedLogURL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initLogList(){
|
|
||||||
$("#logList").html("");
|
|
||||||
$.get("/api/log/list", function(data){
|
|
||||||
//console.log(data);
|
|
||||||
for (let [key, value] of Object.entries(data)) {
|
|
||||||
console.log(key, value);
|
|
||||||
value.reverse(); //Default value was from oldest to newest
|
|
||||||
var fileItemList = "";
|
|
||||||
value.forEach(file => {
|
|
||||||
fileItemList += `<div class="item clickable logfile" onclick="openLog(this, '${key}','${file.Filename}');">
|
|
||||||
<i class="file outline icon"></i>
|
|
||||||
<div class="content">
|
|
||||||
${file.Title} (${formatBytes(file.Filesize)})
|
|
||||||
<div class="showing"><i class="green chevron right icon"></i></div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
})
|
|
||||||
$("#logList").append(`<div class="title">
|
|
||||||
<i class="dropdown icon"></i>
|
|
||||||
${key}
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="ui list">
|
|
||||||
${fileItemList}
|
|
||||||
</div>
|
|
||||||
</div>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(".ui.accordion").accordion();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
initLogList();
|
|
||||||
|
|
||||||
|
|
||||||
function formatBytes(x){
|
function formatBytes(x){
|
||||||
var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||||
let l = 0, n = parseInt(x, 10) || 0;
|
let l = 0, n = parseInt(x, 10) || 0;
|
||||||
@@ -405,26 +587,6 @@ Pick a log file from the menu to start debugging
|
|||||||
textarea.scrollTop = textarea.scrollHeight;
|
textarea.scrollTop = textarea.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filter related functions */
|
|
||||||
$(".filterbtn").on("click", function(evt){
|
|
||||||
//Set filter type
|
|
||||||
let filterType = $(this).attr("filter");
|
|
||||||
currentFilter = (filterType);
|
|
||||||
$(".filterbtn.active").removeClass("active");
|
|
||||||
$(this).addClass('active');
|
|
||||||
|
|
||||||
//Reload the log with filter
|
|
||||||
if (currentOpenedLogURL != ""){
|
|
||||||
$.get(currentOpenedLogURL, function(data){
|
|
||||||
if (data.error !== undefined){
|
|
||||||
alert(data.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
renderLogWithCurrentFilter(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Auto scroll function */
|
/* Auto scroll function */
|
||||||
setInterval(function(){
|
setInterval(function(){
|
||||||
if (autoscroll){
|
if (autoscroll){
|
||||||
@@ -439,174 +601,42 @@ Pick a log file from the menu to start debugging
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 3000);
|
||||||
function handleAutoScrollTicker(event, checked){
|
function handleAutoScrollTicker(event, checked){
|
||||||
autoscroll = checked;
|
autoscroll = checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error Highlights Rendering
|
||||||
/* Log analyzer */
|
function renderErrorHighlights(errors) {
|
||||||
/* --- Log Analyzer UI --- */
|
if (!Array.isArray(errors) || errors.length === 0) {
|
||||||
$("#analyzer").html(`
|
$("#errorHighlightWrapper").html(
|
||||||
<div class="ui segment">
|
`<div class="ui positive message">
|
||||||
<h4 class="ui header">Log Analytics</h4>
|
<div class="header">No errors found</div>
|
||||||
<div class="ui divider"></div>
|
<p>This log file contains no errors.</p>
|
||||||
<div class="ui grid">
|
</div>`
|
||||||
<div class="eight wide column">
|
);
|
||||||
<canvas id="requestTypePie"></canvas>
|
return;
|
||||||
</div>
|
|
||||||
<div class="eight wide column" id="originCharts"></div>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider"></div>
|
|
||||||
<h5>Client IP Table</h5>
|
|
||||||
<div style="max-height:300px;overflow:auto;">
|
|
||||||
<table class="ui celled table" id="clientIpTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Client IP</th>
|
|
||||||
<th>Requests</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Load Chart.js
|
|
||||||
if (typeof Chart === "undefined") {
|
|
||||||
var chartjs = document.createElement("script");
|
|
||||||
chartjs.src = "https://cdn.jsdelivr.net/npm/chart.js";
|
|
||||||
chartjs.onload = function() {
|
|
||||||
setupLogAnalyzer();
|
|
||||||
};
|
|
||||||
document.head.appendChild(chartjs);
|
|
||||||
} else {
|
|
||||||
setupLogAnalyzer();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupLogAnalyzer() {
|
|
||||||
// Add button to analyze current log
|
|
||||||
if ($("#analyzeLogBtn").length === 0) {
|
|
||||||
$("<button class='ui blue button' id='analyzeLogBtn' style='margin-bottom:1em;'>Analyze Current Log</button>")
|
|
||||||
.insertBefore("#analyzer .ui.segment");
|
|
||||||
}
|
}
|
||||||
$("#analyzeLogBtn").off("click").on("click", function() {
|
let html = `<div class="ui list">`;
|
||||||
let log = $("#logrender").val();
|
errors.forEach(function(err) {
|
||||||
if (!log || log.indexOf("Pick a log file") !== -1) {
|
let [timestamp, method, path, code] = err;
|
||||||
alert("Please open a log file first.");
|
html += `
|
||||||
return;
|
<div class="item">
|
||||||
}
|
<div class="ui ${code.startsWith('5') ? 'red' : 'yellow'} message" style="margin-bottom:0.7em;">
|
||||||
analyzeLog(log);
|
<div class="header">
|
||||||
|
<span style="font-family:monospace;">[${timestamp}]</span>
|
||||||
|
<span style="font-family:monospace;">${method}</span>
|
||||||
|
<span style="margin-left:1em;font-family:monospace;">${path}</span>
|
||||||
|
<span style="float:right;font-weight:bold;">${code}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
});
|
});
|
||||||
}
|
html += `</div>`;
|
||||||
|
$("#errorHighlightWrapper").html(html);
|
||||||
function analyzeLog(log) {
|
//Scroll to bottom of the error highlight
|
||||||
// Parse log lines
|
$("#errorHighlightWrapper").scrollTop($("#errorHighlightWrapper")[0].scrollHeight);
|
||||||
let lines = log.split("\n").filter(l => l.trim().length > 0 && l[0] === "[");
|
|
||||||
let reqTypeCount = {};
|
|
||||||
let originDayCount = {};
|
|
||||||
let clientIpCount = {};
|
|
||||||
|
|
||||||
let reqTypeRegex = /\] ([A-Z]+) /;
|
|
||||||
let originRegex = /\[origin:([^\]]+)\]/;
|
|
||||||
let dateRegex = /^\[([0-9\-]+) /;
|
|
||||||
let clientIpRegex = /\[client: ([^\]]+)\]/;
|
|
||||||
lines.forEach(line => {
|
|
||||||
// Request type
|
|
||||||
let reqTypeMatch = line.match(reqTypeRegex);
|
|
||||||
let reqType = reqTypeMatch ? reqTypeMatch[1] : "OTHER";
|
|
||||||
reqTypeCount[reqType] = (reqTypeCount[reqType] || 0) + 1;
|
|
||||||
|
|
||||||
// Origin
|
|
||||||
let originMatch = line.match(originRegex);
|
|
||||||
let origin = originMatch ? originMatch[1] : null;
|
|
||||||
|
|
||||||
// Date
|
|
||||||
let dateMatch = line.match(dateRegex);
|
|
||||||
let date = dateMatch ? dateMatch[1] : null;
|
|
||||||
|
|
||||||
// Skip if origin or date is unknown
|
|
||||||
if (!origin || !date) return;
|
|
||||||
|
|
||||||
// Per-origin per-day
|
|
||||||
if (!originDayCount[origin]) originDayCount[origin] = {};
|
|
||||||
originDayCount[origin][date] = (originDayCount[origin][date] || 0) + 1;
|
|
||||||
|
|
||||||
// Client IP
|
|
||||||
let ipMatch = line.match(clientIpRegex);
|
|
||||||
let ip = ipMatch ? ipMatch[1] : "unknown";
|
|
||||||
clientIpCount[ip] = (clientIpCount[ip] || 0) + 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Pie Chart: Request Type Ratio ---
|
|
||||||
let pieCtx = document.getElementById("requestTypePie").getContext("2d");
|
|
||||||
if (window.requestTypePieChart) window.requestTypePieChart.destroy();
|
|
||||||
window.requestTypePieChart = new Chart(pieCtx, {
|
|
||||||
type: "pie",
|
|
||||||
data: {
|
|
||||||
labels: Object.keys(reqTypeCount),
|
|
||||||
datasets: [{
|
|
||||||
data: Object.values(reqTypeCount),
|
|
||||||
backgroundColor: [
|
|
||||||
"#2185d0", "#21ba45", "#db2828", "#fbbd08", "#a333c8", "#00b5ad", "#b5cc18"
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
plugins: {
|
|
||||||
legend: { position: "bottom" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Histogram Charts: Requests per Host (Origin) per Day ---
|
|
||||||
let $originCharts = $("#originCharts");
|
|
||||||
$originCharts.html("");
|
|
||||||
Object.keys(originDayCount).forEach((origin, idx) => {
|
|
||||||
let chartId = "originChart_" + idx;
|
|
||||||
$originCharts.append(`<div style="margin-bottom:2em;"><b>${origin}</b><canvas id="${chartId}" height="120"></canvas></div>`);
|
|
||||||
let dayCounts = originDayCount[origin];
|
|
||||||
let days = Object.keys(dayCounts).sort();
|
|
||||||
let counts = days.map(d => dayCounts[d]);
|
|
||||||
let ctx = document.getElementById(chartId).getContext("2d");
|
|
||||||
new Chart(ctx, {
|
|
||||||
type: "bar",
|
|
||||||
data: {
|
|
||||||
labels: days,
|
|
||||||
datasets: [{
|
|
||||||
label: "Requests",
|
|
||||||
data: counts,
|
|
||||||
backgroundColor: "#2185d0"
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false }
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
title: { display: true, text: "Date" },
|
|
||||||
grid: { display: false }
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
title: { display: true, text: "Requests" },
|
|
||||||
beginAtZero: true,
|
|
||||||
grid: { display: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Table: Client IPs ---
|
|
||||||
let $tbody = $("#clientIpTable tbody");
|
|
||||||
$tbody.html("");
|
|
||||||
Object.entries(clientIpCount)
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.forEach(([ip, count]) => {
|
|
||||||
$tbody.append(`<tr><td>${ip}</td><td>${count}</td></tr>`);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
Reference in New Issue
Block a user