diff --git a/src/Makefile b/src/Makefile index aa9b2c0..debae3c 100644 --- a/src/Makefile +++ b/src/Makefile @@ -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)) diff --git a/src/api.go b/src/api.go index e98f7fb..ea6dbc9 100644 --- a/src/api.go +++ b/src/api.go @@ -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) } diff --git a/src/mod/info/logger/logger.go b/src/mod/info/logger/logger.go index d71f97e..06b0e45 100644 --- a/src/mod/info/logger/logger.go +++ b/src/mod/info/logger/logger.go @@ -18,9 +18,10 @@ import ( */ type Logger struct { - Prefix string //Prefix for log files - LogFolder string //Folder to store the log file - CurrentLogFile string //Current writing filename + 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 } diff --git a/src/mod/info/logger/rotate.go b/src/mod/info/logger/rotate.go new file mode 100644 index 0000000..c27edcb --- /dev/null +++ b/src/mod/info/logger/rotate.go @@ -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 +} diff --git a/src/mod/info/logviewer/logviewer.go b/src/mod/info/logviewer/logviewer.go index f17d285..fb62c14 100644 --- a/src/mod/info/logviewer/logviewer.go +++ b/src/mod/info/logviewer/logviewer.go @@ -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 - -*/ diff --git a/src/web/snippet/logview.html b/src/web/snippet/logview.html index 0cfd8fe..17ffe1c 100644 --- a/src/web/snippet/logview.html +++ b/src/web/snippet/logview.html @@ -11,7 +11,6 @@ - + - +


- -
+ +

- Dashboard + Summary

-

Welcome to LogVPro! Use the left menu to select a log file

-
+
+
+
+
Success Requests
+
+
+
+
Error Requests
+
+
+
+
Total Requests
+
+
+
+ +

Error Highlights

+
+

No error data loaded

+
+
+
+
+
+ No log file selected +
+

Please select a log file from the dropdown menu to view analysis and summary.

+
- @@ -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("
Error

" + data.error + "

"); + 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( + `
+
Failed to load error highlights
+
` + ); + }); + + } + + function renderSummaryToHTML(data){ + /* Render summary analysis to #analyzer */ + let html = ` +
+
+

Request Methods

+ +
+
+

Hits Per Day

+ +
+
+
+
+
+

Hits Per Site

+
+
+
+
+
+
+

Unique IPs

+ + + + ${Object.entries(data.unique_ips) + .sort((a, b) => b[1] - a[1]) + .map(([ip, count]) => ``) + .join("")} + +
IPRequests
${ip}${count}
+
+
+

Top User Agents

+ + + + ${Object.entries(data.top_user_agents) + .sort((a, b) => b[1] - a[1]) + .map(([ua, count]) => ``) + .join("")} + +
User AgentRequests
${ua}${count}
+
+
+
+
+
+

Top Paths

+ + + + ${Object.entries(data.top_paths) + .sort((a, b) => b[1] - a[1]) + .map(([path, count]) => ``) + .join("")} + +
PathRequests
${path}${count}
+
+
+ `; + + $("#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(` +
+ ${site} + +
+ `); + // 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); }); } - - \ No newline at end of file