feat(api): Add AI intelligence API endpoints
Expose learned AI intelligence data via REST API: New endpoints: - GET /api/ai/intelligence/patterns - Detected failure patterns - GET /api/ai/intelligence/predictions - Failure predictions - GET /api/ai/intelligence/correlations - Resource correlations - GET /api/ai/intelligence/changes - Recent infrastructure changes - GET /api/ai/intelligence/baselines - Learned baselines All endpoints support ?resource_id filter for per-resource queries. Changes endpoint supports ?hours filter (default: 24). Backend additions: - ai_intelligence_handlers.go - Handler implementations - baseline.Store.GetAllBaselines() - Flat baseline export - patrol.GetChangeDetector() - Access change detector This enables frontend to display: - 'OOM expected in 3 days based on pattern' - 'When storage-1 is full, database VM restarts' - 'VM memory baseline: 60-75%' All tests passing.
This commit is contained in:
parent
de8b36d65d
commit
7fc705ba07
4 changed files with 379 additions and 0 deletions
|
|
@ -278,6 +278,50 @@ func (s *Store) ResourceCount() int {
|
||||||
return len(s.baselines)
|
return len(s.baselines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FlatBaseline is a flattened representation of a single metric baseline for API responses
|
||||||
|
type FlatBaseline struct {
|
||||||
|
ResourceID string `json:"resource_id"`
|
||||||
|
Metric string `json:"metric"`
|
||||||
|
Mean float64 `json:"mean"`
|
||||||
|
StdDev float64 `json:"std_dev"`
|
||||||
|
Min float64 `json:"min"`
|
||||||
|
Max float64 `json:"max"`
|
||||||
|
Samples int `json:"samples"`
|
||||||
|
LastUpdate time.Time `json:"last_update"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllBaselines returns all baselines as a flat map for API access
|
||||||
|
func (s *Store) GetAllBaselines() map[string]*FlatBaseline {
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make(map[string]*FlatBaseline)
|
||||||
|
for resourceID, rb := range s.baselines {
|
||||||
|
for metric, mb := range rb.Metrics {
|
||||||
|
key := resourceID + ":" + metric
|
||||||
|
fb := &FlatBaseline{
|
||||||
|
ResourceID: resourceID,
|
||||||
|
Metric: metric,
|
||||||
|
Mean: mb.Mean,
|
||||||
|
StdDev: mb.StdDev,
|
||||||
|
Samples: mb.SampleCount,
|
||||||
|
LastUpdate: rb.LastUpdated,
|
||||||
|
}
|
||||||
|
// Set min/max from percentiles if available
|
||||||
|
if mb.Percentiles != nil {
|
||||||
|
if p5, ok := mb.Percentiles[5]; ok {
|
||||||
|
fb.Min = p5
|
||||||
|
}
|
||||||
|
if p95, ok := mb.Percentiles[95]; ok {
|
||||||
|
fb.Max = p95
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[key] = fb
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Save persists baselines to disk
|
// Save persists baselines to disk
|
||||||
func (s *Store) Save() error {
|
func (s *Store) Save() error {
|
||||||
if s.dataDir == "" {
|
if s.dataDir == "" {
|
||||||
|
|
|
||||||
|
|
@ -415,6 +415,13 @@ func (p *PatrolService) GetCorrelationDetector() *CorrelationDetector {
|
||||||
return p.correlationDetector
|
return p.correlationDetector
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetChangeDetector returns the change detector
|
||||||
|
func (p *PatrolService) GetChangeDetector() *ChangeDetector {
|
||||||
|
p.mu.RLock()
|
||||||
|
defer p.mu.RUnlock()
|
||||||
|
return p.changeDetector
|
||||||
|
}
|
||||||
|
|
||||||
// GetConfig returns the current patrol configuration
|
// GetConfig returns the current patrol configuration
|
||||||
func (p *PatrolService) GetConfig() PatrolConfig {
|
func (p *PatrolService) GetConfig() PatrolConfig {
|
||||||
p.mu.RLock()
|
p.mu.RLock()
|
||||||
|
|
|
||||||
321
internal/api/ai_intelligence_handlers.go
Normal file
321
internal/api/ai_intelligence_handlers.go
Normal file
|
|
@ -0,0 +1,321 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/utils"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandleGetPatterns returns detected failure patterns (GET /api/ai/intelligence/patterns)
|
||||||
|
func (h *AISettingsHandler) HandleGetPatterns(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
patrol := h.aiService.GetPatrolService()
|
||||||
|
if patrol == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"patterns": []interface{}{},
|
||||||
|
"message": "Patrol service not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write patterns response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detector := patrol.GetPatternDetector()
|
||||||
|
if detector == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"patterns": []interface{}{},
|
||||||
|
"message": "Pattern detector not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write patterns response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get resource filter if provided
|
||||||
|
resourceID := r.URL.Query().Get("resource_id")
|
||||||
|
|
||||||
|
patterns := detector.GetPatterns()
|
||||||
|
var result []map[string]interface{}
|
||||||
|
|
||||||
|
for key, pattern := range patterns {
|
||||||
|
if resourceID != "" && pattern.ResourceID != resourceID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"key": key,
|
||||||
|
"resource_id": pattern.ResourceID,
|
||||||
|
"event_type": pattern.EventType,
|
||||||
|
"occurrences": pattern.Occurrences,
|
||||||
|
"average_interval": pattern.AverageInterval.String(),
|
||||||
|
"average_duration": pattern.AverageDuration.String(),
|
||||||
|
"last_occurrence": pattern.LastOccurrence,
|
||||||
|
"confidence": pattern.Confidence,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"patterns": result,
|
||||||
|
"count": len(result),
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write patterns response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetPredictions returns failure predictions (GET /api/ai/intelligence/predictions)
|
||||||
|
func (h *AISettingsHandler) HandleGetPredictions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
patrol := h.aiService.GetPatrolService()
|
||||||
|
if patrol == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"predictions": []interface{}{},
|
||||||
|
"message": "Patrol service not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write predictions response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detector := patrol.GetPatternDetector()
|
||||||
|
if detector == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"predictions": []interface{}{},
|
||||||
|
"message": "Pattern detector not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write predictions response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get resource filter if provided
|
||||||
|
resourceID := r.URL.Query().Get("resource_id")
|
||||||
|
|
||||||
|
var predictions []ai.FailurePrediction
|
||||||
|
if resourceID != "" {
|
||||||
|
predictions = detector.GetPredictionsForResource(resourceID)
|
||||||
|
} else {
|
||||||
|
predictions = detector.GetPredictions()
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []map[string]interface{}
|
||||||
|
for _, pred := range predictions {
|
||||||
|
isOverdue := pred.DaysUntil < 0
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"resource_id": pred.ResourceID,
|
||||||
|
"event_type": pred.EventType,
|
||||||
|
"predicted_at": pred.PredictedAt,
|
||||||
|
"days_until": pred.DaysUntil,
|
||||||
|
"confidence": pred.Confidence,
|
||||||
|
"basis": pred.Basis,
|
||||||
|
"is_overdue": isOverdue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"predictions": result,
|
||||||
|
"count": len(result),
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write predictions response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetCorrelations returns detected resource correlations (GET /api/ai/intelligence/correlations)
|
||||||
|
func (h *AISettingsHandler) HandleGetCorrelations(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
patrol := h.aiService.GetPatrolService()
|
||||||
|
if patrol == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"correlations": []interface{}{},
|
||||||
|
"message": "Patrol service not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write correlations response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detector := patrol.GetCorrelationDetector()
|
||||||
|
if detector == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"correlations": []interface{}{},
|
||||||
|
"message": "Correlation detector not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write correlations response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get resource filter if provided
|
||||||
|
resourceID := r.URL.Query().Get("resource_id")
|
||||||
|
|
||||||
|
var correlations []*ai.Correlation
|
||||||
|
if resourceID != "" {
|
||||||
|
correlations = detector.GetCorrelationsForResource(resourceID)
|
||||||
|
} else {
|
||||||
|
correlations = detector.GetCorrelations()
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []map[string]interface{}
|
||||||
|
for _, corr := range correlations {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"source_id": corr.SourceID,
|
||||||
|
"source_name": corr.SourceName,
|
||||||
|
"source_type": corr.SourceType,
|
||||||
|
"target_id": corr.TargetID,
|
||||||
|
"target_name": corr.TargetName,
|
||||||
|
"target_type": corr.TargetType,
|
||||||
|
"event_pattern": corr.EventPattern,
|
||||||
|
"occurrences": corr.Occurrences,
|
||||||
|
"avg_delay": corr.AvgDelay.String(),
|
||||||
|
"confidence": corr.Confidence,
|
||||||
|
"last_seen": corr.LastSeen,
|
||||||
|
"description": corr.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"correlations": result,
|
||||||
|
"count": len(result),
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write correlations response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetRecentChanges returns recent infrastructure changes (GET /api/ai/intelligence/changes)
|
||||||
|
func (h *AISettingsHandler) HandleGetRecentChanges(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
patrol := h.aiService.GetPatrolService()
|
||||||
|
if patrol == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"changes": []interface{}{},
|
||||||
|
"message": "Patrol service not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write changes response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detector := patrol.GetChangeDetector()
|
||||||
|
if detector == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"changes": []interface{}{},
|
||||||
|
"message": "Change detector not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write changes response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get time range - default to 24 hours
|
||||||
|
hoursStr := r.URL.Query().Get("hours")
|
||||||
|
hours := 24
|
||||||
|
if hoursStr != "" {
|
||||||
|
if h, err := strconv.Atoi(hoursStr); err == nil && h > 0 {
|
||||||
|
hours = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
||||||
|
changes := detector.GetRecentChanges(100, since)
|
||||||
|
|
||||||
|
var result []map[string]interface{}
|
||||||
|
for _, change := range changes {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"id": change.ID,
|
||||||
|
"resource_id": change.ResourceID,
|
||||||
|
"resource_name": change.ResourceName,
|
||||||
|
"resource_type": change.ResourceType,
|
||||||
|
"change_type": change.ChangeType,
|
||||||
|
"before": change.Before,
|
||||||
|
"after": change.After,
|
||||||
|
"detected_at": change.DetectedAt,
|
||||||
|
"description": change.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"changes": result,
|
||||||
|
"count": len(result),
|
||||||
|
"hours": hours,
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write changes response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleGetBaselines returns learned resource baselines (GET /api/ai/intelligence/baselines)
|
||||||
|
func (h *AISettingsHandler) HandleGetBaselines(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
patrol := h.aiService.GetPatrolService()
|
||||||
|
if patrol == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"baselines": []interface{}{},
|
||||||
|
"message": "Patrol service not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write baselines response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store := patrol.GetBaselineStore()
|
||||||
|
if store == nil {
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"baselines": []interface{}{},
|
||||||
|
"message": "Baseline store not initialized",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write baselines response")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get resource filter if provided
|
||||||
|
resourceID := r.URL.Query().Get("resource_id")
|
||||||
|
|
||||||
|
baselines := store.GetAllBaselines()
|
||||||
|
var result []map[string]interface{}
|
||||||
|
|
||||||
|
for key, baseline := range baselines {
|
||||||
|
if resourceID != "" && baseline.ResourceID != resourceID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"key": key,
|
||||||
|
"resource_id": baseline.ResourceID,
|
||||||
|
"metric": baseline.Metric,
|
||||||
|
"mean": baseline.Mean,
|
||||||
|
"std_dev": baseline.StdDev,
|
||||||
|
"min": baseline.Min,
|
||||||
|
"max": baseline.Max,
|
||||||
|
"samples": baseline.Samples,
|
||||||
|
"last_update": baseline.LastUpdate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.WriteJSONResponse(w, map[string]interface{}{
|
||||||
|
"baselines": result,
|
||||||
|
"count": len(result),
|
||||||
|
}); err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to write baselines response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1130,6 +1130,13 @@ func (r *Router) setupRoutes() {
|
||||||
}))
|
}))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/suppressions/", RequireAuth(r.config, r.aiSettingsHandler.HandleDeleteSuppressionRule))
|
r.mux.HandleFunc("/api/ai/patrol/suppressions/", RequireAuth(r.config, r.aiSettingsHandler.HandleDeleteSuppressionRule))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/dismissed", RequireAuth(r.config, r.aiSettingsHandler.HandleGetDismissedFindings))
|
r.mux.HandleFunc("/api/ai/patrol/dismissed", RequireAuth(r.config, r.aiSettingsHandler.HandleGetDismissedFindings))
|
||||||
|
|
||||||
|
// AI Intelligence endpoints - expose learned patterns, correlations, and predictions
|
||||||
|
r.mux.HandleFunc("/api/ai/intelligence/patterns", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatterns))
|
||||||
|
r.mux.HandleFunc("/api/ai/intelligence/predictions", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPredictions))
|
||||||
|
r.mux.HandleFunc("/api/ai/intelligence/correlations", RequireAuth(r.config, r.aiSettingsHandler.HandleGetCorrelations))
|
||||||
|
r.mux.HandleFunc("/api/ai/intelligence/changes", RequireAuth(r.config, r.aiSettingsHandler.HandleGetRecentChanges))
|
||||||
|
r.mux.HandleFunc("/api/ai/intelligence/baselines", RequireAuth(r.config, r.aiSettingsHandler.HandleGetBaselines))
|
||||||
|
|
||||||
// Agent WebSocket for AI command execution
|
// Agent WebSocket for AI command execution
|
||||||
r.mux.HandleFunc("/api/agent/ws", r.handleAgentWebSocket)
|
r.mux.HandleFunc("/api/agent/ws", r.handleAgentWebSocket)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue