package handlers import ( "fmt" "net/http" "strconv" "time" "github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries" "github.com/Fimeg/RedFlag/aggregator-server/internal/models" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // MetricsHandler handles system and storage metrics type MetricsHandler struct { metricsQueries *queries.MetricsQueries agentQueries *queries.AgentQueries commandQueries *queries.CommandQueries } func NewMetricsHandler(mq *queries.MetricsQueries, aq *queries.AgentQueries, cq *queries.CommandQueries) *MetricsHandler { return &MetricsHandler{ metricsQueries: mq, agentQueries: aq, commandQueries: cq, } } // ReportMetrics handles metrics reports from agents using event sourcing func (h *MetricsHandler) ReportMetrics(c *gin.Context) { agentID := c.MustGet("agent_id").(uuid.UUID) // Update last_seen timestamp if err := h.agentQueries.UpdateAgentLastSeen(agentID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update last seen"}) return } var req models.MetricsReportRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate command exists and belongs to agent commandID, err := uuid.Parse(req.CommandID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid command ID format"}) return } command, err := h.commandQueries.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "command not found"}) return } if command.AgentID != agentID { c.JSON(http.StatusForbidden, gin.H{"error": "unauthorized command"}) return } // Convert metrics to events events := make([]models.StoredMetric, 0, len(req.Metrics)) for _, item := range req.Metrics { event := models.StoredMetric{ ID: uuid.New(), AgentID: agentID, PackageType: item.PackageType, PackageName: item.PackageName, CurrentVersion: item.CurrentVersion, AvailableVersion: item.AvailableVersion, Severity: item.Severity, RepositorySource: item.RepositorySource, Metadata: convertStringMapToJSONB(item.Metadata), EventType: "discovered", CreatedAt: req.Timestamp, } events = append(events, event) } // Store events in batch with error isolation if err := h.metricsQueries.CreateMetricsEventsBatch(events); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to record metrics events"}) return } // Update command status to completed result := models.JSONB{ "metrics_count": len(req.Metrics), "logged_at": time.Now(), } if err := h.commandQueries.MarkCommandCompleted(commandID, result); err != nil { fmt.Printf("Warning: Failed to mark metrics command %s as completed: %v\n", commandID, err) } c.JSON(http.StatusOK, gin.H{ "message": "metrics events recorded", "count": len(events), "command_id": req.CommandID, }) } // GetAgentMetrics retrieves metrics for a specific agent func (h *MetricsHandler) GetAgentMetrics(c *gin.Context) { agentIDStr := c.Param("agentId") agentID, err := uuid.Parse(agentIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"}) return } // Parse query parameters page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 50 } offset := (page - 1) * pageSize packageType := c.Query("package_type") severity := c.Query("severity") // Build filter filter := &models.MetricFilter{ AgentID: &agentID, PackageType: nil, Severity: nil, Limit: &pageSize, Offset: &offset, } if packageType != "" { filter.PackageType = &packageType } if severity != "" { filter.Severity = &severity } // Fetch metrics result, err := h.metricsQueries.GetMetrics(filter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch metrics"}) return } c.JSON(http.StatusOK, result) } // GetAgentStorageMetrics retrieves storage metrics for a specific agent func (h *MetricsHandler) GetAgentStorageMetrics(c *gin.Context) { agentIDStr := c.Param("agentId") agentID, err := uuid.Parse(agentIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"}) return } // Filter for storage metrics only packageType := "storage" pageSize := 100 // Get all storage metrics offset := 0 filter := &models.MetricFilter{ AgentID: &agentID, PackageType: &packageType, Limit: &pageSize, Offset: &offset, } result, err := h.metricsQueries.GetMetrics(filter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch storage metrics"}) return } // Convert to storage-specific format storageMetrics := make([]models.StorageMetrics, 0, len(result.Metrics)) for _, metric := range result.Metrics { storageMetric := models.StorageMetrics{ MountPoint: metric.PackageName, TotalBytes: parseBytes(metric.AvailableVersion), // Available version stores total UsedBytes: parseBytes(metric.CurrentVersion), // Current version stores used UsedPercent: calculateUsagePercent(parseBytes(metric.CurrentVersion), parseBytes(metric.AvailableVersion)), Status: metric.Severity, LastUpdated: metric.CreatedAt, } storageMetrics = append(storageMetrics, storageMetric) } c.JSON(http.StatusOK, gin.H{ "storage_metrics": storageMetrics, "is_live": isRecentlyUpdated(result.Metrics), }) } // GetAgentSystemMetrics retrieves system metrics for a specific agent func (h *MetricsHandler) GetAgentSystemMetrics(c *gin.Context) { agentIDStr := c.Param("agentId") agentID, err := uuid.Parse(agentIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"}) return } // Filter for system metrics only packageType := "system" pageSize := 100 offset := 0 filter := &models.MetricFilter{ AgentID: &agentID, PackageType: &packageType, Limit: &pageSize, Offset: &offset, } result, err := h.metricsQueries.GetMetrics(filter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch system metrics"}) return } // Aggregate system metrics systemMetrics := aggregateSystemMetrics(result.Metrics) c.JSON(http.StatusOK, gin.H{ "system_metrics": systemMetrics, "is_live": isRecentlyUpdated(result.Metrics), }) } // Helper function to parse bytes from string func parseBytes(s string) int64 { // Simple implementation - in real code, parse "10GB", "500MB", etc. // For now, return 0 if parsing fails return 0 } // Helper function to calculate usage percentage func calculateUsagePercent(used, total int64) float64 { if total == 0 { return 0 } return float64(used) / float64(total) * 100 } // Helper function to check if metrics are recently updated func isRecentlyUpdated(metrics []models.StoredMetric) bool { if len(metrics) == 0 { return false } // Check if any metric was updated in the last 5 minutes now := time.Now() for _, metric := range metrics { if now.Sub(metric.CreatedAt) < 5*time.Minute { return true } } return false } // Helper function to aggregate system metrics func aggregateSystemMetrics(metrics []models.StoredMetric) *models.SystemMetrics { if len(metrics) == 0 { return nil } // Aggregate the most recent metrics // This is a simplified implementation - real code would need proper aggregation return &models.SystemMetrics{ CPUModel: "Unknown", CPUCores: 0, CPUThreads: 0, MemoryTotal: 0, MemoryUsed: 0, MemoryPercent: 0, Processes: 0, Uptime: "Unknown", LoadAverage: []float64{0, 0, 0}, LastUpdated: metrics[0].CreatedAt, } } // Helper function to convert map[string]string to models.JSONB func convertStringMapToJSONB(data map[string]string) models.JSONB { result := make(map[string]interface{}) for k, v := range data { result[k] = v } return models.JSONB(result) }