Add real-time log monitoring and system stats dashboard

Implements a comprehensive monitoring system for the admin interface:

Backend:
- New monitoring package with Redis ring buffer for log storage
- Zerolog MultiWriter to capture logs to Redis
- System stats collection (CPU, memory, disk, goroutines, GC)
- HTTP metrics middleware (request counts, latency, error rates)
- Asynq queue stats for worker process
- WebSocket endpoint for real-time log streaming
- Admin auth middleware now accepts token in query params (for WebSocket)

Frontend:
- New monitoring page with tabs (Overview, Logs, API Stats, Worker Stats)
- Real-time log viewer with level filtering and search
- System stats cards showing CPU, memory, goroutines, uptime
- HTTP endpoint statistics table
- Asynq queue depth visualization
- Enable/disable monitoring toggle in settings

Memory safeguards:
- Max 200 unique endpoints tracked
- Hourly stats reset to prevent unbounded growth
- Max 1000 log entries in ring buffer
- Max 1000 latency samples for P95 calculation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-09 10:26:40 -06:00
parent 12eac24632
commit eb127fda20
31 changed files with 2880 additions and 213 deletions

View File

@@ -29,6 +29,7 @@ func NewAdminSettingsHandler(db *gorm.DB) *AdminSettingsHandler {
// SettingsResponse represents the settings response
type SettingsResponse struct {
EnableLimitations bool `json:"enable_limitations"`
EnableMonitoring bool `json:"enable_monitoring"`
}
// GetSettings handles GET /api/admin/settings
@@ -37,7 +38,7 @@ func (h *AdminSettingsHandler) GetSettings(c *gin.Context) {
if err := h.db.First(&settings, 1).Error; err != nil {
if err == gorm.ErrRecordNotFound {
// Create default settings
settings = models.SubscriptionSettings{ID: 1, EnableLimitations: false}
settings = models.SubscriptionSettings{ID: 1, EnableLimitations: false, EnableMonitoring: true}
h.db.Create(&settings)
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"})
@@ -47,12 +48,14 @@ func (h *AdminSettingsHandler) GetSettings(c *gin.Context) {
c.JSON(http.StatusOK, SettingsResponse{
EnableLimitations: settings.EnableLimitations,
EnableMonitoring: settings.EnableMonitoring,
})
}
// UpdateSettingsRequest represents the update request
type UpdateSettingsRequest struct {
EnableLimitations *bool `json:"enable_limitations"`
EnableMonitoring *bool `json:"enable_monitoring"`
}
// UpdateSettings handles PUT /api/admin/settings
@@ -66,7 +69,7 @@ func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) {
var settings models.SubscriptionSettings
if err := h.db.First(&settings, 1).Error; err != nil {
if err == gorm.ErrRecordNotFound {
settings = models.SubscriptionSettings{ID: 1}
settings = models.SubscriptionSettings{ID: 1, EnableMonitoring: true}
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch settings"})
return
@@ -77,6 +80,10 @@ func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) {
settings.EnableLimitations = *req.EnableLimitations
}
if req.EnableMonitoring != nil {
settings.EnableMonitoring = *req.EnableMonitoring
}
if err := h.db.Save(&settings).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update settings"})
return
@@ -84,6 +91,7 @@ func (h *AdminSettingsHandler) UpdateSettings(c *gin.Context) {
c.JSON(http.StatusOK, SettingsResponse{
EnableLimitations: settings.EnableLimitations,
EnableMonitoring: settings.EnableMonitoring,
})
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/treytartt/casera-api/internal/admin/handlers"
"github.com/treytartt/casera-api/internal/config"
"github.com/treytartt/casera-api/internal/middleware"
"github.com/treytartt/casera-api/internal/monitoring"
"github.com/treytartt/casera-api/internal/push"
"github.com/treytartt/casera-api/internal/repositories"
"github.com/treytartt/casera-api/internal/services"
@@ -22,6 +23,7 @@ type Dependencies struct {
EmailService *services.EmailService
PushClient *push.Client
OnboardingService *services.OnboardingEmailService
MonitoringHandler *monitoring.Handler
}
// SetupRoutes configures all admin routes
@@ -424,6 +426,17 @@ func SetupRoutes(router *gin.Engine, db *gorm.DB, cfg *config.Config, deps *Depe
onboardingEmails.GET("/:id", onboardingHandler.Get)
onboardingEmails.DELETE("/:id", onboardingHandler.Delete)
}
// System monitoring (logs, stats, websocket)
if deps != nil && deps.MonitoringHandler != nil {
monitoringGroup := protected.Group("/monitoring")
{
monitoringGroup.GET("/logs", deps.MonitoringHandler.GetLogs)
monitoringGroup.GET("/stats", deps.MonitoringHandler.GetStats)
monitoringGroup.DELETE("/logs", deps.MonitoringHandler.ClearLogs)
monitoringGroup.GET("/ws", deps.MonitoringHandler.WebSocket)
}
}
}
}