Skip to content

Commit f937e01

Browse files
author
mirkobrombin
committed
feat: add logs API and dashboard integration for log exploration
1 parent afe4787 commit f937e01

File tree

10 files changed

+383
-21
lines changed

10 files changed

+383
-21
lines changed

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/quic-go/quic-go v0.48.2
1010
github.com/rivo/tview v0.0.0-20241103174730-c76f7879f592
1111
github.com/rs/zerolog v1.33.0
12+
github.com/shirou/gopsutil v3.21.11+incompatible
1213
github.com/spf13/cobra v1.8.1
1314
github.com/valyala/fasthttp v1.58.0
1415
github.com/yookoala/gofast v0.8.0
@@ -19,6 +20,7 @@ require (
1920
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2021
github.com/gdamore/encoding v1.0.0 // indirect
2122
github.com/gdamore/tcell/v2 v2.7.1 // indirect
23+
github.com/go-ole/go-ole v1.2.6 // indirect
2224
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
2325
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
2426
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -31,7 +33,10 @@ require (
3133
github.com/quic-go/qpack v0.5.1 // indirect
3234
github.com/rivo/uniseg v0.4.7 // indirect
3335
github.com/spf13/pflag v1.0.5 // indirect
36+
github.com/tklauser/go-sysconf v0.3.14 // indirect
37+
github.com/tklauser/numcpus v0.8.0 // indirect
3438
github.com/valyala/bytebufferpool v1.0.0 // indirect
39+
github.com/yusufpapurcu/wmi v1.2.4 // indirect
3540
go.uber.org/mock v0.4.0 // indirect
3641
golang.org/x/crypto v0.31.0 // indirect
3742
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect

go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ github.com/gdamore/tcell/v2 v2.7.1 h1:TiCcmpWHiAU7F0rA2I3S2Y4mmLmO9KHxJ7E1QhYzQb
1818
github.com/gdamore/tcell/v2 v2.7.1/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
1919
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
2020
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
21+
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
22+
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
2123
github.com/go-restit/lzjson v0.0.0-20161206095556-efe3c53acc68/go.mod h1:7vXSKQt83WmbPeyVjCfNT9YDJ5BUFmcwFsEjI9SCvYM=
2224
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
2325
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
@@ -70,6 +72,8 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
7072
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
7173
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
7274
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
75+
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
76+
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
7377
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
7478
github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
7579
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -81,6 +85,10 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
8185
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8286
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
8387
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
88+
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
89+
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
90+
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
91+
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
8492
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
8593
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
8694
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
@@ -91,6 +99,8 @@ github.com/yookoala/gofast v0.8.0 h1:UmGTeBj2EF5gvS58ByE9HFdQ9MeYSUIwf7JN9aFno3Y
9199
github.com/yookoala/gofast v0.8.0/go.mod h1:OJU201Q6HCaE1cASckaTbMm3KB6e0cZxK0mgqfwOKvQ=
92100
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
93101
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
102+
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
103+
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
94104
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
95105
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
96106
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -123,6 +133,7 @@ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
123133
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
124134
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
125135
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
136+
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
126137
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
127138
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
128139
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

internal/api/logs_explorer.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// file: internal/api/logs_explorer.go
2+
package api
3+
4+
import (
5+
"io/fs"
6+
"io/ioutil"
7+
"net/http"
8+
"os"
9+
"path/filepath"
10+
"strconv"
11+
"strings"
12+
"time"
13+
14+
"github.com/gorilla/mux"
15+
"github.com/mirkobrombin/goup/internal/config"
16+
)
17+
18+
// LogFileInfo holds data about a single log file.
19+
type LogFileInfo struct {
20+
Domain string `json:"domain"`
21+
Plugin string `json:"plugin,omitempty"`
22+
Year int `json:"year"`
23+
Month int `json:"month"`
24+
Day int `json:"day"`
25+
FileName string `json:"file_name"`
26+
SizeBytes int64 `json:"size_bytes"`
27+
ModTime int64 `json:"mod_time_unix"`
28+
}
29+
30+
// GET /api/logfiles?start=YYYY-MM-DD&end=YYYY-MM-DD&plugin=somePlugin
31+
// Lists all log files, optionally filtered by date range or plugin name.
32+
func listLogFilesHandler(w http.ResponseWriter, r *http.Request) {
33+
startStr := r.URL.Query().Get("start") // YYYY-MM-DD
34+
endStr := r.URL.Query().Get("end") // YYYY-MM-DD
35+
pluginQ := r.URL.Query().Get("plugin") // plugin name
36+
37+
var startTime, endTime time.Time
38+
var err error
39+
40+
if startStr != "" {
41+
startTime, err = time.Parse("2006-01-02", startStr)
42+
if err != nil {
43+
http.Error(w, "Invalid 'start' date format (expected YYYY-MM-DD)", http.StatusBadRequest)
44+
return
45+
}
46+
}
47+
if endStr != "" {
48+
endTime, err = time.Parse("2006-01-02", endStr)
49+
if err != nil {
50+
http.Error(w, "Invalid 'end' date format (expected YYYY-MM-DD)", http.StatusBadRequest)
51+
return
52+
}
53+
}
54+
55+
rootDir := config.GetLogDir()
56+
var results []LogFileInfo
57+
58+
_ = filepath.Walk(rootDir, func(path string, info fs.FileInfo, walkErr error) error {
59+
if walkErr != nil || info.IsDir() {
60+
return walkErr
61+
}
62+
rel, _ := filepath.Rel(rootDir, path)
63+
parts := strings.Split(rel, string(os.PathSeparator))
64+
if len(parts) != 4 {
65+
return nil
66+
}
67+
domain := parts[0]
68+
yearStr := parts[1]
69+
monthStr := parts[2]
70+
fileName := parts[3] // e.g. "05.log" or "05-something.log"
71+
72+
year, yErr := strconv.Atoi(yearStr)
73+
month, mErr := strconv.Atoi(monthStr)
74+
if yErr != nil || mErr != nil {
75+
return nil
76+
}
77+
78+
// Parse day and optional plugin from the file name
79+
// e.g. "05.log" -> day=05, plugin=""
80+
// or "05-MyPlugin.log" -> day=05, plugin="MyPlugin"
81+
dayStr, pluginName := parseDayAndPlugin(fileName)
82+
day, dErr := strconv.Atoi(dayStr)
83+
if dErr != nil {
84+
return nil
85+
}
86+
87+
fileDate := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
88+
89+
// Filter by date if set
90+
if !startTime.IsZero() && fileDate.Before(startTime) {
91+
return nil
92+
}
93+
if !endTime.IsZero() && fileDate.After(endTime) {
94+
return nil
95+
}
96+
if pluginQ != "" && pluginName != pluginQ {
97+
return nil
98+
}
99+
100+
results = append(results, LogFileInfo{
101+
Domain: domain,
102+
Plugin: pluginName,
103+
Year: year,
104+
Month: month,
105+
Day: day,
106+
FileName: rel,
107+
SizeBytes: info.Size(),
108+
ModTime: info.ModTime().Unix(),
109+
})
110+
return nil
111+
})
112+
113+
jsonResponse(w, results)
114+
}
115+
116+
// GET /api/logfiles/{fileName}
117+
// Returns the content of a log file.
118+
func getLogFileHandler(w http.ResponseWriter, r *http.Request) {
119+
vars := mux.Vars(r)
120+
fileName := vars["fileName"]
121+
122+
if fileName == "" {
123+
http.Error(w, "fileName is required", http.StatusBadRequest)
124+
return
125+
}
126+
fullPath := filepath.Join(config.GetLogDir(), fileName)
127+
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
128+
http.Error(w, "Log file not found", http.StatusNotFound)
129+
return
130+
}
131+
data, err := ioutil.ReadFile(fullPath)
132+
if err != nil {
133+
http.Error(w, "Failed to read log file", http.StatusInternalServerError)
134+
return
135+
}
136+
w.Header().Set("Content-Type", "text/plain")
137+
w.Write(data)
138+
}
139+
140+
// parseDayAndPlugin extracts the day and optional plugin name from a log
141+
// file name.
142+
func parseDayAndPlugin(fileName string) (string, string) {
143+
base := strings.TrimSuffix(fileName, ".log")
144+
parts := strings.SplitN(base, "-", 2)
145+
if len(parts) == 1 {
146+
return parts[0], ""
147+
}
148+
return parts[0], parts[1]
149+
}

internal/api/logs_metrics_status.go

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,52 @@ import (
44
"io/fs"
55
"io/ioutil"
66
"net/http"
7-
"os"
87
"path/filepath"
8+
"sync/atomic"
9+
"time"
910

1011
"github.com/mirkobrombin/goup/internal/config"
12+
"github.com/shirou/gopsutil/cpu"
13+
"github.com/shirou/gopsutil/mem"
1114
)
1215

16+
var startTime = time.Now()
17+
var requestsTotal uint64
18+
1319
func getLogsHandler(w http.ResponseWriter, r *http.Request) {
14-
logFile := os.Getenv("GOUP_LOG_FILE")
15-
if logFile == "" {
16-
http.Error(w, "Log file not set", http.StatusNotFound)
17-
return
18-
}
19-
data, err := ioutil.ReadFile(logFile)
20+
logDir := config.GetLogDir()
21+
var logs []byte
22+
err := filepath.Walk(logDir, func(path string, info fs.FileInfo, err error) error {
23+
if err != nil {
24+
return err
25+
}
26+
if !info.IsDir() {
27+
data, readErr := ioutil.ReadFile(path)
28+
if readErr != nil {
29+
return readErr
30+
}
31+
logs = append(logs, data...)
32+
logs = append(logs, '\n')
33+
}
34+
return nil
35+
})
2036
if err != nil {
2137
http.Error(w, "Unable to read log file", http.StatusInternalServerError)
2238
return
2339
}
2440
w.Header().Set("Content-Type", "text/plain")
25-
w.Write(data)
41+
w.Write(logs)
2642
}
2743

2844
func getMetricsHandler(w http.ResponseWriter, r *http.Request) {
45+
atomic.AddUint64(&requestsTotal, 1)
46+
cpuPercent, _ := cpu.Percent(0, false)
47+
vm, _ := mem.VirtualMemory()
2948
metrics := map[string]interface{}{
30-
"requests_total": 1234,
31-
"latency_avg_ms": 45.6,
32-
"cpu_usage": 23.4,
33-
"ram_usage_mb": 512,
49+
"requests_total": atomic.LoadUint64(&requestsTotal),
50+
"latency_avg_ms": 0,
51+
"cpu_usage": cpuPercent,
52+
"ram_usage_mb": vm.Used / 1024 / 1024,
3453
"active_sites": len(config.SiteConfigs),
3554
"active_plugins": len(config.GlobalConf.EnabledPlugins),
3655
}
@@ -39,7 +58,7 @@ func getMetricsHandler(w http.ResponseWriter, r *http.Request) {
3958

4059
func getStatusHandler(w http.ResponseWriter, r *http.Request) {
4160
status := map[string]interface{}{
42-
"uptime": "72h",
61+
"uptime": time.Since(startTime).String(),
4362
"sites": len(config.SiteConfigs),
4463
"plugins": config.GlobalConf.EnabledPlugins,
4564
"apiAlive": true,

internal/api/router.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ func SetupRoutes() *mux.Router {
3030
r.HandleFunc("/api/logweight", getLogWeightHandler).Methods("GET")
3131
r.HandleFunc("/api/pluginusage", getPluginUsageHandler).Methods("GET")
3232

33+
// Log files
34+
r.HandleFunc("/api/logfiles", listLogFilesHandler).Methods("GET")
35+
r.HandleFunc("/api/logfiles/{fileName:.*}", getLogFileHandler).Methods("GET")
36+
3337
// Restart
3438
r.HandleFunc("/api/restart", restartHandler).Methods("POST")
3539

internal/dashboard/static/entry.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Plugins from "./js/plugins.js";
55
import Sites from "./js/sites.js";
66
import Tools from "./js/tools.js";
77
import Search from "./js/search.js";
8+
import Logs from "./js/logs.js";
89

910
const router = new Navigo("/", { hash: false });
1011

@@ -21,6 +22,7 @@ router
2122
"/sites": () => render(Sites),
2223
"/config": () => render(Config),
2324
"/tools": () => render(Tools),
25+
"/logs": () => render(Logs),
2426
})
2527
.resolve();
2628

internal/dashboard/static/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ <h1 class="text-xl font-bold text-gray-700">Dashboard</h1>
2222
</div>
2323

2424
<div class="relative flex-1 mx-8">
25-
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"> search
26-
</span>
25+
<span
26+
class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">search</span>
2727
<input id="globalSearch" type="text" placeholder="Search anything..."
2828
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-full focus:outline-none focus:ring-2 focus:ring-cyan-500" />
2929
</div>
@@ -35,6 +35,7 @@ <h1 class="text-xl font-bold text-gray-700">Dashboard</h1>
3535
<a href="/sites" data-navigo class="text-gray-600 hover:text-cyan-600">Sites</a>
3636
<a href="/config" data-navigo class="text-gray-600 hover:text-cyan-600">Config</a>
3737
<a href="/tools" data-navigo class="text-gray-600 hover:text-cyan-600">Tools</a>
38+
<a href="/logs" data-navigo class="text-gray-600 hover:text-cyan-600">Logs</a> <!-- new link -->
3839
</nav>
3940
</div>
4041
</header>

0 commit comments

Comments
 (0)