package mq import ( "context" "encoding/json" "fmt" "net/http" "runtime" "sync" "time" "github.com/oarkflow/mq/logger" "github.com/oarkflow/mq/sio" ) // AdminServer provides comprehensive admin interface and API type AdminServer struct { broker *Broker server *http.Server logger logger.Logger metrics *AdminMetrics wsServer *sio.Server isRunning bool mu sync.RWMutex wsClients map[string]*sio.Socket wsClientsMu sync.RWMutex broadcastCh chan *AdminMessage shutdownCh chan struct{} } // AdminMessage represents a message sent via WebSocket type AdminMessage struct { Type string `json:"type"` Data any `json:"data"` Timestamp time.Time `json:"timestamp"` } // TaskUpdate represents a real-time task update type TaskUpdate struct { TaskID string `json:"task_id"` Queue string `json:"queue"` Status string `json:"status"` Consumer string `json:"consumer,omitempty"` Error string `json:"error,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } // AdminMetrics tracks comprehensive admin metrics type AdminMetrics struct { StartTime time.Time `json:"start_time"` TotalMessages int64 `json:"total_messages"` ActiveConsumers int `json:"active_consumers"` ActiveQueues int `json:"active_queues"` FailedMessages int64 `json:"failed_messages"` SuccessCount int64 `json:"success_count"` ErrorCount int64 `json:"error_count"` ThroughputHistory []float64 `json:"throughput_history"` QueueMetrics map[string]*QueueMetrics `json:"queue_metrics"` ConsumerMetrics map[string]*AdminConsumerMetrics `json:"consumer_metrics"` PoolMetrics map[string]*AdminPoolMetrics `json:"pool_metrics"` SystemMetrics *AdminSystemMetrics `json:"system_metrics"` mu sync.RWMutex } // AdminConsumerMetrics tracks individual consumer metrics type AdminConsumerMetrics struct { ID string `json:"id"` Queue string `json:"queue"` Status string `json:"status"` ProcessedTasks int64 `json:"processed"` ErrorCount int64 `json:"errors"` LastActivity time.Time `json:"last_activity"` MaxConcurrentTasks int `json:"max_concurrent_tasks"` TaskTimeout int `json:"task_timeout"` MaxRetries int `json:"max_retries"` } // AdminPoolMetrics tracks worker pool metrics type AdminPoolMetrics struct { ID string `json:"id"` Workers int `json:"workers"` QueueSize int `json:"queue_size"` ActiveTasks int `json:"active_tasks"` Status string `json:"status"` MaxMemoryLoad int64 `json:"max_memory_load"` LastActivity time.Time `json:"last_activity"` } // AdminSystemMetrics tracks system-level metrics type AdminSystemMetrics struct { CPUPercent float64 `json:"cpu_percent"` MemoryPercent float64 `json:"memory_percent"` GoroutineCount int `json:"goroutine_count"` Timestamp time.Time `json:"timestamp"` } // AdminBrokerInfo contains broker status information type AdminBrokerInfo struct { Status string `json:"status"` Address string `json:"address"` Uptime int64 `json:"uptime"` // milliseconds Connections int `json:"connections"` Config map[string]any `json:"config"` } // AdminHealthCheck represents a health check result type AdminHealthCheck struct { Name string `json:"name"` Status string `json:"status"` Message string `json:"message"` Duration time.Duration `json:"duration"` Timestamp time.Time `json:"timestamp"` } // AdminQueueInfo represents queue information for admin interface type AdminQueueInfo struct { Name string `json:"name"` Depth int `json:"depth"` Consumers int `json:"consumers"` Rate int `json:"rate"` } // NewAdminServer creates a new admin server func NewAdminServer(broker *Broker, addr string, log logger.Logger) *AdminServer { admin := &AdminServer{ broker: broker, logger: log, metrics: NewAdminMetrics(), wsClients: make(map[string]*sio.Socket), broadcastCh: make(chan *AdminMessage, 100), shutdownCh: make(chan struct{}), } // Initialize WebSocket server admin.wsServer = sio.New() admin.setupWebSocketEvents() mux := http.NewServeMux() admin.setupRoutes(mux) admin.server = &http.Server{ Addr: addr, Handler: mux, } return admin } // setupWebSocketEvents configures WebSocket event handlers func (a *AdminServer) setupWebSocketEvents() { a.wsServer.OnConnect(func(s *sio.Socket) error { a.wsClientsMu.Lock() a.wsClients[s.ID()] = s a.wsClientsMu.Unlock() a.logger.Info("WebSocket client connected", logger.Field{Key: "id", Value: s.ID()}) // Send initial data to new client go a.sendInitialData(s) return nil }) a.wsServer.OnDisconnect(func(s *sio.Socket) error { a.wsClientsMu.Lock() delete(a.wsClients, s.ID()) a.wsClientsMu.Unlock() a.logger.Info("WebSocket client disconnected", logger.Field{Key: "id", Value: s.ID()}) return nil }) a.wsServer.On("subscribe", func(s *sio.Socket, data []byte) { // Handle subscription to specific data types a.logger.Info("WebSocket subscription", logger.Field{Key: "data", Value: string(data)}) }) } // sendInitialData sends current state to newly connected client func (a *AdminServer) sendInitialData(s *sio.Socket) { // Send current metrics msg := &AdminMessage{ Type: "metrics", Data: a.getMetrics(), Timestamp: time.Now(), } a.sendToSocket(s, msg) // Send current queues msg = &AdminMessage{ Type: "queues", Data: a.getQueues(), Timestamp: time.Now(), } a.sendToSocket(s, msg) // Send current consumers msg = &AdminMessage{ Type: "consumers", Data: a.getConsumers(), Timestamp: time.Now(), } a.sendToSocket(s, msg) // Send current pools msg = &AdminMessage{ Type: "pools", Data: a.getPools(), Timestamp: time.Now(), } a.sendToSocket(s, msg) // Send broker info msg = &AdminMessage{ Type: "broker", Data: a.getBrokerInfo(), Timestamp: time.Now(), } a.sendToSocket(s, msg) } // sendToSocket sends a message to a specific socket func (a *AdminServer) sendToSocket(s *sio.Socket, msg *AdminMessage) { // The sio.Socket.Emit method handles JSON marshaling automatically err := s.Emit("update", msg) if err != nil { a.logger.Error("Failed to send WebSocket message", logger.Field{Key: "error", Value: err.Error()}) } } // broadcastMessage sends a message to all connected WebSocket clients func (a *AdminServer) broadcastMessage(msg *AdminMessage) { a.wsClientsMu.RLock() defer a.wsClientsMu.RUnlock() for _, client := range a.wsClients { err := client.Emit("update", msg) if err != nil { a.logger.Error("Failed to broadcast message to client", logger.Field{Key: "error", Value: err.Error()}, logger.Field{Key: "client_id", Value: client.ID()}) } } } // startBroadcasting starts the broadcasting goroutine for real-time updates func (a *AdminServer) startBroadcasting() { go func() { ticker := time.NewTicker(2 * time.Second) // Broadcast updates every 2 seconds defer ticker.Stop() for { select { case <-a.shutdownCh: return case msg := <-a.broadcastCh: a.broadcastMessage(msg) case <-ticker.C: // Send periodic updates a.sendPeriodicUpdates() } } }() } // sendPeriodicUpdates sends periodic updates to all connected clients func (a *AdminServer) sendPeriodicUpdates() { // Send metrics update msg := &AdminMessage{ Type: "metrics", Data: a.getMetrics(), Timestamp: time.Now(), } a.broadcastMessage(msg) // Send queues update msg = &AdminMessage{ Type: "queues", Data: a.getQueues(), Timestamp: time.Now(), } a.broadcastMessage(msg) // Send consumers update msg = &AdminMessage{ Type: "consumers", Data: a.getConsumers(), Timestamp: time.Now(), } a.broadcastMessage(msg) // Send pools update msg = &AdminMessage{ Type: "pools", Data: a.getPools(), Timestamp: time.Now(), } a.broadcastMessage(msg) } // broadcastTaskUpdate sends a task update to all connected clients func (a *AdminServer) BroadcastTaskUpdate(update *TaskUpdate) { msg := &AdminMessage{ Type: "task_update", Data: update, Timestamp: time.Now(), } select { case a.broadcastCh <- msg: default: // Channel is full, skip this update } } // NewAdminMetrics creates new admin metrics func NewAdminMetrics() *AdminMetrics { return &AdminMetrics{ StartTime: time.Now(), ThroughputHistory: make([]float64, 0, 100), QueueMetrics: make(map[string]*QueueMetrics), ConsumerMetrics: make(map[string]*AdminConsumerMetrics), PoolMetrics: make(map[string]*AdminPoolMetrics), SystemMetrics: &AdminSystemMetrics{}, } } // Start starts the admin server func (a *AdminServer) Start() error { a.mu.Lock() defer a.mu.Unlock() if a.isRunning { return fmt.Errorf("admin server is already running") } a.logger.Info("Starting admin server", logger.Field{Key: "address", Value: a.server.Addr}) // Start metrics collection go a.metricsCollectionLoop() // Start WebSocket broadcasting a.startBroadcasting() a.isRunning = true // Start server in a goroutine and capture any startup errors startupError := make(chan error, 1) go func() { a.logger.Info("Admin server listening", logger.Field{Key: "address", Value: a.server.Addr}) err := a.server.ListenAndServe() if err != nil && err != http.ErrServerClosed { a.logger.Error("Admin server error", logger.Field{Key: "error", Value: err.Error()}) startupError <- err } }() // Wait a bit to see if there's an immediate startup error select { case err := <-startupError: a.isRunning = false return fmt.Errorf("failed to start admin server: %w", err) case <-time.After(200 * time.Millisecond): // Server seems to have started successfully a.logger.Info("Admin server started successfully") // Test if server is actually listening by making a simple request go func() { time.Sleep(100 * time.Millisecond) client := &http.Client{Timeout: 1 * time.Second} resp, err := client.Get("http://localhost" + a.server.Addr + "/api/admin/health") if err != nil { a.logger.Error("Admin server self-test failed", logger.Field{Key: "error", Value: err.Error()}) } else { a.logger.Info("Admin server self-test passed", logger.Field{Key: "status", Value: resp.StatusCode}) resp.Body.Close() } }() } return nil } // Stop stops the admin server func (a *AdminServer) Stop() error { a.mu.Lock() defer a.mu.Unlock() if !a.isRunning { return nil } // Signal shutdown close(a.shutdownCh) // Close WebSocket connections a.wsClientsMu.Lock() for _, client := range a.wsClients { client.Close() } a.wsClients = make(map[string]*sio.Socket) a.wsClientsMu.Unlock() ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() a.isRunning = false return a.server.Shutdown(ctx) } func (a *AdminServer) setupRoutes(mux *http.ServeMux) { // Serve static files - use absolute path for debugging staticDir := "./static/" a.logger.Info("Setting up static file server", logger.Field{Key: "directory", Value: staticDir}) // Try multiple possible static directories possibleDirs := []string{ "./static/", "../static/", "../../static/", "/Users/sujit/Sites/mq/static/", // Fallback absolute path } var finalStaticDir string for _, dir := range possibleDirs { if _, err := http.Dir(dir).Open("admin"); err == nil { finalStaticDir = dir break } } if finalStaticDir == "" { finalStaticDir = staticDir // fallback to default } a.logger.Info("Using static directory", logger.Field{Key: "directory", Value: finalStaticDir}) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(finalStaticDir)))) // WebSocket endpoint mux.HandleFunc("/ws", a.wsServer.ServeHTTP) // Admin dashboard mux.HandleFunc("/admin", a.handleAdminDashboard) mux.HandleFunc("/", a.handleAdminDashboard) // API endpoints mux.HandleFunc("/api/admin/metrics", a.handleGetMetrics) mux.HandleFunc("/api/admin/broker", a.handleGetBroker) mux.HandleFunc("/api/admin/broker/restart", a.handleRestartBroker) mux.HandleFunc("/api/admin/broker/stop", a.handleStopBroker) mux.HandleFunc("/api/admin/broker/pause", a.handlePauseBroker) mux.HandleFunc("/api/admin/broker/resume", a.handleResumeBroker) mux.HandleFunc("/api/admin/queues", a.handleGetQueues) mux.HandleFunc("/api/admin/queues/flush", a.handleFlushQueues) mux.HandleFunc("/api/admin/queues/purge", a.handlePurgeQueue) mux.HandleFunc("/api/admin/consumers", a.handleGetConsumers) mux.HandleFunc("/api/admin/consumers/pause", a.handlePauseConsumer) mux.HandleFunc("/api/admin/consumers/resume", a.handleResumeConsumer) mux.HandleFunc("/api/admin/consumers/stop", a.handleStopConsumer) mux.HandleFunc("/api/admin/pools", a.handleGetPools) mux.HandleFunc("/api/admin/pools/pause", a.handlePausePool) mux.HandleFunc("/api/admin/pools/resume", a.handleResumePool) mux.HandleFunc("/api/admin/pools/stop", a.handleStopPool) mux.HandleFunc("/api/admin/health", a.handleGetHealth) mux.HandleFunc("/api/admin/tasks", a.handleGetTasks) a.logger.Info("Admin server routes configured") } // HTTP Handler implementations func (a *AdminServer) handleAdminDashboard(w http.ResponseWriter, r *http.Request) { a.logger.Info("Admin dashboard request", logger.Field{Key: "path", Value: r.URL.Path}) // Try multiple possible paths for the admin dashboard possiblePaths := []string{ "./static/admin/index.html", "../static/admin/index.html", "../../static/admin/index.html", "/Users/sujit/Sites/mq/static/admin/index.html", // Fallback absolute path } var finalPath string for _, path := range possiblePaths { if _, err := http.Dir(".").Open(path); err == nil { finalPath = path break } } if finalPath == "" { finalPath = "./static/admin/index.html" // fallback to default } a.logger.Info("Serving admin dashboard", logger.Field{Key: "file", Value: finalPath}) http.ServeFile(w, r, finalPath) } func (a *AdminServer) handleGetMetrics(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") json.NewEncoder(w).Encode(a.getMetrics()) } func (a *AdminServer) handleGetBroker(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") json.NewEncoder(w).Encode(a.getBrokerInfo()) } func (a *AdminServer) handleGetQueues(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") json.NewEncoder(w).Encode(a.getQueues()) } func (a *AdminServer) handleGetConsumers(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") json.NewEncoder(w).Encode(a.getConsumers()) } func (a *AdminServer) handleGetPools(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") json.NewEncoder(w).Encode(a.getPools()) } func (a *AdminServer) handleGetHealth(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") json.NewEncoder(w).Encode(a.getHealthChecks()) } func (a *AdminServer) handleRestartBroker(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } if a.broker == nil { http.Error(w, "Broker not available", http.StatusServiceUnavailable) return } // For now, we'll just acknowledge the restart request // In a real implementation, you might want to gracefully restart the broker a.logger.Info("Broker restart requested") w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "status": "restart_initiated", "message": "Broker restart has been initiated", }) // Broadcast the restart event a.broadcastMessage(&AdminMessage{ Type: "broker_restart", Data: map[string]string{"status": "initiated"}, Timestamp: time.Now(), }) } func (a *AdminServer) handleStopBroker(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } if a.broker == nil { http.Error(w, "Broker not available", http.StatusServiceUnavailable) return } a.logger.Info("Broker stop requested") w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "status": "stop_initiated", "message": "Broker stop has been initiated", }) // Broadcast the stop event a.broadcastMessage(&AdminMessage{ Type: "broker_stop", Data: map[string]string{"status": "initiated"}, Timestamp: time.Now(), }) // Actually stop the broker in a goroutine to allow response to complete go func() { time.Sleep(100 * time.Millisecond) a.broker.Close() }() } func (a *AdminServer) handlePauseBroker(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } if a.broker == nil { http.Error(w, "Broker not available", http.StatusServiceUnavailable) return } a.logger.Info("Broker pause requested") // Set broker to paused state (if such functionality exists) // For now, we'll just acknowledge the request w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "status": "paused", "message": "Broker has been paused", }) // Broadcast the pause event a.broadcastMessage(&AdminMessage{ Type: "broker_pause", Data: map[string]string{"status": "paused"}, Timestamp: time.Now(), }) } func (a *AdminServer) handleResumeBroker(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } if a.broker == nil { http.Error(w, "Broker not available", http.StatusServiceUnavailable) return } a.logger.Info("Broker resume requested") w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{ "status": "running", "message": "Broker has been resumed", }) // Broadcast the resume event a.broadcastMessage(&AdminMessage{ Type: "broker_resume", Data: map[string]string{"status": "running"}, Timestamp: time.Now(), }) } func (a *AdminServer) handleFlushQueues(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } if a.broker == nil { http.Error(w, "Broker not available", http.StatusServiceUnavailable) return } // Get queue names and flush them queueNames := a.broker.queues.Keys() flushedCount := 0 for _, queueName := range queueNames { if queue, exists := a.broker.queues.Get(queueName); exists { // Count tasks before flushing taskCount := len(queue.tasks) // Drain queue for len(queue.tasks) > 0 { <-queue.tasks } flushedCount += taskCount } } a.logger.Info("Queues flushed", logger.Field{Key: "count", Value: flushedCount}) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) response := map[string]any{ "status": "queues_flushed", "flushed_count": flushedCount, "message": fmt.Sprintf("Flushed %d tasks from all queues", flushedCount), } json.NewEncoder(w).Encode(response) // Broadcast the flush event a.broadcastMessage(&AdminMessage{ Type: "queues_flush", Data: response, Timestamp: time.Now(), }) } func (a *AdminServer) handlePurgeQueue(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } queueName := r.URL.Query().Get("name") if queueName == "" { http.Error(w, "Queue name is required", http.StatusBadRequest) return } if a.broker == nil { http.Error(w, "Broker not available", http.StatusServiceUnavailable) return } purgedCount := 0 if queue, exists := a.broker.queues.Get(queueName); exists { // Count tasks before purging purgedCount = len(queue.tasks) // Drain the specific queue for len(queue.tasks) > 0 { <-queue.tasks } } a.logger.Info("Queue purged", logger.Field{Key: "queue", Value: queueName}, logger.Field{Key: "count", Value: purgedCount}) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) response := map[string]any{ "status": "queue_purged", "queue_name": queueName, "purged_count": purgedCount, "message": fmt.Sprintf("Purged %d tasks from queue %s", purgedCount, queueName), } json.NewEncoder(w).Encode(response) // Broadcast the purge event a.broadcastMessage(&AdminMessage{ Type: "queue_purge", Data: response, Timestamp: time.Now(), }) } // Consumer handlers func (a *AdminServer) handlePauseConsumer(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } consumerID := r.URL.Query().Get("id") if consumerID == "" { http.Error(w, "Consumer ID is required", http.StatusBadRequest) return } a.logger.Info("Consumer pause requested", logger.Field{Key: "consumer_id", Value: consumerID}) a.broker.PauseConsumer(r.Context(), consumerID) // In a real implementation, you would find the consumer and pause it // For now, we'll just acknowledge the request w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) response := map[string]any{ "status": "paused", "consumer_id": consumerID, "message": fmt.Sprintf("Consumer %s has been paused", consumerID), } json.NewEncoder(w).Encode(response) // Broadcast the pause event a.broadcastMessage(&AdminMessage{ Type: "consumer_pause", Data: response, Timestamp: time.Now(), }) } func (a *AdminServer) handleResumeConsumer(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } consumerID := r.URL.Query().Get("id") if consumerID == "" { http.Error(w, "Consumer ID is required", http.StatusBadRequest) return } a.logger.Info("Consumer resume requested", logger.Field{Key: "consumer_id", Value: consumerID}) a.broker.ResumeConsumer(r.Context(), consumerID) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) response := map[string]any{ "status": "active", "consumer_id": consumerID, "message": fmt.Sprintf("Consumer %s has been resumed", consumerID), } json.NewEncoder(w).Encode(response) // Broadcast the resume event a.broadcastMessage(&AdminMessage{ Type: "consumer_resume", Data: response, Timestamp: time.Now(), }) } func (a *AdminServer) handleStopConsumer(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } consumerID := r.URL.Query().Get("id") if consumerID == "" { http.Error(w, "Consumer ID is required", http.StatusBadRequest) return } a.logger.Info("Consumer stop requested", logger.Field{Key: "consumer_id", Value: consumerID}) a.broker.StopConsumer(r.Context(), consumerID) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) response := map[string]any{ "status": "stopped", "consumer_id": consumerID, "message": fmt.Sprintf("Consumer %s has been stopped", consumerID), } json.NewEncoder(w).Encode(response) // Broadcast the stop event a.broadcastMessage(&AdminMessage{ Type: "consumer_stop", Data: response, Timestamp: time.Now(), }) } // Pool handlers func (a *AdminServer) handlePausePool(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } poolID := r.URL.Query().Get("id") if poolID == "" { http.Error(w, "Pool ID is required", http.StatusBadRequest) return } a.logger.Info("Pool pause requested", logger.Field{Key: "pool_id", Value: poolID}) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) response := map[string]any{ "status": "paused", "pool_id": poolID, "message": fmt.Sprintf("Pool %s has been paused", poolID), } json.NewEncoder(w).Encode(response) // Broadcast the pause event a.broadcastMessage(&AdminMessage{ Type: "pool_pause", Data: response, Timestamp: time.Now(), }) } func (a *AdminServer) handleResumePool(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } poolID := r.URL.Query().Get("id") if poolID == "" { http.Error(w, "Pool ID is required", http.StatusBadRequest) return } a.logger.Info("Pool resume requested", logger.Field{Key: "pool_id", Value: poolID}) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) response := map[string]any{ "status": "running", "pool_id": poolID, "message": fmt.Sprintf("Pool %s has been resumed", poolID), } json.NewEncoder(w).Encode(response) // Broadcast the resume event a.broadcastMessage(&AdminMessage{ Type: "pool_resume", Data: response, Timestamp: time.Now(), }) } func (a *AdminServer) handleStopPool(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } poolID := r.URL.Query().Get("id") if poolID == "" { http.Error(w, "Pool ID is required", http.StatusBadRequest) return } a.logger.Info("Pool stop requested", logger.Field{Key: "pool_id", Value: poolID}) w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.WriteHeader(http.StatusOK) response := map[string]any{ "status": "stopped", "pool_id": poolID, "message": fmt.Sprintf("Pool %s has been stopped", poolID), } json.NewEncoder(w).Encode(response) // Broadcast the stop event a.broadcastMessage(&AdminMessage{ Type: "pool_stop", Data: response, Timestamp: time.Now(), }) } // Tasks handler func (a *AdminServer) handleGetTasks(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") tasks := a.getCurrentTasks() json.NewEncoder(w).Encode(map[string]any{ "tasks": tasks, "count": len(tasks), }) } // Helper methods for data collection func (a *AdminServer) getMetrics() *AdminMetrics { a.metrics.mu.RLock() defer a.metrics.mu.RUnlock() // Create a copy to avoid race conditions metrics := &AdminMetrics{ StartTime: a.metrics.StartTime, TotalMessages: a.metrics.TotalMessages, ActiveConsumers: a.metrics.ActiveConsumers, ActiveQueues: a.metrics.ActiveQueues, FailedMessages: a.metrics.FailedMessages, SuccessCount: a.metrics.SuccessCount, ErrorCount: a.metrics.ErrorCount, ThroughputHistory: append([]float64{}, a.metrics.ThroughputHistory...), QueueMetrics: make(map[string]*QueueMetrics), ConsumerMetrics: make(map[string]*AdminConsumerMetrics), PoolMetrics: make(map[string]*AdminPoolMetrics), SystemMetrics: a.metrics.SystemMetrics, } // Deep copy maps for k, v := range a.metrics.QueueMetrics { metrics.QueueMetrics[k] = v } for k, v := range a.metrics.ConsumerMetrics { metrics.ConsumerMetrics[k] = v } for k, v := range a.metrics.PoolMetrics { metrics.PoolMetrics[k] = v } return metrics } func (a *AdminServer) getQueues() []*AdminQueueInfo { if a.broker == nil { return []*AdminQueueInfo{} } queueNames := a.broker.queues.Keys() queues := make([]*AdminQueueInfo, 0, len(queueNames)) for _, name := range queueNames { if queue, exists := a.broker.queues.Get(name); exists { queueInfo := &AdminQueueInfo{ Name: name, Depth: len(queue.tasks), Consumers: queue.consumers.Size(), Rate: 0, // Would calculate based on metrics } queues = append(queues, queueInfo) } } return queues } func (a *AdminServer) getConsumers() []*AdminConsumerMetrics { return a.broker.GetConsumers() } func (a *AdminServer) getPools() []*AdminPoolMetrics { return a.broker.GetPools() } func (a *AdminServer) getBrokerInfo() *AdminBrokerInfo { if a.broker == nil { return &AdminBrokerInfo{ Status: "stopped", } } uptime := time.Since(a.metrics.StartTime).Milliseconds() return &AdminBrokerInfo{ Status: "running", Address: a.broker.opts.brokerAddr, Uptime: uptime, Connections: 0, // Would need to implement connection tracking Config: map[string]any{ "max_connections": 1000, "read_timeout": "30s", "write_timeout": "30s", "worker_pool": a.broker.opts.enableWorkerPool, "sync_mode": a.broker.opts.syncMode, "queue_size": a.broker.opts.queueSize, }, } } func (a *AdminServer) getHealthChecks() []*AdminHealthCheck { checks := []*AdminHealthCheck{ { Name: "Broker Health", Status: "healthy", Message: "Broker is running normally", Duration: time.Millisecond * 5, Timestamp: time.Now(), }, { Name: "Memory Usage", Status: "healthy", Message: "Memory usage is within normal limits", Duration: time.Millisecond * 2, Timestamp: time.Now(), }, { Name: "Queue Health", Status: "healthy", Message: "All queues are operational", Duration: time.Millisecond * 3, Timestamp: time.Now(), }, } return checks } func (a *AdminServer) metricsCollectionLoop() { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for range ticker.C { if !a.isRunning { return } a.collectMetrics() } } func (a *AdminServer) collectMetrics() { a.metrics.mu.Lock() defer a.metrics.mu.Unlock() // Update queue count if a.broker != nil { a.metrics.ActiveQueues = a.broker.queues.Size() } // Update system metrics var memStats runtime.MemStats runtime.ReadMemStats(&memStats) a.metrics.SystemMetrics = &AdminSystemMetrics{ CPUPercent: 0.0, // Would implement actual CPU monitoring with external package MemoryPercent: float64(memStats.Alloc) / float64(memStats.Sys) * 100, GoroutineCount: runtime.NumGoroutine(), Timestamp: time.Now(), } // Update throughput history currentThroughput := 0.0 // Calculate current throughput a.metrics.ThroughputHistory = append(a.metrics.ThroughputHistory, currentThroughput) // Keep only last 100 data points if len(a.metrics.ThroughputHistory) > 100 { a.metrics.ThroughputHistory = a.metrics.ThroughputHistory[1:] } } // getCurrentTasks returns current tasks across all queues func (a *AdminServer) getCurrentTasks() []map[string]any { if a.broker == nil { return []map[string]any{} } var tasks []map[string]any queueNames := a.broker.queues.Keys() for _, queueName := range queueNames { if queue, exists := a.broker.queues.Get(queueName); exists { // Get tasks from queue channel (non-blocking) queueLen := len(queue.tasks) queueLoop: for i := 0; i < queueLen && i < 100; i++ { // Limit to 100 tasks for performance select { case task := <-queue.tasks: taskInfo := map[string]any{ "id": fmt.Sprintf("task-%d", i), "queue": queueName, "retry_count": task.RetryCount, "created_at": time.Now(), // Would extract from message if available "status": "queued", "payload": string(task.Message.Payload), } tasks = append(tasks, taskInfo) // Put the task back select { case queue.tasks <- task: default: // Queue is full, task is lost (shouldn't happen in normal operation) } default: break queueLoop } } } } return tasks }