diff --git a/internal/ai/baseline_exports.go b/internal/ai/baseline_exports.go new file mode 100644 index 0000000..5b5f009 --- /dev/null +++ b/internal/ai/baseline_exports.go @@ -0,0 +1,24 @@ +package ai + +import ( + "github.com/rcourtman/pulse-go-rewrite/internal/ai/baseline" +) + +// BaselineConfig is an alias for the baseline package config +type BaselineConfig = baseline.StoreConfig + +// BaselineStore is an alias for the baseline.Store type +type BaselineStore = baseline.Store + +// BaselineMetricPoint is an alias for the baseline.MetricPoint type +type BaselineMetricPoint = baseline.MetricPoint + +// DefaultBaselineConfig returns the default baseline configuration +func DefaultBaselineConfig() BaselineConfig { + return baseline.DefaultConfig() +} + +// NewBaselineStore creates a new baseline store +func NewBaselineStore(cfg BaselineConfig) *BaselineStore { + return baseline.NewStore(cfg) +} diff --git a/internal/ai/service.go b/internal/ai/service.go index d143175..872001f 100644 --- a/internal/ai/service.go +++ b/internal/ai/service.go @@ -175,6 +175,17 @@ func (s *Service) SetMetricsHistoryProvider(provider MetricsHistoryProvider) { } } +// SetBaselineStore sets the baseline store for anomaly detection +func (s *Service) SetBaselineStore(store *BaselineStore) { + s.mu.RLock() + patrol := s.patrolService + s.mu.RUnlock() + + if patrol != nil { + patrol.SetBaselineStore(store) + } +} + // StartPatrol starts the background patrol service func (s *Service) StartPatrol(ctx context.Context) { s.mu.RLock() diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go index 1f7860c..a65b14a 100644 --- a/internal/api/ai_handlers.go +++ b/internal/api/ai_handlers.go @@ -96,6 +96,11 @@ func (h *AISettingsHandler) SetMetricsHistoryProvider(provider ai.MetricsHistory h.aiService.SetMetricsHistoryProvider(provider) } +// SetBaselineStore sets the baseline store for anomaly detection +func (h *AISettingsHandler) SetBaselineStore(store *ai.BaselineStore) { + h.aiService.SetBaselineStore(store) +} + // StopPatrol stops the background AI patrol service func (h *AISettingsHandler) StopPatrol() { h.aiService.StopPatrol() diff --git a/internal/api/router.go b/internal/api/router.go index 5fa6de2..8ab8eb5 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1418,6 +1418,20 @@ func (r *Router) StartPatrol(ctx context.Context) { if adapter != nil { r.aiSettingsHandler.SetMetricsHistoryProvider(adapter) } + + // Initialize baseline store for anomaly detection + // Uses config dir for persistence + baselineCfg := ai.DefaultBaselineConfig() + if r.persistence != nil { + baselineCfg.DataDir = r.persistence.DataDir() + } + baselineStore := ai.NewBaselineStore(baselineCfg) + if baselineStore != nil { + r.aiSettingsHandler.SetBaselineStore(baselineStore) + + // Start background baseline learning loop + go r.startBaselineLearning(ctx, baselineStore, metricsHistory) + } } } @@ -1432,6 +1446,117 @@ func (r *Router) StopPatrol() { } } +// startBaselineLearning runs a background loop that learns baselines from metrics history +// This enables anomaly detection by understanding what "normal" looks like for each resource +func (r *Router) startBaselineLearning(ctx context.Context, store *ai.BaselineStore, metricsHistory *monitoring.MetricsHistory) { + if store == nil || metricsHistory == nil { + return + } + + // Learn every hour + ticker := time.NewTicker(1 * time.Hour) + defer ticker.Stop() + + // Run initial learning after a short delay (allow metrics to accumulate) + select { + case <-ctx.Done(): + return + case <-time.After(5 * time.Minute): + r.learnBaselines(store, metricsHistory) + } + + log.Info().Msg("Baseline learning loop started") + + for { + select { + case <-ctx.Done(): + // Save baselines before exit + if err := store.Save(); err != nil { + log.Warn().Err(err).Msg("Failed to save baselines on shutdown") + } + log.Info().Msg("Baseline learning loop stopped") + return + case <-ticker.C: + r.learnBaselines(store, metricsHistory) + } + } +} + +// learnBaselines updates baselines for all resources from metrics history +func (r *Router) learnBaselines(store *ai.BaselineStore, metricsHistory *monitoring.MetricsHistory) { + if r.monitor == nil { + return + } + + state := r.monitor.GetState() + learningWindow := 7 * 24 * time.Hour // Learn from 7 days of data + var learned int + + // Learn baselines for nodes + for _, node := range state.Nodes { + for _, metric := range []string{"cpu", "memory"} { + points := metricsHistory.GetNodeMetrics(node.ID, metric, learningWindow) + if len(points) > 0 { + baselinePoints := make([]ai.BaselineMetricPoint, len(points)) + for i, p := range points { + baselinePoints[i] = ai.BaselineMetricPoint{Value: p.Value, Timestamp: p.Timestamp} + } + if err := store.Learn(node.ID, "node", metric, baselinePoints); err == nil { + learned++ + } + } + } + } + + // Learn baselines for VMs + for _, vm := range state.VMs { + if vm.Template { + continue + } + for _, metric := range []string{"cpu", "memory", "disk"} { + points := metricsHistory.GetGuestMetrics(vm.ID, metric, learningWindow) + if len(points) > 0 { + baselinePoints := make([]ai.BaselineMetricPoint, len(points)) + for i, p := range points { + baselinePoints[i] = ai.BaselineMetricPoint{Value: p.Value, Timestamp: p.Timestamp} + } + if err := store.Learn(vm.ID, "vm", metric, baselinePoints); err == nil { + learned++ + } + } + } + } + + // Learn baselines for containers + for _, ct := range state.Containers { + if ct.Template { + continue + } + for _, metric := range []string{"cpu", "memory", "disk"} { + points := metricsHistory.GetGuestMetrics(ct.ID, metric, learningWindow) + if len(points) > 0 { + baselinePoints := make([]ai.BaselineMetricPoint, len(points)) + for i, p := range points { + baselinePoints[i] = ai.BaselineMetricPoint{Value: p.Value, Timestamp: p.Timestamp} + } + if err := store.Learn(ct.ID, "container", metric, baselinePoints); err == nil { + learned++ + } + } + } + } + + // Save after learning + if err := store.Save(); err != nil { + log.Warn().Err(err).Msg("Failed to save baselines") + } + + log.Debug(). + Int("baselines_updated", learned). + Int("resources", store.ResourceCount()). + Msg("Baseline learning complete") +} + // GetAlertTriggeredAnalyzer returns the alert-triggered analyzer for wiring into the monitor's alert callback // This enables AI to analyze specific resources when alerts fire, providing token-efficient real-time insights func (r *Router) GetAlertTriggeredAnalyzer() *ai.AlertTriggeredAnalyzer {