Added wip new log viewer

- Added DNS challenge maintainer tag
- Added wip log viewer
This commit is contained in:
Toby Chui
2025-08-31 11:17:07 +08:00
parent 471e94c893
commit 7c3a1a9cfc
4 changed files with 628 additions and 1 deletions

View File

@@ -198,6 +198,12 @@ Some section of Zoraxy are contributed by our amazing community and if you have
- (Legacy) Authentik Support added by [@JokerQyou](https://github.com/JokerQyou)
- ACME
- ACME integration (Looking for maintainer)
- DNS Challenge by [@zen8841](https://github.com/zen8841)
- Docker Container List by [@eyerrock](https://github.com/eyerrock)
- Stream Proxy [@jemmy1794](https://github.com/jemmy1794)

View File

@@ -382,6 +382,7 @@ func initAPIs(targetMux *http.ServeMux) {
authRouter.HandleFunc("/api/conf/import", ImportConfigFromZip)
authRouter.HandleFunc("/api/log/list", LogViewer.HandleListLog)
authRouter.HandleFunc("/api/log/read", LogViewer.HandleReadLog)
authRouter.HandleFunc("/api/log/summary", LogViewer.HandleReadLogSummary)
//Debug
authRouter.HandleFunc("/api/info/pprof", pprof.Index)

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"imuslab.com/zoraxy/mod/utils"
@@ -21,6 +22,21 @@ type Viewer struct {
option *ViewerOption
}
type LogSummary struct {
TotalReqests int64 `json:"total_requests"`
TotalValid int64 `json:"total_valid"`
TotalErrors int64 `json:"total_errors"`
LogSource string `json:"log_source"`
RequestMethods map[string]int64 `json:"request_methods"` //Request methods (key: method, value: hit count)
HitPerDay map[string]int64 `json:"hit_per_day"` //Total hit count per day (key: date, value: hit count)
HiPerSite map[string][]int64 `json:"hit_per_site"` //origin to hit count per day (key: origin, value: []int64{hit count per day})
UniqueIPs map[string]int64 `json:"unique_ips"` //Unique IPs per day (key: date, value: unique IP count)
TopOrigins map[string]int64 `json:"top_origins"` //Top origins (key: origin, value: hit count)
TopUserAgents map[string]int64 `json:"top_user_agents"` //Top user agents (key: user agent, value: hit count)
TopPaths map[string]int64 `json:"top_paths"` //Top paths (key: path, value: hit count)
TotalSize int64 `json:"total_size"` //Total size of the log file
}
type LogFile struct {
Title string
Filename string
@@ -51,15 +67,65 @@ func (v *Viewer) HandleReadLog(w http.ResponseWriter, r *http.Request) {
return
}
filter, err := utils.GetPara(r, "filter")
if err != nil {
filter = ""
}
content, err := v.LoadLogFile(strings.TrimSpace(filepath.Base(filename)))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
//If filter is given, only return lines that contains the filter string
if filter != "" {
lines := strings.Split(content, "\n")
filteredLines := []string{}
for _, line := range lines {
switch filter {
case "error":
if strings.Contains(line, ":error]") {
filteredLines = append(filteredLines, line)
}
case "request":
if strings.Contains(line, "[router:") {
filteredLines = append(filteredLines, line)
}
case "system":
if strings.Contains(line, "[system:") {
filteredLines = append(filteredLines, line)
}
case "all":
filteredLines = append(filteredLines, line)
default:
if strings.Contains(line, filter) {
filteredLines = append(filteredLines, line)
}
}
}
content = strings.Join(filteredLines, "\n")
}
utils.SendTextResponse(w, content)
}
func (v *Viewer) HandleReadLogSummary(w http.ResponseWriter, r *http.Request) {
filename, err := utils.GetPara(r, "file")
if err != nil {
utils.SendErrorResponse(w, "invalid filename given")
return
}
summary, err := v.LoadLogSummary(strings.TrimSpace(filepath.Base(filename)))
if err != nil {
utils.SendErrorResponse(w, err.Error())
return
}
utils.SendJSONResponse(w, summary)
}
/*
Log Access Functions
*/
@@ -116,3 +182,244 @@ func (v *Viewer) LoadLogFile(filename string) (string, error) {
return "", errors.New("log file not found")
}
}
func (v *Viewer) LoadLogSummary(filename string) (string, error) {
filename = filepath.ToSlash(filename)
filename = strings.ReplaceAll(filename, "../", "")
logFilepath := filepath.Join(v.option.RootFolder, filename)
if utils.FileExists(logFilepath) {
//Load it
content, err := os.ReadFile(logFilepath)
if err != nil {
return "", err
}
var summary LogSummary
summary.LogSource = filepath.Base(filename)
summary.TotalSize = int64(len(content))
summary.RequestMethods = map[string]int64{}
summary.HitPerDay = map[string]int64{} // Initialize to avoid nil map error
summary.HiPerSite = map[string][]int64{}
summary.UniqueIPs = map[string]int64{}
summary.TopOrigins = map[string]int64{}
summary.TopUserAgents = map[string]int64{}
summary.TopPaths = map[string]int64{}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.TrimSpace(line) == "" {
continue // Skip empty lines
}
if !strings.Contains(line, "[router:") {
continue // Only process router: type logs
}
summary.TotalReqests++
// Extract the date from the log line
parts := strings.Split(line, "]")
if len(parts) < 2 {
continue // Skip malformed lines
}
datePart := strings.TrimSpace(parts[0][1:]) // Remove the leading '['
date := datePart[:10] // Get the date part (YYYY-MM-DD)
// Increment hit count for the day
summary.HitPerDay[date]++
// Extract origin, user agent, and path
origin := ""
userAgent := ""
path := ""
method := ""
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "[origin:") {
origin = strings.TrimPrefix(part, "[origin:")
origin = strings.TrimSuffix(origin, "]")
} else if strings.HasPrefix(part, "[useragent:") {
userAgent = strings.TrimPrefix(part, "[useragent:")
userAgent = strings.TrimSuffix(userAgent, "]")
} else if !strings.HasPrefix(part, "[") && !strings.HasSuffix(part, "]") && method == "" {
// This is likely the HTTP method (GET, POST, etc.)
fields := strings.Fields(part)
if len(fields) > 0 {
method = fields[0]
summary.RequestMethods[method]++
if len(fields) > 1 {
path = fields[1] // The path is the second field
}
}
}
}
if origin != "" {
if _, exists := summary.HiPerSite[origin]; !exists {
summary.HiPerSite[origin] = make([]int64, 32) // Initialize for 31 days
}
//Get the day of month from date
dayIndex := 0
if len(date) >= 10 {
dayStr := date[8:10] // Get the day part (DD)
dayIndex, _ = strconv.Atoi(dayStr) // Convert to integer
}
if dayIndex < 1 || dayIndex > 31 {
dayIndex = 0 // Default to 0 if out of range
}
summary.HiPerSite[origin][dayIndex-1]++ // Increment hit count for the specific day
summary.HitPerDay[date]++ // Increment total hit count for the date
}
if userAgent != "" {
summary.TopUserAgents[userAgent]++
}
if path != "" {
if idx := strings.IndexAny(path, "?#"); idx != -1 {
path = path[:idx]
}
summary.TopPaths[path]++
}
// Increment unique IPs (assuming IP is the first part of the line)
ipPart := strings.Split(line, "[client:")[1]
if ipPart != "" {
ip := strings.TrimSpace(strings.Split(ipPart, "]")[0])
if _, exists := summary.UniqueIPs[ip]; !exists {
summary.UniqueIPs[ip] = 0
}
summary.UniqueIPs[ip]++ // Increment unique IP count for the day
}
// Check for errors: count if status code is not 1xx or 2xx
statusParts := strings.Fields(line)
if len(statusParts) > 0 {
statusStr := statusParts[len(statusParts)-1]
if len(statusStr) == 3 {
if statusCode := statusStr[0]; statusCode != '1' && statusCode != '2' {
summary.TotalErrors++
} else {
summary.TotalValid++
}
}
}
}
js, err := json.Marshal(summary)
if err != nil {
return "", err
}
return string(js), nil
} else {
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
*/

View File

@@ -107,6 +107,13 @@
body.darkTheme .loglist small {
color: #bbbbbb;
}
@media (min-width: 768px) {
#logfileDropdown {
margin-left: 0.4em !important;
}
}
</style>
</head>
@@ -114,6 +121,76 @@
<link rel="stylesheet" href="../darktheme.css">
<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 LogVPro
</div>
<a class="item active panel_menu_btn" id="dashboardMenu">
Dashboard
</a>
<a class="item panel_menu_btn" id="logViewMenu">
Log View
</a>
<a class="item panel_menu_btn" id="settingsMenu">
Settings
</a>
</div>
</div>
<div class="ui container">
<div class="ui divider"></div>
<div class="ui stackable secondary menu">
<!-- Filter Dropdown -->
<div class="ui selection dropdown" id="filterDropdown" style="margin-top:0.4em;">
<div class="text">Select Filter</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" data-value="all"><i class="filter icon"></i> All</div>
<div class="item" data-value="system"><i class="blue info circle icon"></i> System</div>
<div class="item" data-value="request"><i class="green exchange icon"></i> Request</div>
<div class="item" data-value="error"><i class="red exclamation triangle icon"></i> Error</div>
</div>
</div>
<!-- Log File Dropdown -->
<div class="ui selection dropdown" id="logfileDropdown" style="margin-top:0.4em;">
<div class="text">Select Log File</div>
<i class="dropdown icon"></i>
<div class="menu" id="logfileDropdownMenu">
<!-- Log files will be populated here -->
</div>
</div>
</div>
</div>
<br><br>
<!-- Dashboard View -->
<div id="dashboard" class="ui container subpanel">
<h3 class="ui header">
Dashboard
</h3>
<div class="ui divider"></div>
<p>Welcome to LogVPro! Use the left menu to select a log file </p>
<div id="analyzer">
</div>
</div>
<!-- Log Viewer -->
<div id="logviewer" class="ui container subpanel" style="display:none;">
</div>
<!-- Settings -->
<div id="settings" class="ui container subpanel" style="display:none;">
<h3 class="ui header">
Settings
</h3>
</div>
<br><br>
<div class="ui container">
<div class="ui stackable grid">
<div class="four wide column loglist">
@@ -152,10 +229,80 @@
</div>
</div>
<br>
</body>
<script>
var currentOpenedLogURL = "";
//LogVPro Implemnetation
var currentFilter = "all";
var currentOpenedLogURL = "";
var currentLogFile = "";
/* Menu Subpanel Switch */
$(".subpanel").hide();
$("#dashboard").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();
} else if (id === "logViewMenu") {
$("#logviewer").show();
} else if (id === "settingsMenu") {
$("#settings").show();
}
});
/* Log file dropdown */
function populateLogfileDropdown() {
$.get("/api/log/list", function(data){
let $menu = $("#logfileDropdownMenu");
$menu.html("");
for (let [key, value] of Object.entries(data)) {
value.reverse();
value.forEach(file => {
$menu.append(
`<div class="item" data-value="${file.Filename}" data-category="${key}">${file.Title} (${formatBytes(file.Filesize)})</div>`
);
});
}
$('#logfileDropdown').dropdown('refresh');
});
}
$('#logfileDropdown').dropdown({
onChange: function(value, text, $choice) {
if (value) {
openLog(null, $choice.data('category'), value);
}
}
});
populateLogfileDropdown();
/* Filter dropdown */
$('#filterDropdown').dropdown({
onChange: function(value) {
currentFilter = value;
$(".filterbtn.active").removeClass("active");
$(`.filterbtn[filter="${value}"]`).addClass("active");
if (currentOpenedLogURL != "") {
$.get(currentOpenedLogURL, function(data){
if (data.error !== undefined){
alert(data.error);
return;
}
renderLogWithCurrentFilter(data);
});
}
}
});
// Set default filter to "error"
$('#filterDropdown').dropdown('set selected', 'error');
currentFilter = "error";
</script>
<script>
var autoscroll = false;
$(".checkbox").checkbox();
@@ -169,6 +316,7 @@
function openLog(object, catergory, filename){
$(".logfile.active").removeClass('active');
$(object).addClass("active");
currentLogFile = filename;
currentOpenedLogURL = "/api/log/read?file=" + filename;
$.get(currentOpenedLogURL, function(data){
if (data.error !== undefined){
@@ -283,5 +431,170 @@
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.");
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>`);
});
}
</script>
</html>