Skip to content

Commit 05043bc

Browse files
committed
feat: capture and show server logs
1 parent 0d2baa0 commit 05043bc

File tree

8 files changed

+664
-0
lines changed

8 files changed

+664
-0
lines changed

cmd/godns/godns.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/TimothyYe/godns/internal/manager"
1111
"github.com/TimothyYe/godns/internal/settings"
1212
"github.com/TimothyYe/godns/internal/utils"
13+
"github.com/TimothyYe/godns/pkg/lib"
1314

1415
log "github.com/sirupsen/logrus"
1516

@@ -53,6 +54,12 @@ func main() {
5354
log.Fatal(err)
5455
}
5556

57+
// Initialize log buffer for web interface
58+
lib.InitLogBuffer(1000) // Store last 1000 log entries
59+
logBuffer := lib.GetLogBuffer()
60+
logHook := lib.NewLogHook(logBuffer)
61+
log.AddHook(logHook)
62+
5663
// set the log level
5764
log.SetOutput(os.Stdout)
5865

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package controllers
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/TimothyYe/godns/pkg/lib"
7+
"github.com/gofiber/fiber/v2"
8+
)
9+
10+
// GetLogs returns log entries.
11+
func (c *Controller) GetLogs(ctx *fiber.Ctx) error {
12+
// Get query parameters
13+
limitStr := ctx.Query("limit", "100")
14+
level := ctx.Query("level", "")
15+
16+
limit, err := strconv.Atoi(limitStr)
17+
if err != nil || limit <= 0 {
18+
limit = 100
19+
}
20+
21+
// Get log buffer
22+
logBuffer := lib.GetLogBuffer()
23+
entries := logBuffer.GetRecent(limit)
24+
25+
// Filter by level if specified
26+
if level != "" {
27+
filteredEntries := make([]lib.LogEntry, 0)
28+
for _, entry := range entries {
29+
if entry.Level == level {
30+
filteredEntries = append(filteredEntries, entry)
31+
}
32+
}
33+
entries = filteredEntries
34+
}
35+
36+
return ctx.JSON(fiber.Map{
37+
"logs": entries,
38+
"total": len(entries),
39+
})
40+
}
41+
42+
// ClearLogs clears all log entries.
43+
func (c *Controller) ClearLogs(ctx *fiber.Ctx) error {
44+
logBuffer := lib.GetLogBuffer()
45+
logBuffer.Clear()
46+
47+
return ctx.JSON(fiber.Map{
48+
"message": "Logs cleared successfully",
49+
})
50+
}
51+
52+
// GetLogLevels returns available log levels.
53+
func (c *Controller) GetLogLevels(ctx *fiber.Ctx) error {
54+
levels := []string{"panic", "fatal", "error", "warn", "info", "debug", "trace"}
55+
56+
return ctx.JSON(fiber.Map{
57+
"levels": levels,
58+
})
59+
}

