From da48f65e436677e331ebdeb3d0c19b837e6142d7 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Thu, 27 Mar 2025 11:11:26 -0500 Subject: [PATCH] debug page --- core/http/server.go | 36 +++++++++ core/routers/luarouter.go | 47 +++++++++++ core/runner/luarunner.go | 26 ++++++ core/utils/debug.go | 165 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 274 insertions(+) create mode 100644 core/utils/debug.go diff --git a/core/http/server.go b/core/http/server.go index ea0eac9..5930f43 100644 --- a/core/http/server.go +++ b/core/http/server.go @@ -74,6 +74,18 @@ func (s *Server) Shutdown(ctx context.Context) error { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { start := time.Now() + // Special case for debug stats when debug mode is enabled + if s.debugMode && r.URL.Path == "/debug/stats" { + s.handleDebugStats(w, r) + + // Calculate and log request duration + duration := time.Since(start) + if s.loggingEnabled { + LogRequest(s.logger, http.StatusOK, r, duration) + } + return + } + // Wrap the ResponseWriter to capture status code wrappedWriter := newStatusCaptureWriter(w) @@ -281,3 +293,27 @@ func setContentTypeIfMissing(w http.ResponseWriter, contentType string) { w.Header().Set("Content-Type", contentType) } } + +// handleDebugStats displays debug statistics +func (s *Server) handleDebugStats(w http.ResponseWriter, r *http.Request) { + // Collect system stats + stats := utils.CollectSystemStats() + + // Add component stats + routeCount, bytecodeBytes := s.luaRouter.GetRouteStats() + moduleCount := s.luaRunner.GetModuleCount() + + stats.Components = utils.ComponentStats{ + RouteCount: routeCount, + BytecodeBytes: bytecodeBytes, + ModuleCount: moduleCount, + } + + // Generate HTML page + html := utils.DebugStatsPage(stats) + + // Send the response + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write([]byte(html)) +} diff --git a/core/routers/luarouter.go b/core/routers/luarouter.go index 18b6288..4bd8439 100644 --- a/core/routers/luarouter.go +++ b/core/routers/luarouter.go @@ -349,3 +349,50 @@ func (r *LuaRouter) GetNodeWithError(method, path string, params *Params) (*Node Error: node.err, }, true } + +// GetRouteStats returns statistics about the router +func (r *LuaRouter) GetRouteStats() (int, int64) { + r.mu.RLock() + defer r.mu.RUnlock() + + routeCount := 0 + bytecodeBytes := int64(0) + + // Count routes and bytecode size + for _, root := range r.routes { + count, bytes := countNodesAndBytecode(root) + routeCount += count + bytecodeBytes += bytes + } + + return routeCount, bytecodeBytes +} + +// countNodesAndBytecode traverses the tree and counts nodes and bytecode size +func countNodesAndBytecode(n *node) (count int, bytecodeBytes int64) { + if n == nil { + return 0, 0 + } + + // Count this node if it has a handler + if n.handler != "" { + count = 1 + bytecodeBytes = int64(len(n.bytecode)) + } + + // Count static children + for _, child := range n.staticChild { + childCount, childBytes := countNodesAndBytecode(child) + count += childCount + bytecodeBytes += childBytes + } + + // Count parameter child + if n.paramChild != nil { + childCount, childBytes := countNodesAndBytecode(n.paramChild) + count += childCount + bytecodeBytes += childBytes + } + + return count, bytecodeBytes +} diff --git a/core/runner/luarunner.go b/core/runner/luarunner.go index a694bd0..00633cf 100644 --- a/core/runner/luarunner.go +++ b/core/runner/luarunner.go @@ -305,3 +305,29 @@ func (r *LuaRunner) RefreshModuleByName(modName string) bool { func (r *LuaRunner) AddModule(name string, module any) { r.sandbox.AddModule(name, module) } + +// GetModuleCount returns the number of loaded modules +func (r *LuaRunner) GetModuleCount() int { + r.mu.RLock() + defer r.mu.RUnlock() + + count := 0 + + // Get module count from Lua + if r.state != nil { + // Execute a Lua snippet to count modules + if res, err := r.state.ExecuteWithResult(` + local count = 0 + for _ in pairs(package.loaded) do + count = count + 1 + end + return count + `); err == nil { + if num, ok := res.(float64); ok { + count = int(num) + } + } + } + + return count +} diff --git a/core/utils/debug.go b/core/utils/debug.go new file mode 100644 index 0000000..9439b02 --- /dev/null +++ b/core/utils/debug.go @@ -0,0 +1,165 @@ +package utils + +import ( + "fmt" + "html/template" + "runtime" + "strings" + "time" +) + +// ComponentStats holds stats from various system components +type ComponentStats struct { + RouteCount int // Number of routes + BytecodeBytes int64 // Total size of bytecode in bytes + ModuleCount int // Number of loaded modules +} + +// SystemStats represents system statistics for debugging +type SystemStats struct { + Timestamp time.Time + GoVersion string + GoRoutines int + Memory runtime.MemStats + Components ComponentStats +} + +// CollectSystemStats gathers basic system statistics +func CollectSystemStats() SystemStats { + var stats SystemStats + var mem runtime.MemStats + + // Collect basic system info + stats.Timestamp = time.Now() + stats.GoVersion = runtime.Version() + stats.GoRoutines = runtime.NumGoroutine() + + // Collect memory stats + runtime.ReadMemStats(&mem) + stats.Memory = mem + + return stats +} + +// DebugStatsPage generates an HTML debug stats page +func DebugStatsPage(stats SystemStats) string { + const debugTemplate = ` + + + + Moonshark + + + +

Moonshark

+
Generated at: {{.Timestamp.Format "2006-01-02 15:04:05"}}
+ +
+

System

+
+ + + +
Go Version{{.GoVersion}}
Goroutines{{.GoRoutines}}
+
+
+ +
+

Memory

+
+ + + + + +
Allocated{{ByteCount .Memory.Alloc}}
Total Allocated{{ByteCount .Memory.TotalAlloc}}
System Memory{{ByteCount .Memory.Sys}}
GC Cycles{{.Memory.NumGC}}
+
+
+ +
+

Lua Engine

+
+ + + + +
Active Routes{{.Components.RouteCount}}
Bytecode Size{{ByteCount .Components.BytecodeBytes}}
Loaded Modules{{.Components.ModuleCount}}
+
+
+ + +` + + // Create a template function map + funcMap := template.FuncMap{ + "ByteCount": func(b interface{}) string { + var bytes uint64 + + // Convert the value to uint64 + switch v := b.(type) { + case uint64: + bytes = v + case int64: + bytes = uint64(v) + case int: + bytes = uint64(v) + default: + return "Unknown" + } + + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := uint64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) + }, + } + + // Parse the template + tmpl, err := template.New("debug").Funcs(funcMap).Parse(debugTemplate) + if err != nil { + return fmt.Sprintf("Error parsing template: %v", err) + } + + // Execute the template + var output strings.Builder + if err := tmpl.Execute(&output, stats); err != nil { + return fmt.Sprintf("Error executing template: %v", err) + } + + return output.String() +}