mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-09-11 22:59:36 +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 := 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 /, ,$@)
|
||||
os = $(word 1, $(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/read", LogViewer.HandleReadLog)
|
||||
authRouter.HandleFunc("/api/log/summary", LogViewer.HandleReadLogSummary)
|
||||
|
||||
authRouter.HandleFunc("/api/log/errors", LogViewer.HandleLogErrorSummary)
|
||||
//Debug
|
||||
authRouter.HandleFunc("/api/info/pprof", pprof.Index)
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ type Logger struct {
|
||||
Prefix string //Prefix for log files
|
||||
LogFolder string //Folder to store the log file
|
||||
CurrentLogFile string //Current writing filename
|
||||
RotateOption RotateOption //Options for log rotation, see rotate.go
|
||||
logger *log.Logger
|
||||
file *os.File
|
||||
}
|
||||
|
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)
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -322,104 +368,3 @@ func (v *Viewer) LoadLogSummary(filename string) (string, error) {
|
||||
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/semantic/semantic.min.js"></script>
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<link rel="stylesheet" href="darktheme.css">
|
||||
<style>
|
||||
.clickable{
|
||||
cursor: pointer;
|
||||
@@ -70,59 +69,16 @@
|
||||
color:white;
|
||||
}
|
||||
|
||||
body.darkTheme .loglist {
|
||||
background-color: #1b1c1d;
|
||||
color: #ffffff;
|
||||
#errorHighlightWrapper{
|
||||
background-color: #f9f9f9;
|
||||
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) {
|
||||
#logfileDropdown {
|
||||
margin-left: 0.4em !important;
|
||||
@@ -136,23 +92,35 @@
|
||||
margin-left: 0.4em !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
#toggleFullscreenBtn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#logrenderWrapper{
|
||||
margin-left: 0em !important;
|
||||
margin-right: 0em !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Currently not darktheme ready -->
|
||||
<link rel="stylesheet" href="../darktheme.css">
|
||||
<script src="../script/darktheme.js"></script>
|
||||
<!-- <script src="../script/darktheme.js"></script> -->
|
||||
<br>
|
||||
<div class="ui container">
|
||||
<div class="ui stackable secondary menu">
|
||||
<div class="item" style="font-weight: bold;">
|
||||
Zoraxy LogView
|
||||
</div>
|
||||
<a class="item active panel_menu_btn" id="dashboardMenu">
|
||||
Dashboard
|
||||
</a>
|
||||
<a class="item panel_menu_btn" id="logViewMenu">
|
||||
<a class="item active panel_menu_btn" id="logViewMenu">
|
||||
Log View
|
||||
</a>
|
||||
<a class="item panel_menu_btn" id="summaryMenu">
|
||||
Summary
|
||||
</a>
|
||||
<a class="item panel_menu_btn" id="settingsMenu">
|
||||
Settings
|
||||
</a>
|
||||
@@ -194,33 +162,84 @@
|
||||
</div>
|
||||
</div>
|
||||
<br><br>
|
||||
<!-- Dashboard View -->
|
||||
<div id="dashboard" class="ui container subpanel">
|
||||
<!-- Summary View -->
|
||||
<div id="summary" class="ui container subpanel">
|
||||
<h3 class="ui header">
|
||||
Dashboard
|
||||
Summary
|
||||
</h3>
|
||||
<div class="ui divider"></div>
|
||||
<p>Welcome to LogVPro! Use the left menu to select a log file </p>
|
||||
<div id="analyzer">
|
||||
<div class="ui small statistics" style="display: flex; justify-content: center;">
|
||||
<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>
|
||||
|
||||
<!-- Log Viewer -->
|
||||
<div id="logviewer" class="ui container subpanel" style="display:none;">
|
||||
<div id="logviewer" class="subpanel" style="display:none;">
|
||||
<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
|
||||
</textarea>
|
||||
<br><br>
|
||||
<!-- Full Screen Button -->
|
||||
<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;">
|
||||
<i class="expand arrows alternate icon"></i>
|
||||
</button>
|
||||
<!-- Scroll to Bottom Button -->
|
||||
<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 10 seconds</small></label>
|
||||
<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>
|
||||
|
||||
<!-- Settings -->
|
||||
<div id="settings" class="ui container subpanel" style="display:none;">
|
||||
@@ -247,14 +266,14 @@ Pick a log file from the menu to start debugging
|
||||
|
||||
/* Menu Subpanel Switch */
|
||||
$(".subpanel").hide();
|
||||
$("#dashboard").show();
|
||||
$("#logviewer").show();
|
||||
$(".panel_menu_btn").on("click", function() {
|
||||
var id = $(this).attr("id");
|
||||
$(".subpanel").hide();
|
||||
$(".ui.menu .item").removeClass("active");
|
||||
$(this).addClass("active");
|
||||
if (id === "dashboardMenu") {
|
||||
$("#dashboard").show();
|
||||
if (id === "summaryMenu") {
|
||||
$("#summary").show();
|
||||
} else if (id === "logViewMenu") {
|
||||
$("#logviewer").show();
|
||||
} else if (id === "settingsMenu") {
|
||||
@@ -276,12 +295,18 @@ 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");
|
||||
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"){
|
||||
$(".logfile.active").removeClass('active');
|
||||
@@ -347,48 +564,13 @@ Pick a log file from the menu to start debugging
|
||||
renderLogWithCurrentFilter(data);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
|
||||
function openLogInNewTab(){
|
||||
if (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){
|
||||
var units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
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;
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
setInterval(function(){
|
||||
if (autoscroll){
|
||||
@@ -439,174 +601,42 @@ Pick a log file from the menu to start debugging
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 10000);
|
||||
}, 3000);
|
||||
function handleAutoScrollTicker(event, checked){
|
||||
autoscroll = checked;
|
||||
}
|
||||
|
||||
|
||||
/* Log analyzer */
|
||||
/* --- Log Analyzer UI --- */
|
||||
$("#analyzer").html(`
|
||||
<div class="ui segment">
|
||||
<h4 class="ui header">Log Analytics</h4>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui grid">
|
||||
<div class="eight wide column">
|
||||
<canvas id="requestTypePie"></canvas>
|
||||
</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 log = $("#logrender").val();
|
||||
if (!log || log.indexOf("Pick a log file") !== -1) {
|
||||
alert("Please open a log file first.");
|
||||
// Error Highlights Rendering
|
||||
function renderErrorHighlights(errors) {
|
||||
if (!Array.isArray(errors) || errors.length === 0) {
|
||||
$("#errorHighlightWrapper").html(
|
||||
`<div class="ui positive message">
|
||||
<div class="header">No errors found</div>
|
||||
<p>This log file contains no errors.</p>
|
||||
</div>`
|
||||
);
|
||||
return;
|
||||
}
|
||||
analyzeLog(log);
|
||||
});
|
||||
}
|
||||
|
||||
function analyzeLog(log) {
|
||||
// Parse log lines
|
||||
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>`);
|
||||
let html = `<div class="ui list">`;
|
||||
errors.forEach(function(err) {
|
||||
let [timestamp, method, path, code] = err;
|
||||
html += `
|
||||
<div class="item">
|
||||
<div class="ui ${code.startsWith('5') ? 'red' : 'yellow'} message" style="margin-bottom:0.7em;">
|
||||
<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);
|
||||
//Scroll to bottom of the error highlight
|
||||
$("#errorHighlightWrapper").scrollTop($("#errorHighlightWrapper")[0].scrollHeight);
|
||||
}
|
||||
</script>
|
||||
</html>
|
Reference in New Issue
Block a user