feat(ai): Add multi-resource correlation detection (Phase 6)

Create internal/ai/correlation package:

1. Correlation Detector (detector.go):
   - Tracks events across resources
   - Detects when events on one resource follow events on another
   - Calculates average delay between correlated events
   - Confidence scoring based on occurrence count
   - Persists to ai_correlations.json

2. Features:
   - GetCorrelations() - All detected relationships
   - GetCorrelationsForResource() - Relationships for one resource
   - GetDependencies() - What resources depend on this one
   - GetDependsOn() - What this resource depends on
   - PredictCascade() - Predict what will be affected
   - FormatForContext() - AI-consumable summary

3. Integration:
   - Wire to alert history in router startup
   - Map alert types to correlation event types
   - Add correlation context to enriched AI context

Example AI context now includes:
'When local-zfs experiences high usage, database often follows within 5 minutes'

This enables the AI to understand infrastructure dependencies
and predict cascade failures.

All tests passing.
This commit is contained in:
rcourtman 2025-12-12 14:26:10 +00:00
parent 8b3bfb60d2
commit f6d31d166a
7 changed files with 882 additions and 11 deletions

View file

@ -0,0 +1,501 @@
// Package correlation detects relationships between resources.
// It tracks when events on one resource are followed by events on another,
// enabling the AI to understand dependencies and predict cascade failures.
package correlation
import (
"encoding/json"
"os"
"path/filepath"
"sort"
"sync"
"time"
"github.com/rs/zerolog/log"
)
// EventType represents the type of event being correlated
type EventType string
const (
EventAlert EventType = "alert" // Alert triggered
EventRestart EventType = "restart" // Resource restarted
EventHighCPU EventType = "high_cpu" // CPU spike
EventHighMem EventType = "high_mem" // Memory spike
EventDiskFull EventType = "disk_full" // Disk space critical
EventOffline EventType = "offline" // Resource went offline
EventMigration EventType = "migration" // Resource migrated
)
// Event represents a tracked event for correlation analysis
type Event struct {
ID string `json:"id"`
ResourceID string `json:"resource_id"`
ResourceName string `json:"resource_name,omitempty"`
ResourceType string `json:"resource_type,omitempty"` // node, vm, container, storage
EventType EventType `json:"event_type"`
Timestamp time.Time `json:"timestamp"`
Value float64 `json:"value,omitempty"` // Metric value if applicable
}
// Correlation represents a detected relationship between two resources
type Correlation struct {
SourceID string `json:"source_id"` // Resource that triggers
SourceName string `json:"source_name"`
SourceType string `json:"source_type"`
TargetID string `json:"target_id"` // Resource that follows
TargetName string `json:"target_name"`
TargetType string `json:"target_type"`
EventPattern string `json:"event_pattern"` // e.g., "high_mem -> restart"
Occurrences int `json:"occurrences"` // Number of times observed
AvgDelay time.Duration `json:"avg_delay"` // Average time between events
Confidence float64 `json:"confidence"` // 0-1 confidence level
LastSeen time.Time `json:"last_seen"`
Description string `json:"description"`
}
// Detector tracks events and detects correlations between resources
type Detector struct {
mu sync.RWMutex
events []Event
correlations map[string]*Correlation // key: sourceID:targetID:pattern
// Configuration
maxEvents int
correlationWindow time.Duration // How long after source event to look for target
minOccurrences int // Minimum co-occurrences to form correlation
retentionWindow time.Duration // How long to keep events
// Persistence
dataDir string
}
// Config configures the correlation detector
type Config struct {
MaxEvents int
CorrelationWindow time.Duration // Default: 10 minutes
MinOccurrences int // Default: 3
RetentionWindow time.Duration // Default: 30 days
DataDir string
}
// DefaultConfig returns default configuration
func DefaultConfig() Config {
return Config{
MaxEvents: 10000,
CorrelationWindow: 10 * time.Minute,
MinOccurrences: 3,
RetentionWindow: 30 * 24 * time.Hour,
}
}
// NewDetector creates a new correlation detector
func NewDetector(cfg Config) *Detector {
if cfg.MaxEvents <= 0 {
cfg.MaxEvents = 10000
}
if cfg.CorrelationWindow <= 0 {
cfg.CorrelationWindow = 10 * time.Minute
}
if cfg.MinOccurrences <= 0 {
cfg.MinOccurrences = 3
}
if cfg.RetentionWindow <= 0 {
cfg.RetentionWindow = 30 * 24 * time.Hour
}
d := &Detector{
events: make([]Event, 0),
correlations: make(map[string]*Correlation),
maxEvents: cfg.MaxEvents,
correlationWindow: cfg.CorrelationWindow,
minOccurrences: cfg.MinOccurrences,
retentionWindow: cfg.RetentionWindow,
dataDir: cfg.DataDir,
}
// Load existing data
if cfg.DataDir != "" {
if err := d.loadFromDisk(); err != nil {
log.Warn().Err(err).Msg("Failed to load correlation data from disk")
} else if len(d.events) > 0 {
log.Info().Int("events", len(d.events)).Int("correlations", len(d.correlations)).
Msg("Loaded correlation data from disk")
}
}
return d
}
// RecordEvent records a new event for correlation analysis
func (d *Detector) RecordEvent(event Event) {
d.mu.Lock()
defer d.mu.Unlock()
if event.ID == "" {
event.ID = generateEventID()
}
if event.Timestamp.IsZero() {
event.Timestamp = time.Now()
}
d.events = append(d.events, event)
d.trimEvents()
// Check for correlations with recent events on OTHER resources
d.detectCorrelations(event)
// Persist asynchronously
go func() {
if err := d.saveToDisk(); err != nil {
log.Warn().Err(err).Msg("Failed to save correlation data")
}
}()
}
// detectCorrelations looks for patterns where this event follows a recent event on another resource
func (d *Detector) detectCorrelations(newEvent Event) {
cutoff := newEvent.Timestamp.Add(-d.correlationWindow)
for _, oldEvent := range d.events {
// Skip same resource
if oldEvent.ResourceID == newEvent.ResourceID {
continue
}
// Skip events outside the correlation window
if oldEvent.Timestamp.Before(cutoff) || oldEvent.Timestamp.After(newEvent.Timestamp) {
continue
}
// Found a potential correlation: oldEvent -> newEvent
key := correlationKey(oldEvent.ResourceID, newEvent.ResourceID, oldEvent.EventType, newEvent.EventType)
pattern := string(oldEvent.EventType) + " -> " + string(newEvent.EventType)
delay := newEvent.Timestamp.Sub(oldEvent.Timestamp)
if existing, ok := d.correlations[key]; ok {
// Update existing correlation
existing.Occurrences++
existing.AvgDelay = (existing.AvgDelay*time.Duration(existing.Occurrences-1) + delay) / time.Duration(existing.Occurrences)
existing.LastSeen = newEvent.Timestamp
existing.Confidence = d.calculateConfidence(existing.Occurrences)
existing.Description = d.formatCorrelationDescription(existing)
} else {
// Create new correlation
d.correlations[key] = &Correlation{
SourceID: oldEvent.ResourceID,
SourceName: oldEvent.ResourceName,
SourceType: oldEvent.ResourceType,
TargetID: newEvent.ResourceID,
TargetName: newEvent.ResourceName,
TargetType: newEvent.ResourceType,
EventPattern: pattern,
Occurrences: 1,
AvgDelay: delay,
Confidence: 0.1, // Low initial confidence
LastSeen: newEvent.Timestamp,
}
}
}
}
// calculateConfidence returns confidence based on occurrence count
func (d *Detector) calculateConfidence(occurrences int) float64 {
// Confidence grows with occurrences, capped at 0.95
if occurrences < d.minOccurrences {
return float64(occurrences) * 0.1
}
// Logarithmic growth after threshold
confidence := 0.3 + 0.1*float64(occurrences-d.minOccurrences)
if confidence > 0.95 {
confidence = 0.95
}
return confidence
}
// formatCorrelationDescription creates a human-readable description
func (d *Detector) formatCorrelationDescription(c *Correlation) string {
sourceName := c.SourceName
if sourceName == "" {
sourceName = c.SourceID
}
targetName := c.TargetName
if targetName == "" {
targetName = c.TargetID
}
delayStr := formatDuration(c.AvgDelay)
return "When " + sourceName + " experiences " + string(c.EventPattern[:len(c.EventPattern)/2]) +
", " + targetName + " often follows within " + delayStr
}
// GetCorrelations returns all detected correlations above minimum confidence
func (d *Detector) GetCorrelations() []*Correlation {
d.mu.RLock()
defer d.mu.RUnlock()
var result []*Correlation
for _, c := range d.correlations {
if c.Occurrences >= d.minOccurrences && c.Confidence >= 0.3 {
result = append(result, c)
}
}
// Sort by confidence descending
sort.Slice(result, func(i, j int) bool {
return result[i].Confidence > result[j].Confidence
})
return result
}
// GetCorrelationsForResource returns correlations involving a specific resource
func (d *Detector) GetCorrelationsForResource(resourceID string) []*Correlation {
d.mu.RLock()
defer d.mu.RUnlock()
var result []*Correlation
for _, c := range d.correlations {
if (c.SourceID == resourceID || c.TargetID == resourceID) && c.Occurrences >= d.minOccurrences {
result = append(result, c)
}
}
return result
}
// GetDependencies returns resources that depend on the given resource
// (resources that experience events after this resource has an event)
func (d *Detector) GetDependencies(resourceID string) []string {
d.mu.RLock()
defer d.mu.RUnlock()
deps := make(map[string]bool)
for _, c := range d.correlations {
if c.SourceID == resourceID && c.Occurrences >= d.minOccurrences {
deps[c.TargetID] = true
}
}
result := make([]string, 0, len(deps))
for dep := range deps {
result = append(result, dep)
}
return result
}
// GetDependsOn returns resources that the given resource depends on
// (resources whose events precede events on this resource)
func (d *Detector) GetDependsOn(resourceID string) []string {
d.mu.RLock()
defer d.mu.RUnlock()
deps := make(map[string]bool)
for _, c := range d.correlations {
if c.TargetID == resourceID && c.Occurrences >= d.minOccurrences {
deps[c.SourceID] = true
}
}
result := make([]string, 0, len(deps))
for dep := range deps {
result = append(result, dep)
}
return result
}
// PredictCascade predicts what resources might be affected if the given resource has an issue
func (d *Detector) PredictCascade(resourceID string, eventType EventType) []CascadePrediction {
d.mu.RLock()
defer d.mu.RUnlock()
var predictions []CascadePrediction
for _, c := range d.correlations {
if c.SourceID == resourceID && c.Occurrences >= d.minOccurrences {
// Check if the event pattern starts with the given event type
if len(c.EventPattern) > 0 && EventType(c.EventPattern[:len(string(eventType))]) == eventType {
predictions = append(predictions, CascadePrediction{
ResourceID: c.TargetID,
ResourceName: c.TargetName,
EventPattern: c.EventPattern,
ExpectedIn: c.AvgDelay,
Confidence: c.Confidence,
})
}
}
}
// Sort by confidence
sort.Slice(predictions, func(i, j int) bool {
return predictions[i].Confidence > predictions[j].Confidence
})
return predictions
}
// CascadePrediction represents a predicted downstream effect
type CascadePrediction struct {
ResourceID string `json:"resource_id"`
ResourceName string `json:"resource_name"`
EventPattern string `json:"event_pattern"`
ExpectedIn time.Duration `json:"expected_in"`
Confidence float64 `json:"confidence"`
}
// FormatForContext formats correlations for AI consumption
func (d *Detector) FormatForContext(resourceID string) string {
var correlations []*Correlation
if resourceID != "" {
correlations = d.GetCorrelationsForResource(resourceID)
} else {
correlations = d.GetCorrelations()
}
if len(correlations) == 0 {
return ""
}
var result string
result = "\n## 🔗 Resource Correlations\n"
result += "Observed relationships between resources:\n"
for i, c := range correlations {
if i >= 10 { // Limit to 10 correlations
result += "\n... and more\n"
break
}
if c.Description != "" {
result += "- " + c.Description + "\n"
} else {
result += "- " + c.EventPattern + " (" + formatConfidence(c.Confidence) + " confidence)\n"
}
}
return result
}
// trimEvents removes old events
func (d *Detector) trimEvents() {
// Remove events beyond maxEvents
if len(d.events) > d.maxEvents {
d.events = d.events[len(d.events)-d.maxEvents:]
}
// Remove events older than retention window
cutoff := time.Now().Add(-d.retentionWindow)
kept := make([]Event, 0, len(d.events))
for _, e := range d.events {
if e.Timestamp.After(cutoff) {
kept = append(kept, e)
}
}
d.events = kept
}
// saveToDisk persists data
func (d *Detector) saveToDisk() error {
if d.dataDir == "" {
return nil
}
d.mu.RLock()
data := struct {
Events []Event `json:"events"`
Correlations map[string]*Correlation `json:"correlations"`
}{
Events: d.events,
Correlations: d.correlations,
}
d.mu.RUnlock()
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
path := filepath.Join(d.dataDir, "ai_correlations.json")
tmpPath := path + ".tmp"
if err := os.WriteFile(tmpPath, jsonData, 0600); err != nil {
return err
}
return os.Rename(tmpPath, path)
}
// loadFromDisk loads data
func (d *Detector) loadFromDisk() error {
if d.dataDir == "" {
return nil
}
path := filepath.Join(d.dataDir, "ai_correlations.json")
jsonData, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
var data struct {
Events []Event `json:"events"`
Correlations map[string]*Correlation `json:"correlations"`
}
if err := json.Unmarshal(jsonData, &data); err != nil {
return err
}
d.events = data.Events
d.correlations = data.Correlations
return nil
}
// Helper functions
var eventCounter int64
func generateEventID() string {
eventCounter++
return time.Now().Format("20060102150405") + "-" + intToStr(int(eventCounter%1000))
}
func intToStr(n int) string {
if n == 0 {
return "0"
}
var result string
for n > 0 {
result = string(rune('0'+n%10)) + result
n /= 10
}
return result
}
func correlationKey(sourceID, targetID string, sourceType, targetType EventType) string {
return sourceID + ":" + targetID + ":" + string(sourceType) + ":" + string(targetType)
}
func formatDuration(d time.Duration) string {
if d < time.Minute {
return "seconds"
}
if d < time.Hour {
mins := int(d.Minutes())
if mins == 1 {
return "1 minute"
}
return intToStr(mins) + " minutes"
}
hours := int(d.Hours())
if hours == 1 {
return "1 hour"
}
return intToStr(hours) + " hours"
}
func formatConfidence(c float64) string {
pct := int(c * 100)
return intToStr(pct) + "%"
}