internal/server/server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ func (s *Server) initRoutes() {
127127
route.Get("/network", s.controller.GetNetworkSettings)
128128
route.Put("/network", s.controller.UpdateNetworkSettings)
129129

130+
// Log related routes
131+
route.Get("/logs", s.controller.GetLogs)
132+
route.Delete("/logs", s.controller.ClearLogs)
133+
route.Get("/logs/levels", s.controller.GetLogLevels)
134+
130135
// Serve embedded files
131136
s.app.Use("/", filesystem.New(filesystem.Config{
132137
Root: http.FS(embeddedFiles),

pkg/lib/log_buffer.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package lib
2+
3+
import (
4+
"sync"
5+
"time"
6+
7+
"github.com/sirupsen/logrus"
8+
)
9+
10+
// LogEntry represents a single log entry.
11+
type LogEntry struct {
12+
Timestamp time.Time `json:"timestamp"`
13+
Level string `json:"level"`
14+
Message string `json:"message"`
15+
Fields logrus.Fields `json:"fields,omitempty"`
16+
}
17+
18+
// LogBuffer is a thread-safe circular buffer for log entries.
19+
type LogBuffer struct {
20+
entries []LogEntry
21+
size int
22+
index int
23+
mutex sync.RWMutex
24+
full bool
25+
}
26+
27+
// NewLogBuffer creates a new log buffer with specified size.
28+
func NewLogBuffer(size int) *LogBuffer {
29+
return &LogBuffer{
30+
entries: make([]LogEntry, size),
31+
size: size,
32+
index: 0,
33+
full: false,
34+
}
35+
}
36+
37+
// Add adds a new log entry to the buffer.
38+
func (lb *LogBuffer) Add(entry LogEntry) {
39+
lb.mutex.Lock()
40+
defer lb.mutex.Unlock()
41+
42+
lb.entries[lb.index] = entry
43+
lb.index = (lb.index + 1) % lb.size
44+
45+
if lb.index == 0 {
46+
lb.full = true
47+
}
48+
}
49+
50+
// GetAll returns all log entries in chronological order.
51+
func (lb *LogBuffer) GetAll() []LogEntry {
52+
lb.mutex.RLock()
53+
defer lb.mutex.RUnlock()
54+
55+
if !lb.full && lb.index == 0 {
56+
return []LogEntry{}
57+
}
58+
59+
var result []LogEntry
60+
61+
if lb.full {
62+
// Buffer is full, start from current index (oldest entry)
63+
result = make([]LogEntry, 0, lb.size)
64+
for i := 0; i < lb.size; i++ {
65+
idx := (lb.index + i) % lb.size
66+
result = append(result, lb.entries[idx])
67+
}
68+
} else {
69+
// Buffer is not full, return entries from 0 to index
70+
result = make([]LogEntry, lb.index)
71+
copy(result, lb.entries[:lb.index])
72+
}
73+
74+
return result
75+
}
76+
77+
// GetRecent returns the most recent n log entries.
78+
func (lb *LogBuffer) GetRecent(n int) []LogEntry {
79+
all := lb.GetAll()
80+
if len(all) <= n {
81+
return all
82+
}
83+
return all[len(all)-n:]
84+
}
85+
86+
// Clear clears all log entries.
87+
func (lb *LogBuffer) Clear() {
88+
lb.mutex.Lock()
89+
defer lb.mutex.Unlock()
90+
91+
lb.index = 0
92+
lb.full = false
93+
lb.entries = make([]LogEntry, lb.size)
94+
}
95+
96+
// Global log buffer instance.
97+
var globalLogBuffer *LogBuffer
98+
99+
// InitLogBuffer initializes the global log buffer.
100+
func InitLogBuffer(size int) {
101+
globalLogBuffer = NewLogBuffer(size)
102+
}
103+
104+
// GetLogBuffer returns the global log buffer instance.
105+
func GetLogBuffer() *LogBuffer {
106+
if globalLogBuffer == nil {
107+
InitLogBuffer(1000) // Default size
108+
}
109+
return globalLogBuffer
110+
}
111+
112+
// LogHook is a logrus hook that captures logs in the buffer.
113+
type LogHook struct {
114+
buffer *LogBuffer
115+
}
116+
117+
// NewLogHook creates a new log hook.
118+
func NewLogHook(buffer *LogBuffer) *LogHook {
119+
return &LogHook{
120+
buffer: buffer,
121+
}
122+
}
123+
124+
// Fire is called when a log event is fired.
125+
func (hook *LogHook) Fire(entry *logrus.Entry) error {
126+
logEntry := LogEntry{
127+
Timestamp: entry.Time,
128+
Level: entry.Level.String(),
129+
Message: entry.Message,
130+
Fields: make(logrus.Fields),
131+
}
132+
133+
// Copy fields
134+
for k, v := range entry.Data {
135+
logEntry.Fields[k] = v
136+
}
137+
138+
hook.buffer.Add(logEntry)
139+
return nil
140+
}
141+
142+
// Levels returns the available logging levels.
143+
func (hook *LogHook) Levels() []logrus.Level {
144+
return logrus.AllLevels
145+
}

web/api/logs.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { get_api_server } from '@/api/env';
2+
3+
export interface LogEntry {
4+
timestamp: string;
5+
level: string;
6+
message: string;
7+
fields?: { [key: string]: any };
8+
}
9+
10+
export interface LogsResponse {
11+
logs: LogEntry[];
12+
total: number;
13+
}
14+
15+
export async function get_logs(credentials: string, limit?: number, level?: string): Promise<LogsResponse | null> {
16+
if (!credentials) {
17+
return null;
18+
}
19+
20+
const params = new URLSearchParams();
21+
if (limit) params.append('limit', limit.toString());
22+
if (level) params.append('level', level);
23+
24+
try {
25+
const resp = await fetch(get_api_server() + '/api/v1/logs?' + params.toString(), {
26+
method: 'GET',
27+
headers: {
28+
'Authorization': `Basic ${credentials}`
29+
}
30+
});
31+
32+
if (resp.status === 200) {
33+
return resp.json();
34+
}
35+
} catch (error) {
36+
console.error('Error fetching logs:', error);
37+
}
38+
39+
return null;
40+
}
41+
42+
export async function clear_logs(credentials: string): Promise<boolean> {
43+
if (!credentials) {
44+
return false;
45+
}
46+
47+
try {
48+
const resp = await fetch(get_api_server() + '/api/v1/logs', {
49+
method: 'DELETE',
50+
headers: {
51+
'Authorization': `Basic ${credentials}`
52+
}
53+
});
54+
55+
return resp.status === 200;
56+
} catch (error) {
57+
console.error('Error clearing logs:', error);
58+
}
59+
60+
return false;
61+
}
62+
63+
export async function get_log_levels(credentials: string): Promise<string[]> {
64+
if (!credentials) {
65+
return [];
66+
}
67+
68+
try {
69+
const resp = await fetch(get_api_server() + '/api/v1/logs/levels', {
70+
method: 'GET',
71+
headers: {
72+
'Authorization': `Basic ${credentials}`
73+
}
74+
});
75+
76+
if (resp.status === 200) {
77+
const data = await resp.json();
78+
return data.levels || [];
79+
}
80+
} catch (error) {
81+
console.error('Error fetching log levels:', error);
82+
}
83+
84+
return [];
85+
}

0 commit comments

Comments
 (0)