View file

@ -0,0 +1,246 @@
package correlation
import (
"testing"
"time"
)
func TestDetector_RecordEvent(t *testing.T) {
d := NewDetector(DefaultConfig())
d.RecordEvent(Event{
ResourceID: "vm-100",
ResourceName: "web-server",
EventType: EventHighMem,
Timestamp: time.Now(),
})
if len(d.events) != 1 {
t.Errorf("Expected 1 event, got %d", len(d.events))
}
}
func TestDetector_CorrelationDetection(t *testing.T) {
d := NewDetector(Config{
MaxEvents: 1000,
CorrelationWindow: 10 * time.Minute,
MinOccurrences: 2, // Lower threshold for testing
RetentionWindow: 30 * 24 * time.Hour,
})
now := time.Now()
// Simulate pattern: when storage has high usage, database VM restarts
// Occurrence 1
d.RecordEvent(Event{
ResourceID: "storage-1",
ResourceName: "local-zfs",
ResourceType: "storage",
EventType: EventDiskFull,
Timestamp: now.Add(-2 * time.Hour),
})
d.RecordEvent(Event{
ResourceID: "vm-100",
ResourceName: "database",
ResourceType: "vm",
EventType: EventRestart,
Timestamp: now.Add(-2*time.Hour + 5*time.Minute),
})
// Occurrence 2
d.RecordEvent(Event{
ResourceID: "storage-1",
ResourceName: "local-zfs",
ResourceType: "storage",
EventType: EventDiskFull,
Timestamp: now.Add(-1 * time.Hour),
})
d.RecordEvent(Event{
ResourceID: "vm-100",
ResourceName: "database",
ResourceType: "vm",
EventType: EventRestart,
Timestamp: now.Add(-1*time.Hour + 5*time.Minute),
})
// Occurrence 3
d.RecordEvent(Event{
ResourceID: "storage-1",
ResourceName: "local-zfs",
ResourceType: "storage",
EventType: EventDiskFull,
Timestamp: now,
})
d.RecordEvent(Event{
ResourceID: "vm-100",
ResourceName: "database",
ResourceType: "vm",
EventType: EventRestart,
Timestamp: now.Add(5 * time.Minute),
})
// Check correlations
correlations := d.GetCorrelations()
found := false
for _, c := range correlations {
if c.SourceID == "storage-1" && c.TargetID == "vm-100" {
found = true
if c.Occurrences < 2 {
t.Errorf("Expected at least 2 occurrences, got %d", c.Occurrences)
}
break
}
}
if !found {
t.Error("Expected correlation between storage-1 and vm-100")
}
}
func TestDetector_GetCorrelationsForResource(t *testing.T) {
d := NewDetector(Config{
MaxEvents: 1000,
CorrelationWindow: 10 * time.Minute,
MinOccurrences: 2,
RetentionWindow: 30 * 24 * time.Hour,
})
now := time.Now()
// Create correlation pattern
for i := 0; i < 3; i++ {
offset := time.Duration(-i) * time.Hour
d.RecordEvent(Event{
ResourceID: "node-1",
EventType: EventHighCPU,
Timestamp: now.Add(offset),
})
d.RecordEvent(Event{
ResourceID: "vm-100",
EventType: EventHighMem,
Timestamp: now.Add(offset + 2*time.Minute),
})
}
// Get correlations for node-1
correlations := d.GetCorrelationsForResource("node-1")
if len(correlations) == 0 {
t.Error("Expected correlations for node-1")
}
}
func TestDetector_GetDependencies(t *testing.T) {
d := NewDetector(Config{
MaxEvents: 1000,
CorrelationWindow: 10 * time.Minute,
MinOccurrences: 3,
RetentionWindow: 30 * 24 * time.Hour,
})
now := time.Now()
// When storage is full, both VM and container have issues
for i := 0; i < 4; i++ {
offset := time.Duration(-i) * time.Hour
d.RecordEvent(Event{
ResourceID: "storage-1",
EventType: EventDiskFull,
Timestamp: now.Add(offset),
})
d.RecordEvent(Event{
ResourceID: "vm-100",
EventType: EventRestart,
Timestamp: now.Add(offset + 3*time.Minute),
})
d.RecordEvent(Event{
ResourceID: "ct-200",
EventType: EventRestart,
Timestamp: now.Add(offset + 5*time.Minute),
})
}
deps := d.GetDependencies("storage-1")
if len(deps) < 2 {
t.Errorf("Expected at least 2 dependencies, got %d", len(deps))
}
}
func TestDetector_PredictCascade(t *testing.T) {
d := NewDetector(Config{
MaxEvents: 1000,
CorrelationWindow: 10 * time.Minute,
MinOccurrences: 3,
RetentionWindow: 30 * 24 * time.Hour,
})
now := time.Now()
// Create strong correlation
for i := 0; i < 5; i++ {
offset := time.Duration(-i) * time.Hour
d.RecordEvent(Event{
ResourceID: "node-1",
ResourceName: "pve-main",
EventType: EventHighMem,
Timestamp: now.Add(offset),
})
d.RecordEvent(Event{
ResourceID: "vm-100",
ResourceName: "database",
EventType: EventRestart,
Timestamp: now.Add(offset + 5*time.Minute),
})
}
predictions := d.PredictCascade("node-1", EventHighMem)
if len(predictions) == 0 {
t.Error("Expected cascade predictions")
}
}
func TestDetector_FormatForContext(t *testing.T) {
d := NewDetector(Config{
MaxEvents: 1000,
CorrelationWindow: 10 * time.Minute,
MinOccurrences: 2,
RetentionWindow: 30 * 24 * time.Hour,
})
now := time.Now()
// Create correlations
for i := 0; i < 3; i++ {
offset := time.Duration(-i) * time.Hour
d.RecordEvent(Event{
ResourceID: "storage-1",
ResourceName: "local-zfs",
EventType: EventDiskFull,
Timestamp: now.Add(offset),
})
d.RecordEvent(Event{
ResourceID: "vm-100",
ResourceName: "database",
EventType: EventRestart,
Timestamp: now.Add(offset + 5*time.Minute),
})
}
context := d.FormatForContext("")
if context == "" {
t.Error("Expected non-empty context")
}
if !contains(context, "Correlation") {
t.Errorf("Expected context to mention correlations: %s", context)
}
}
func contains(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

View file

@ -0,0 +1,44 @@
package ai
import (
"github.com/rcourtman/pulse-go-rewrite/internal/ai/correlation"
)
// CorrelationDetector is an alias for correlation.Detector
type CorrelationDetector = correlation.Detector
// CorrelationConfig is an alias for correlation.Config
type CorrelationConfig = correlation.Config
// CorrelationEvent is an alias for correlation.Event
type CorrelationEvent = correlation.Event
// Correlation is an alias for correlation.Correlation
type Correlation = correlation.Correlation
// CascadePrediction is an alias for correlation.CascadePrediction
type CascadePrediction = correlation.CascadePrediction
// CorrelationEventType is an alias for correlation.EventType
type CorrelationEventType = correlation.EventType
// Event type constants
const (
CorrelationEventAlert = correlation.EventAlert
CorrelationEventRestart = correlation.EventRestart
CorrelationEventHighCPU = correlation.EventHighCPU
CorrelationEventHighMem = correlation.EventHighMem
CorrelationEventDiskFull = correlation.EventDiskFull
CorrelationEventOffline = correlation.EventOffline
CorrelationEventMigration = correlation.EventMigration
)
// NewCorrelationDetector creates a new correlation detector
func NewCorrelationDetector(cfg CorrelationConfig) *CorrelationDetector {
return correlation.NewDetector(cfg)
}
// DefaultCorrelationConfig returns default correlation detector configuration
func DefaultCorrelationConfig() CorrelationConfig {
return correlation.DefaultConfig()
}

View file

@ -204,17 +204,18 @@ const MaxPatrolRunHistory = 100
type PatrolService struct {
mu sync.RWMutex
aiService *Service
stateProvider StateProvider
thresholdProvider ThresholdProvider
config PatrolConfig
findings *FindingsStore
knowledgeStore *knowledge.Store // For per-resource notes in patrol context
metricsHistory MetricsHistoryProvider // For trend analysis and predictions
baselineStore *baseline.Store // For anomaly detection via learned baselines
changeDetector *ChangeDetector // For tracking infrastructure changes
remediationLog *RemediationLog // For tracking remediation actions
patternDetector *PatternDetector // For failure prediction from historical patterns
aiService *Service
stateProvider StateProvider
thresholdProvider ThresholdProvider
config PatrolConfig
findings *FindingsStore
knowledgeStore *knowledge.Store // For per-resource notes in patrol context
metricsHistory MetricsHistoryProvider // For trend analysis and predictions
baselineStore *baseline.Store // For anomaly detection via learned baselines
changeDetector *ChangeDetector // For tracking infrastructure changes
remediationLog *RemediationLog // For tracking remediation actions
patternDetector *PatternDetector // For failure prediction from historical patterns
correlationDetector *CorrelationDetector // For multi-resource correlation
// Cached thresholds (recalculated when thresholdProvider changes)
thresholds PatrolThresholds
@ -399,6 +400,21 @@ func (p *PatrolService) GetPatternDetector() *PatternDetector {
return p.patternDetector
}
// SetCorrelationDetector sets the correlation detector for multi-resource correlation
func (p *PatrolService) SetCorrelationDetector(detector *CorrelationDetector) {
p.mu.Lock()
defer p.mu.Unlock()
p.correlationDetector = detector
log.Info().Msg("AI Patrol: Correlation detector set for multi-resource analysis")
}
// GetCorrelationDetector returns the correlation detector
func (p *PatrolService) GetCorrelationDetector() *CorrelationDetector {
p.mu.RLock()
defer p.mu.RUnlock()
return p.correlationDetector
}
// GetConfig returns the current patrol configuration
func (p *PatrolService) GetConfig() PatrolConfig {
p.mu.RLock()
@ -1802,6 +1818,7 @@ func (p *PatrolService) buildEnrichedContext(state models.StateSnapshot) string
// Append failure predictions if pattern detector is available
p.mu.RLock()
patternDetector := p.patternDetector
correlationDetector := p.correlationDetector
p.mu.RUnlock()
if patternDetector != nil {
@ -1810,6 +1827,14 @@ func (p *PatrolService) buildEnrichedContext(state models.StateSnapshot) string
formatted += predictionsContext
}
}
// Append resource correlations if correlation detector is available
if correlationDetector != nil {
correlationsContext := correlationDetector.FormatForContext("")
if correlationsContext != "" {
formatted += correlationsContext
}
}
log.Debug().
Int("resources", infraCtx.TotalResources).

View file

@ -252,6 +252,17 @@ func (s *Service) SetPatternDetector(detector *PatternDetector) {
}
}
// SetCorrelationDetector sets the correlation detector for multi-resource correlation
func (s *Service) SetCorrelationDetector(detector *CorrelationDetector) {
s.mu.RLock()
patrol := s.patrolService
s.mu.RUnlock()
if patrol != nil {
patrol.SetCorrelationDetector(detector)
}
}
// StartPatrol starts the background patrol service
func (s *Service) StartPatrol(ctx context.Context) {
s.mu.RLock()

View file

@ -120,6 +120,11 @@ func (h *AISettingsHandler) SetPatternDetector(detector *ai.PatternDetector) {
h.aiService.SetPatternDetector(detector)
}
// SetCorrelationDetector sets the correlation detector for multi-resource correlation
func (h *AISettingsHandler) SetCorrelationDetector(detector *ai.CorrelationDetector) {
h.aiService.SetCorrelationDetector(detector)
}
// StopPatrol stops the background AI patrol service
func (h *AISettingsHandler) StopPatrol() {
h.aiService.StopPatrol()

View file

@ -1480,6 +1480,45 @@ func (r *Router) StartPatrol(ctx context.Context) {
log.Info().Msg("AI Pattern Detector: Wired to alert history for failure prediction")
}
}
// Initialize correlation detector for multi-resource relationships
correlationDetector := ai.NewCorrelationDetector(ai.CorrelationConfig{
MaxEvents: 10000,
CorrelationWindow: 10 * time.Minute,
MinOccurrences: 3,
RetentionWindow: 30 * 24 * time.Hour,
DataDir: dataDir,
})
if correlationDetector != nil {
r.aiSettingsHandler.SetCorrelationDetector(correlationDetector)
// Wire alert history to correlation detector
if alertManager := r.monitor.GetAlertManager(); alertManager != nil {
alertManager.OnAlertHistory(func(alert alerts.Alert) {
// Record as correlation event
eventType := ai.CorrelationEventType(ai.CorrelationEventAlert)
switch alert.Type {
case "cpu":
eventType = ai.CorrelationEventHighCPU
case "memory":
eventType = ai.CorrelationEventHighMem
case "disk":
eventType = ai.CorrelationEventDiskFull
case "offline", "connectivity":
eventType = ai.CorrelationEventOffline
}
correlationDetector.RecordEvent(ai.CorrelationEvent{
ResourceID: alert.ResourceID,
ResourceName: alert.ResourceName,
ResourceType: alert.Type,
EventType: eventType,
Timestamp: alert.StartTime,
Value: alert.Value,
})
})
log.Info().Msg("AI Correlation Detector: Wired to alert history for multi-resource analysis")
}
}
r.aiSettingsHandler.StartPatrol(ctx)
}