mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-06 16:16:26 +00:00
1362 lines
38 KiB
Go
1362 lines
38 KiB
Go
// Package ai provides AI-powered infrastructure monitoring and investigation.
|
|
// This file contains the unified AIIntelligence orchestrator that ties together
|
|
// all AI subsystems into one coherent intelligence layer.
|
|
package ai
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/baseline"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/correlation"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/knowledge"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/memory"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/patterns"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
// HealthGrade represents the overall health assessment
|
|
type HealthGrade string
|
|
|
|
const (
|
|
HealthGradeA HealthGrade = "A" // Excellent - no issues
|
|
HealthGradeB HealthGrade = "B" // Good - minor issues
|
|
HealthGradeC HealthGrade = "C" // Fair - some concerns
|
|
HealthGradeD HealthGrade = "D" // Poor - needs attention
|
|
HealthGradeF HealthGrade = "F" // Critical - immediate action needed
|
|
)
|
|
|
|
// HealthTrend indicates the direction of health over time
|
|
type HealthTrend string
|
|
|
|
const (
|
|
HealthTrendImproving HealthTrend = "improving"
|
|
HealthTrendStable HealthTrend = "stable"
|
|
HealthTrendDeclining HealthTrend = "declining"
|
|
)
|
|
|
|
// HealthFactor represents a single component affecting health
|
|
type HealthFactor struct {
|
|
Name string `json:"name"`
|
|
Impact float64 `json:"impact"` // -1 to 1, negative is bad
|
|
Description string `json:"description"`
|
|
Category string `json:"category"` // finding, prediction, baseline, incident
|
|
}
|
|
|
|
// HealthScore represents the overall health of a resource or system
|
|
type HealthScore struct {
|
|
Score float64 `json:"score"` // 0-100
|
|
Grade HealthGrade `json:"grade"` // A, B, C, D, F
|
|
Trend HealthTrend `json:"trend"` // improving, stable, declining
|
|
Factors []HealthFactor `json:"factors"` // What's affecting the score
|
|
Prediction string `json:"prediction"` // Human-readable outlook
|
|
}
|
|
|
|
// ResourceIntelligence aggregates all AI knowledge about a single resource
|
|
type ResourceIntelligence struct {
|
|
ResourceID string `json:"resource_id"`
|
|
ResourceName string `json:"resource_name,omitempty"`
|
|
ResourceType string `json:"resource_type,omitempty"`
|
|
Health HealthScore `json:"health"`
|
|
ActiveFindings []*Finding `json:"active_findings,omitempty"`
|
|
Predictions []patterns.FailurePrediction `json:"predictions,omitempty"`
|
|
Dependencies []string `json:"dependencies,omitempty"` // Resources this depends on
|
|
Dependents []string `json:"dependents,omitempty"` // Resources that depend on this
|
|
Correlations []*correlation.Correlation `json:"correlations,omitempty"`
|
|
Baselines map[string]*baseline.FlatBaseline `json:"baselines,omitempty"`
|
|
Anomalies []AnomalyReport `json:"anomalies,omitempty"`
|
|
RecentIncidents []*memory.Incident `json:"recent_incidents,omitempty"`
|
|
RecentChanges []unifiedresources.ResourceChange `json:"recent_changes,omitempty"`
|
|
Knowledge *knowledge.GuestKnowledge `json:"knowledge,omitempty"`
|
|
NoteCount int `json:"note_count"`
|
|
}
|
|
|
|
// AnomalyReport describes a metric that's deviating from baseline
|
|
type AnomalyReport struct {
|
|
Metric string `json:"metric"`
|
|
CurrentValue float64 `json:"current_value"`
|
|
BaselineMean float64 `json:"baseline_mean"`
|
|
ZScore float64 `json:"z_score"`
|
|
Severity baseline.AnomalySeverity `json:"severity"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// IntelligenceSummary provides a system-wide intelligence overview
|
|
type IntelligenceSummary struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
OverallHealth HealthScore `json:"overall_health"`
|
|
|
|
// Findings summary
|
|
FindingsCount FindingsCounts `json:"findings_count"`
|
|
TopFindings []*Finding `json:"top_findings,omitempty"` // Most critical
|
|
|
|
// Predictions
|
|
PredictionsCount int `json:"predictions_count"`
|
|
UpcomingRisks []patterns.FailurePrediction `json:"upcoming_risks,omitempty"`
|
|
|
|
// Recent activity
|
|
RecentChangesCount int `json:"recent_changes_count"`
|
|
RecentChanges []unifiedresources.ResourceChange `json:"recent_changes,omitempty"`
|
|
RecentRemediations []memory.RemediationRecord `json:"recent_remediations,omitempty"`
|
|
|
|
// Data governance posture
|
|
PolicyPosture *unifiedresources.PolicyPostureSummary `json:"policy_posture,omitempty"`
|
|
|
|
// Learning progress
|
|
Learning LearningStats `json:"learning"`
|
|
|
|
// Resources needing attention
|
|
ResourcesAtRisk []ResourceRiskSummary `json:"resources_at_risk,omitempty"`
|
|
}
|
|
|
|
// FindingsCounts provides a breakdown of findings by severity
|
|
type FindingsCounts struct {
|
|
Critical int `json:"critical"`
|
|
Warning int `json:"warning"`
|
|
Watch int `json:"watch"`
|
|
Info int `json:"info"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
// LearningStats shows how much the AI has learned
|
|
type LearningStats struct {
|
|
ResourcesWithKnowledge int `json:"resources_with_knowledge"`
|
|
TotalNotes int `json:"total_notes"`
|
|
ResourcesWithBaselines int `json:"resources_with_baselines"`
|
|
PatternsDetected int `json:"patterns_detected"`
|
|
CorrelationsLearned int `json:"correlations_learned"`
|
|
IncidentsTracked int `json:"incidents_tracked"`
|
|
}
|
|
|
|
// ResourceRiskSummary is a brief summary of a resource at risk
|
|
type ResourceRiskSummary struct {
|
|
ResourceID string `json:"resource_id"`
|
|
ResourceName string `json:"resource_name"`
|
|
ResourceType string `json:"resource_type"`
|
|
Health HealthScore `json:"health"`
|
|
TopIssue string `json:"top_issue"`
|
|
}
|
|
|
|
// Intelligence orchestrates all AI subsystems into a unified system
|
|
type Intelligence struct {
|
|
mu sync.RWMutex
|
|
|
|
// Core subsystems
|
|
findings *FindingsStore
|
|
patterns *patterns.Detector
|
|
correlations *correlation.Detector
|
|
baselines *baseline.Store
|
|
incidents *memory.IncidentStore
|
|
knowledge *knowledge.Store
|
|
changes *memory.ChangeDetector
|
|
remediations *memory.RemediationLog
|
|
runHistoryStore *PatrolRunHistoryStore
|
|
resourceTimelineStore unifiedresources.ResourceStore
|
|
resourceTimelineStoreOrgID string
|
|
unifiedResourceProvider UnifiedResourceProvider
|
|
|
|
// State access
|
|
stateProvider StateProvider
|
|
|
|
// Optional hook for anomaly detection (used by patrol integration/tests)
|
|
anomalyDetector func(resourceID string) []AnomalyReport
|
|
|
|
// Configuration
|
|
dataDir string
|
|
}
|
|
|
|
const (
|
|
intelligencePatrolCoverageWindow = 24 * time.Hour
|
|
intelligenceRecentRunLimit = 10
|
|
)
|
|
|
|
// IntelligenceConfig configures the unified intelligence layer
|
|
type IntelligenceConfig struct {
|
|
DataDir string
|
|
}
|
|
|
|
// NewIntelligence creates a new unified intelligence orchestrator
|
|
func NewIntelligence(cfg IntelligenceConfig) *Intelligence {
|
|
return &Intelligence{
|
|
dataDir: cfg.DataDir,
|
|
}
|
|
}
|
|
|
|
// SetSubsystems wires up all the AI subsystems
|
|
func (i *Intelligence) SetSubsystems(
|
|
findings *FindingsStore,
|
|
patternsDetector *patterns.Detector,
|
|
correlationsDetector *correlation.Detector,
|
|
baselinesStore *baseline.Store,
|
|
incidentsStore *memory.IncidentStore,
|
|
knowledgeStore *knowledge.Store,
|
|
changesDetector *memory.ChangeDetector,
|
|
remediationsLog *memory.RemediationLog,
|
|
) {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
|
|
i.findings = findings
|
|
i.patterns = patternsDetector
|
|
i.correlations = correlationsDetector
|
|
i.baselines = baselinesStore
|
|
i.incidents = incidentsStore
|
|
i.knowledge = knowledgeStore
|
|
i.changes = changesDetector
|
|
i.remediations = remediationsLog
|
|
}
|
|
|
|
// SetResourceTimelineStore wires the canonical unified-resource timeline used
|
|
// for recent change summaries when available.
|
|
func (i *Intelligence) SetResourceTimelineStore(store unifiedresources.ResourceStore, orgID string) {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
i.resourceTimelineStore = store
|
|
i.resourceTimelineStoreOrgID = strings.TrimSpace(orgID)
|
|
}
|
|
|
|
// SetRunHistoryStore wires Patrol run history so intelligence summaries can
|
|
// distinguish broad successful coverage from recent scoped or incomplete runs.
|
|
func (i *Intelligence) SetRunHistoryStore(store *PatrolRunHistoryStore) {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
i.runHistoryStore = store
|
|
}
|
|
|
|
// SetUnifiedResourceProvider wires the canonical unified resource provider used
|
|
// for infrastructure-wide posture summaries.
|
|
func (i *Intelligence) SetUnifiedResourceProvider(urp UnifiedResourceProvider) {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
i.unifiedResourceProvider = urp
|
|
}
|
|
|
|
// SetStateProvider sets the state provider for current metrics
|
|
func (i *Intelligence) SetStateProvider(sp StateProvider) {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
i.stateProvider = sp
|
|
}
|
|
|
|
// GetSummary returns a comprehensive intelligence summary
|
|
func (i *Intelligence) GetSummary() *IntelligenceSummary {
|
|
summary := &IntelligenceSummary{
|
|
Timestamp: time.Now(),
|
|
}
|
|
|
|
i.mu.RLock()
|
|
findings := i.findings
|
|
patternsDetector := i.patterns
|
|
remediations := i.remediations
|
|
runHistoryStore := i.runHistoryStore
|
|
unifiedResourceProvider := i.unifiedResourceProvider
|
|
i.mu.RUnlock()
|
|
|
|
// Aggregate findings
|
|
if findings != nil {
|
|
all := findings.GetActive(FindingSeverityInfo) // Get all active findings
|
|
summary.FindingsCount = i.countFindings(all)
|
|
summary.TopFindings = i.getTopFindings(all, 5)
|
|
}
|
|
|
|
// Aggregate predictions
|
|
if patternsDetector != nil {
|
|
predictions := patternsDetector.GetPredictions()
|
|
summary.PredictionsCount = len(predictions)
|
|
summary.UpcomingRisks = i.getUpcomingRisks(predictions, 5)
|
|
}
|
|
|
|
// Aggregate recent activity
|
|
if recent := i.GetRecentChanges(time.Now().Add(-24*time.Hour), 100); len(recent) > 0 {
|
|
summary.RecentChangesCount = len(recent)
|
|
summary.RecentChanges = append([]unifiedresources.ResourceChange{}, recent[:min(len(recent), 5)]...)
|
|
}
|
|
|
|
if remediations != nil {
|
|
recent := remediations.GetRecentRemediations(5, time.Now().Add(-24*time.Hour))
|
|
summary.RecentRemediations = recent
|
|
}
|
|
|
|
if unifiedResourceProvider != nil {
|
|
summary.PolicyPosture = unifiedresources.SummarizePolicyPosture(
|
|
unifiedresources.RefreshCanonicalMetadataSlice(unifiedResourceProvider.GetAll()),
|
|
)
|
|
}
|
|
|
|
// Learning stats
|
|
summary.Learning = i.getLearningStats()
|
|
|
|
// Calculate overall health
|
|
summary.OverallHealth = i.calculateOverallHealth(summary, runHistoryStore)
|
|
|
|
// Resources at risk
|
|
summary.ResourcesAtRisk = i.getResourcesAtRisk(5)
|
|
|
|
return summary
|
|
}
|
|
|
|
// GetResourceIntelligence returns aggregated intelligence for a specific resource
|
|
func (i *Intelligence) GetResourceIntelligence(resourceID string) *ResourceIntelligence {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
|
|
intel := &ResourceIntelligence{
|
|
ResourceID: resourceID,
|
|
}
|
|
|
|
// Active findings
|
|
if i.findings != nil {
|
|
intel.ActiveFindings = i.findings.GetByResource(resourceID)
|
|
if len(intel.ActiveFindings) > 0 {
|
|
intel.ResourceName = intel.ActiveFindings[0].ResourceName
|
|
intel.ResourceType = intel.ActiveFindings[0].ResourceType
|
|
}
|
|
}
|
|
|
|
// Predictions
|
|
if i.patterns != nil {
|
|
intel.Predictions = i.patterns.GetPredictionsForResource(resourceID)
|
|
}
|
|
|
|
// Correlations and dependencies
|
|
if i.correlations != nil {
|
|
intel.Correlations = i.correlations.GetCorrelationsForResource(resourceID)
|
|
intel.Dependencies = i.correlations.GetDependsOn(resourceID)
|
|
intel.Dependents = i.correlations.GetDependencies(resourceID)
|
|
}
|
|
|
|
// Baselines
|
|
if i.baselines != nil {
|
|
if rb, ok := i.baselines.GetResourceBaseline(resourceID); ok {
|
|
intel.Baselines = make(map[string]*baseline.FlatBaseline)
|
|
for metric, mb := range rb.Metrics {
|
|
intel.Baselines[metric] = &baseline.FlatBaseline{
|
|
ResourceID: resourceID,
|
|
Metric: metric,
|
|
Mean: mb.Mean,
|
|
StdDev: mb.StdDev,
|
|
Samples: mb.SampleCount,
|
|
LastUpdate: rb.LastUpdated,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recent incidents
|
|
if i.incidents != nil {
|
|
intel.RecentIncidents = i.incidents.ListIncidentsByResource(resourceID, 5)
|
|
}
|
|
|
|
// Recent changes
|
|
if recentChanges := i.getRecentChangesForResource(resourceID, 5); len(recentChanges) > 0 {
|
|
intel.RecentChanges = recentChanges
|
|
}
|
|
|
|
// Knowledge
|
|
if i.knowledge != nil {
|
|
if k, err := i.knowledge.GetKnowledge(resourceID); err == nil && k != nil {
|
|
intel.Knowledge = k
|
|
intel.NoteCount = len(k.Notes)
|
|
if intel.ResourceName == "" && k.GuestName != "" {
|
|
intel.ResourceName = k.GuestName
|
|
}
|
|
if intel.ResourceType == "" && k.GuestType != "" {
|
|
intel.ResourceType = k.GuestType
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate health score
|
|
intel.Health = i.calculateResourceHealth(intel)
|
|
|
|
return intel
|
|
}
|
|
|
|
// HasRecentChangesSource reports whether the intelligence layer has any source
|
|
// available for recent infrastructure changes.
|
|
func (i *Intelligence) HasRecentChangesSource() bool {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
return i.resourceTimelineStore != nil || i.changes != nil
|
|
}
|
|
|
|
// DescribeResource returns the canonical display name and type for a resource
|
|
// when the unified resource provider is available.
|
|
func (i *Intelligence) DescribeResource(resourceID string) (string, string) {
|
|
resourceID = strings.TrimSpace(resourceID)
|
|
if resourceID == "" {
|
|
return "", ""
|
|
}
|
|
|
|
i.mu.RLock()
|
|
unifiedResourceProvider := i.unifiedResourceProvider
|
|
i.mu.RUnlock()
|
|
if unifiedResourceProvider == nil {
|
|
return "", ""
|
|
}
|
|
|
|
for _, resource := range unifiedresources.RefreshCanonicalMetadataSlice(unifiedResourceProvider.GetAll()) {
|
|
if strings.TrimSpace(resource.ID) != resourceID {
|
|
continue
|
|
}
|
|
return unifiedresources.ResourceDisplayName(resource), string(resource.Type)
|
|
}
|
|
|
|
return "", ""
|
|
}
|
|
|
|
// GetRecentChanges returns the most recent infrastructure changes across the
|
|
// canonical unified-resource timeline, with patrol-local memory as fallback.
|
|
func (i *Intelligence) GetRecentChanges(since time.Time, limit int) []unifiedresources.ResourceChange {
|
|
if limit <= 0 {
|
|
return nil
|
|
}
|
|
|
|
i.mu.RLock()
|
|
resourceTimelineStore := i.resourceTimelineStore
|
|
changesDetector := i.changes
|
|
i.mu.RUnlock()
|
|
|
|
if resourceTimelineStore != nil {
|
|
if recent, err := resourceTimelineStore.GetRecentChanges("", since, limit); err == nil && len(recent) > 0 {
|
|
return recent
|
|
}
|
|
}
|
|
|
|
if changesDetector == nil {
|
|
return nil
|
|
}
|
|
|
|
recent := changesDetector.GetRecentChanges(limit, since)
|
|
if len(recent) == 0 {
|
|
return nil
|
|
}
|
|
|
|
converted := make([]unifiedresources.ResourceChange, 0, len(recent))
|
|
for _, change := range recent {
|
|
converted = append(converted, memory.ResourceChangeFromMemoryChange(change))
|
|
}
|
|
return converted
|
|
}
|
|
|
|
// FormatContext builds a comprehensive context string for AI prompts
|
|
func (i *Intelligence) FormatContext(resourceID string) string {
|
|
i.mu.RLock()
|
|
knowledgeStore := i.knowledge
|
|
baselinesStore := i.baselines
|
|
patternsDetector := i.patterns
|
|
correlationsDetector := i.correlations
|
|
incidentsStore := i.incidents
|
|
resourceTimelineStore := i.resourceTimelineStore
|
|
changesDetector := i.changes
|
|
anomalyDetector := i.anomalyDetector
|
|
i.mu.RUnlock()
|
|
|
|
var sections []string
|
|
|
|
// Knowledge (most important - what we've learned)
|
|
if knowledgeStore != nil {
|
|
if ctx := knowledgeStore.FormatForContext(resourceID); ctx != "" {
|
|
sections = append(sections, ctx)
|
|
}
|
|
}
|
|
|
|
// Baselines (what's normal for this resource)
|
|
if baselinesStore != nil {
|
|
if ctx := i.formatBaselinesForContext(resourceID); ctx != "" {
|
|
sections = append(sections, ctx)
|
|
}
|
|
}
|
|
|
|
// Current anomalies
|
|
if anomalyDetector != nil {
|
|
if anomalies := anomalyDetector(resourceID); len(anomalies) > 0 {
|
|
sections = append(sections, i.formatAnomaliesForContext(anomalies))
|
|
}
|
|
}
|
|
|
|
// Canonical recent changes (preferred) with patrol-local fallback
|
|
if recentChanges := i.buildRecentChangesContext(
|
|
resourceID,
|
|
resourceTimelineStore,
|
|
changesDetector,
|
|
false,
|
|
5,
|
|
); recentChanges != "" {
|
|
sections = append(sections, recentChanges)
|
|
}
|
|
|
|
// Patterns/Predictions
|
|
if patternsDetector != nil {
|
|
if ctx := patternsDetector.FormatForContext(resourceID); ctx != "" {
|
|
sections = append(sections, ctx)
|
|
}
|
|
}
|
|
|
|
// Correlations
|
|
if correlationsDetector != nil {
|
|
if ctx := correlationsDetector.FormatForContext(resourceID); ctx != "" {
|
|
sections = append(sections, ctx)
|
|
}
|
|
}
|
|
|
|
// Incidents
|
|
if incidentsStore != nil {
|
|
if ctx := incidentsStore.FormatForResource(resourceID, 5); ctx != "" {
|
|
sections = append(sections, ctx)
|
|
}
|
|
}
|
|
|
|
return strings.Join(sections, "\n")
|
|
}
|
|
|
|
// FormatGlobalContext builds context for infrastructure-wide analysis
|
|
func (i *Intelligence) FormatGlobalContext() string {
|
|
i.mu.RLock()
|
|
knowledgeStore := i.knowledge
|
|
incidentsStore := i.incidents
|
|
correlationsDetector := i.correlations
|
|
patternsDetector := i.patterns
|
|
resourceTimelineStore := i.resourceTimelineStore
|
|
changesDetector := i.changes
|
|
i.mu.RUnlock()
|
|
|
|
var sections []string
|
|
|
|
// All saved knowledge (limited)
|
|
if knowledgeStore != nil {
|
|
if ctx := knowledgeStore.FormatAllForContext(); ctx != "" {
|
|
sections = append(sections, ctx)
|
|
}
|
|
}
|
|
|
|
// Canonical recent changes across infrastructure (preferred) with patrol-local fallback
|
|
if recentChanges := i.buildRecentChangesContext(
|
|
"",
|
|
resourceTimelineStore,
|
|
changesDetector,
|
|
true,
|
|
5,
|
|
); recentChanges != "" {
|
|
sections = append(sections, recentChanges)
|
|
}
|
|
|
|
// Recent incidents across infrastructure
|
|
if incidentsStore != nil {
|
|
if ctx := incidentsStore.FormatForPatrol(8); ctx != "" {
|
|
sections = append(sections, ctx)
|
|
}
|
|
}
|
|
|
|
// Top correlations
|
|
if correlationsDetector != nil {
|
|
if ctx := correlationsDetector.FormatForContext(""); ctx != "" {
|
|
sections = append(sections, ctx)
|
|
}
|
|
}
|
|
|
|
// Top predictions
|
|
if patternsDetector != nil {
|
|
if ctx := patternsDetector.FormatForContext(""); ctx != "" {
|
|
sections = append(sections, ctx)
|
|
}
|
|
}
|
|
|
|
return strings.Join(sections, "\n")
|
|
}
|
|
|
|
func (i *Intelligence) buildRecentChangesContext(resourceID string, resourceTimelineStore unifiedresources.ResourceStore, changesDetector *memory.ChangeDetector, includeResourcePrefix bool, limit int) string {
|
|
since := time.Now().Add(-24 * time.Hour)
|
|
|
|
if resourceTimelineStore != nil {
|
|
if recent, err := resourceTimelineStore.GetRecentChanges(resourceID, since, limit); err == nil && len(recent) > 0 {
|
|
return unifiedresources.FormatResourceRecentChangesContext(recent, includeResourcePrefix, "##")
|
|
}
|
|
}
|
|
|
|
if changesDetector == nil {
|
|
return ""
|
|
}
|
|
|
|
var recent []memory.Change
|
|
if resourceID != "" {
|
|
recent = changesDetector.GetChangesForResource(resourceID, limit)
|
|
} else {
|
|
recent = changesDetector.GetRecentChanges(limit, since)
|
|
}
|
|
if len(recent) == 0 {
|
|
return ""
|
|
}
|
|
return memory.FormatRecentChangesContext(recent, includeResourcePrefix, "##")
|
|
}
|
|
|
|
// RecordLearning saves a learning to the knowledge store after a fix
|
|
func (i *Intelligence) RecordLearning(resourceID, resourceName, resourceType, title, content string) error {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
|
|
if i.knowledge == nil {
|
|
return nil
|
|
}
|
|
|
|
return i.knowledge.SaveNote(resourceID, resourceName, resourceType, "learning", title, content)
|
|
}
|
|
|
|
// CheckBaselinesForResource checks current metrics against baselines and returns anomalies
|
|
func (i *Intelligence) CheckBaselinesForResource(resourceID string, metrics map[string]float64) []AnomalyReport {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
|
|
if i.baselines == nil {
|
|
return nil
|
|
}
|
|
|
|
var anomalies []AnomalyReport
|
|
for metric, value := range metrics {
|
|
severity, zScore, bl := i.baselines.CheckAnomaly(resourceID, metric, value)
|
|
if severity != baseline.AnomalyNone && bl != nil {
|
|
anomalies = append(anomalies, AnomalyReport{
|
|
Metric: metric,
|
|
CurrentValue: value,
|
|
BaselineMean: bl.Mean,
|
|
ZScore: zScore,
|
|
Severity: severity,
|
|
Description: i.formatAnomalyDescription(metric, value, bl, zScore),
|
|
})
|
|
}
|
|
}
|
|
|
|
return anomalies
|
|
}
|
|
|
|
// CreatePredictionFinding creates a finding from a prediction that's imminent
|
|
func (i *Intelligence) CreatePredictionFinding(pred patterns.FailurePrediction) *Finding {
|
|
severity := FindingSeverityWatch
|
|
if pred.DaysUntil < 1 {
|
|
severity = FindingSeverityWarning
|
|
}
|
|
if pred.Confidence > 0.8 && pred.DaysUntil < 1 {
|
|
severity = FindingSeverityCritical
|
|
}
|
|
|
|
return &Finding{
|
|
ID: fmt.Sprintf("pred-%s-%s", pred.ResourceID, pred.EventType),
|
|
Key: fmt.Sprintf("prediction:%s:%s", pred.ResourceID, pred.EventType),
|
|
Severity: severity,
|
|
Category: FindingCategoryReliability,
|
|
ResourceID: pred.ResourceID,
|
|
Title: fmt.Sprintf("Predicted: %s", pred.EventType),
|
|
Description: pred.Basis,
|
|
DetectedAt: time.Now(),
|
|
LastSeenAt: time.Now(),
|
|
}
|
|
}
|
|
|
|
// Helper methods
|
|
|
|
func (i *Intelligence) countFindings(findings []*Finding) FindingsCounts {
|
|
counts := FindingsCounts{}
|
|
for _, f := range findings {
|
|
if f == nil {
|
|
continue
|
|
}
|
|
counts.Total++
|
|
switch f.Severity {
|
|
case FindingSeverityCritical:
|
|
counts.Critical++
|
|
case FindingSeverityWarning:
|
|
counts.Warning++
|
|
case FindingSeverityWatch:
|
|
counts.Watch++
|
|
case FindingSeverityInfo:
|
|
counts.Info++
|
|
}
|
|
}
|
|
return counts
|
|
}
|
|
|
|
func (i *Intelligence) getTopFindings(findings []*Finding, limit int) []*Finding {
|
|
if len(findings) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Sort by severity (critical first) then by detection time (newest first)
|
|
sorted := make([]*Finding, len(findings))
|
|
copy(sorted, findings)
|
|
sort.Slice(sorted, func(a, b int) bool {
|
|
sevA := severityOrder(sorted[a].Severity)
|
|
sevB := severityOrder(sorted[b].Severity)
|
|
if sevA != sevB {
|
|
return sevA < sevB
|
|
}
|
|
return sorted[a].DetectedAt.After(sorted[b].DetectedAt)
|
|
})
|
|
|
|
if len(sorted) > limit {
|
|
sorted = sorted[:limit]
|
|
}
|
|
return sorted
|
|
}
|
|
|
|
func severityOrder(s FindingSeverity) int {
|
|
switch s {
|
|
case FindingSeverityCritical:
|
|
return 0
|
|
case FindingSeverityWarning:
|
|
return 1
|
|
case FindingSeverityWatch:
|
|
return 2
|
|
case FindingSeverityInfo:
|
|
return 3
|
|
default:
|
|
return 4
|
|
}
|
|
}
|
|
|
|
func (i *Intelligence) getUpcomingRisks(predictions []patterns.FailurePrediction, limit int) []patterns.FailurePrediction {
|
|
if len(predictions) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Filter to next 7 days and sort by days until
|
|
var upcoming []patterns.FailurePrediction
|
|
for _, p := range predictions {
|
|
if p.DaysUntil <= 7 && p.Confidence >= 0.5 {
|
|
upcoming = append(upcoming, p)
|
|
}
|
|
}
|
|
|
|
sort.Slice(upcoming, func(a, b int) bool {
|
|
return upcoming[a].DaysUntil < upcoming[b].DaysUntil
|
|
})
|
|
|
|
if len(upcoming) > limit {
|
|
upcoming = upcoming[:limit]
|
|
}
|
|
return upcoming
|
|
}
|
|
|
|
func (i *Intelligence) getLearningStats() LearningStats {
|
|
stats := LearningStats{}
|
|
|
|
if i.knowledge != nil {
|
|
guests, _ := i.knowledge.ListGuests()
|
|
for _, guestID := range guests {
|
|
if k, err := i.knowledge.GetKnowledge(guestID); err == nil && k != nil && len(k.Notes) > 0 {
|
|
stats.ResourcesWithKnowledge++
|
|
stats.TotalNotes += len(k.Notes)
|
|
}
|
|
}
|
|
}
|
|
|
|
if i.baselines != nil {
|
|
stats.ResourcesWithBaselines = i.baselines.ResourceCount()
|
|
}
|
|
|
|
if i.patterns != nil {
|
|
p := i.patterns.GetPatterns()
|
|
stats.PatternsDetected = len(p)
|
|
}
|
|
|
|
if i.correlations != nil {
|
|
c := i.correlations.GetCorrelations()
|
|
stats.CorrelationsLearned = len(c)
|
|
}
|
|
|
|
if i.incidents != nil {
|
|
// Count is not available, so we skip this stat for now
|
|
// Could be added to IncidentStore if needed
|
|
stats.IncidentsTracked = 0
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
// HasCorrelationsSource reports whether the intelligence layer can provide
|
|
// learned correlation data.
|
|
func (i *Intelligence) HasCorrelationsSource() bool {
|
|
i.mu.RLock()
|
|
defer i.mu.RUnlock()
|
|
return i.correlations != nil
|
|
}
|
|
|
|
// GetCorrelations returns canonical learned correlations for the optional
|
|
// resource scope.
|
|
func (i *Intelligence) GetCorrelations(resourceID string) []*correlation.Correlation {
|
|
i.mu.RLock()
|
|
detector := i.correlations
|
|
i.mu.RUnlock()
|
|
|
|
if detector == nil {
|
|
return nil
|
|
}
|
|
|
|
resourceID = strings.TrimSpace(resourceID)
|
|
if resourceID != "" {
|
|
return detector.GetCorrelationsForResource(resourceID)
|
|
}
|
|
return detector.GetCorrelations()
|
|
}
|
|
|
|
// FormatCorrelationsContext returns the canonical AI prompt section for learned
|
|
// correlations.
|
|
func (i *Intelligence) FormatCorrelationsContext(resourceID string) string {
|
|
i.mu.RLock()
|
|
detector := i.correlations
|
|
i.mu.RUnlock()
|
|
if detector == nil {
|
|
return ""
|
|
}
|
|
return detector.FormatForContext(resourceID)
|
|
}
|
|
|
|
func (i *Intelligence) calculateOverallHealth(summary *IntelligenceSummary, runHistoryStore *PatrolRunHistoryStore) HealthScore {
|
|
health := HealthScore{
|
|
Score: 100,
|
|
Grade: HealthGradeA,
|
|
Trend: HealthTrendStable,
|
|
Factors: []HealthFactor{},
|
|
}
|
|
|
|
// Deduct for findings
|
|
if summary.FindingsCount.Critical > 0 {
|
|
impact := float64(summary.FindingsCount.Critical) * 20
|
|
if impact > 40 {
|
|
impact = 40
|
|
}
|
|
health.Score -= impact
|
|
health.Factors = append(health.Factors, HealthFactor{
|
|
Name: "Critical findings",
|
|
Impact: -impact / 100,
|
|
Description: fmt.Sprintf("%d critical issues need immediate attention", summary.FindingsCount.Critical),
|
|
Category: "finding",
|
|
})
|
|
}
|
|
|
|
if summary.FindingsCount.Warning > 0 {
|
|
impact := float64(summary.FindingsCount.Warning) * 10
|
|
if impact > 20 {
|
|
impact = 20
|
|
}
|
|
health.Score -= impact
|
|
health.Factors = append(health.Factors, HealthFactor{
|
|
Name: "Warnings",
|
|
Impact: -impact / 100,
|
|
Description: fmt.Sprintf("%d warnings need attention soon", summary.FindingsCount.Warning),
|
|
Category: "finding",
|
|
})
|
|
}
|
|
|
|
// Deduct for imminent predictions
|
|
for _, pred := range summary.UpcomingRisks {
|
|
if pred.DaysUntil < 3 && pred.Confidence > 0.7 {
|
|
impact := 10.0
|
|
health.Score -= impact
|
|
health.Factors = append(health.Factors, HealthFactor{
|
|
Name: "Predicted issue",
|
|
Impact: -impact / 100,
|
|
Description: fmt.Sprintf("%s predicted within %.1f days", pred.EventType, pred.DaysUntil),
|
|
Category: "prediction",
|
|
})
|
|
}
|
|
}
|
|
|
|
if factor, ok := summarizeRecentPatrolCoverage(runHistoryStore, time.Now()); ok {
|
|
health.Score -= factor.impact
|
|
health.Factors = append(health.Factors, HealthFactor{
|
|
Name: factor.name,
|
|
Impact: -factor.impact / 100,
|
|
Description: factor.description,
|
|
Category: "coverage",
|
|
})
|
|
}
|
|
|
|
// Bonus for learning progress
|
|
if summary.Learning.ResourcesWithKnowledge > 5 {
|
|
bonus := 5.0
|
|
health.Score += bonus
|
|
health.Factors = append(health.Factors, HealthFactor{
|
|
Name: "Knowledge learned",
|
|
Impact: bonus / 100,
|
|
Description: fmt.Sprintf("Pulse Patrol has learned about %d resources", summary.Learning.ResourcesWithKnowledge),
|
|
Category: "learning",
|
|
})
|
|
}
|
|
|
|
// Clamp score
|
|
if health.Score < 0 {
|
|
health.Score = 0
|
|
}
|
|
if health.Score > 100 {
|
|
health.Score = 100
|
|
}
|
|
|
|
// Assign grade
|
|
health.Grade = scoreToGrade(health.Score)
|
|
|
|
// Generate prediction text
|
|
health.Prediction = i.generateHealthPrediction(health, summary)
|
|
|
|
return health
|
|
}
|
|
|
|
func (i *Intelligence) calculateResourceHealth(intel *ResourceIntelligence) HealthScore {
|
|
health := HealthScore{
|
|
Score: 100,
|
|
Grade: HealthGradeA,
|
|
Trend: HealthTrendStable,
|
|
Factors: []HealthFactor{},
|
|
}
|
|
|
|
// Deduct for active findings
|
|
for _, f := range intel.ActiveFindings {
|
|
if f == nil {
|
|
continue
|
|
}
|
|
var impact float64
|
|
switch f.Severity {
|
|
case FindingSeverityCritical:
|
|
impact = 30
|
|
case FindingSeverityWarning:
|
|
impact = 15
|
|
case FindingSeverityWatch:
|
|
impact = 5
|
|
case FindingSeverityInfo:
|
|
impact = 2
|
|
}
|
|
health.Score -= impact
|
|
health.Factors = append(health.Factors, HealthFactor{
|
|
Name: f.Title,
|
|
Impact: -impact / 100,
|
|
Description: f.Description,
|
|
Category: "finding",
|
|
})
|
|
}
|
|
|
|
// Deduct for predictions
|
|
for _, p := range intel.Predictions {
|
|
if p.DaysUntil < 7 && p.Confidence > 0.5 {
|
|
impact := 10.0 * p.Confidence
|
|
health.Score -= impact
|
|
health.Factors = append(health.Factors, HealthFactor{
|
|
Name: "Predicted: " + string(p.EventType),
|
|
Impact: -impact / 100,
|
|
Description: p.Basis,
|
|
Category: "prediction",
|
|
})
|
|
}
|
|
}
|
|
|
|
// Deduct for anomalies
|
|
for _, a := range intel.Anomalies {
|
|
var impact float64
|
|
switch a.Severity {
|
|
case baseline.AnomalyCritical:
|
|
impact = 20
|
|
case baseline.AnomalyHigh:
|
|
impact = 10
|
|
case baseline.AnomalyMedium:
|
|
impact = 5
|
|
case baseline.AnomalyLow:
|
|
impact = 2
|
|
}
|
|
health.Score -= impact
|
|
health.Factors = append(health.Factors, HealthFactor{
|
|
Name: a.Metric + " anomaly",
|
|
Impact: -impact / 100,
|
|
Description: a.Description,
|
|
Category: "baseline",
|
|
})
|
|
}
|
|
|
|
// Bonus for having knowledge
|
|
if intel.NoteCount > 0 {
|
|
bonus := 2.0
|
|
health.Score += bonus
|
|
health.Factors = append(health.Factors, HealthFactor{
|
|
Name: "Documented",
|
|
Impact: bonus / 100,
|
|
Description: fmt.Sprintf("%d notes saved for this resource", intel.NoteCount),
|
|
Category: "learning",
|
|
})
|
|
}
|
|
|
|
// Clamp
|
|
if health.Score < 0 {
|
|
health.Score = 0
|
|
}
|
|
if health.Score > 100 {
|
|
health.Score = 100
|
|
}
|
|
|
|
health.Grade = scoreToGrade(health.Score)
|
|
|
|
return health
|
|
}
|
|
|
|
func scoreToGrade(score float64) HealthGrade {
|
|
switch {
|
|
case score >= 90:
|
|
return HealthGradeA
|
|
case score >= 75:
|
|
return HealthGradeB
|
|
case score >= 60:
|
|
return HealthGradeC
|
|
case score >= 40:
|
|
return HealthGradeD
|
|
default:
|
|
return HealthGradeF
|
|
}
|
|
}
|
|
|
|
func (i *Intelligence) generateHealthPrediction(health HealthScore, summary *IntelligenceSummary) string {
|
|
for _, factor := range health.Factors {
|
|
if factor.Category == "coverage" {
|
|
return factor.Description
|
|
}
|
|
}
|
|
|
|
if health.Grade == HealthGradeA {
|
|
return "Infrastructure is healthy with no significant issues detected."
|
|
}
|
|
|
|
if summary.FindingsCount.Critical > 0 {
|
|
return fmt.Sprintf("Immediate attention required: %d critical issues.", summary.FindingsCount.Critical)
|
|
}
|
|
|
|
if len(summary.UpcomingRisks) > 0 {
|
|
risk := summary.UpcomingRisks[0]
|
|
return fmt.Sprintf("Predicted %s event on resource within %.1f days (%.0f%% confidence).",
|
|
risk.EventType, risk.DaysUntil, risk.Confidence*100)
|
|
}
|
|
|
|
if summary.FindingsCount.Warning > 0 {
|
|
return fmt.Sprintf("%d warnings should be addressed soon to maintain stability.", summary.FindingsCount.Warning)
|
|
}
|
|
|
|
return "Infrastructure is stable with minor issues to monitor."
|
|
}
|
|
|
|
type patrolCoverageFactor struct {
|
|
name string
|
|
description string
|
|
impact float64
|
|
}
|
|
|
|
func summarizeRecentPatrolCoverage(
|
|
runHistoryStore *PatrolRunHistoryStore,
|
|
now time.Time,
|
|
) (patrolCoverageFactor, bool) {
|
|
if runHistoryStore == nil {
|
|
return patrolCoverageFactor{}, false
|
|
}
|
|
|
|
recentRuns := runHistoryStore.GetRecent(intelligenceRecentRunLimit)
|
|
if len(recentRuns) == 0 {
|
|
return patrolCoverageFactor{}, false
|
|
}
|
|
|
|
cutoff := now.Add(-intelligencePatrolCoverageWindow)
|
|
relevant := make([]PatrolRunRecord, 0, len(recentRuns))
|
|
for _, run := range recentRuns {
|
|
if run.CompletedAt.IsZero() || run.CompletedAt.Before(cutoff) {
|
|
continue
|
|
}
|
|
relevant = append(relevant, run)
|
|
}
|
|
if len(relevant) == 0 {
|
|
return patrolCoverageFactor{}, false
|
|
}
|
|
|
|
var recentErrors int
|
|
var hasSuccessfulFullRun bool
|
|
var hasRecentFullRun bool
|
|
var limitedActivityRuns int
|
|
for _, run := range relevant {
|
|
if run.ErrorCount > 0 || strings.EqualFold(strings.TrimSpace(run.Status), "error") {
|
|
recentErrors++
|
|
}
|
|
if isFullPatrolRun(run) {
|
|
hasRecentFullRun = true
|
|
} else {
|
|
limitedActivityRuns++
|
|
}
|
|
if isSuccessfulFullPatrolRun(run) {
|
|
hasSuccessfulFullRun = true
|
|
}
|
|
}
|
|
|
|
limitedActivityLabel := describeLimitedPatrolActivity(relevant)
|
|
|
|
switch {
|
|
case !hasSuccessfulFullRun && hasRecentFullRun && recentErrors > 0:
|
|
return patrolCoverageFactor{
|
|
name: "Patrol coverage incomplete",
|
|
description: "Patrol coverage is incomplete: a recent full patrol ended with errors, so overall health is not fully verified.",
|
|
impact: 35,
|
|
}, true
|
|
case !hasSuccessfulFullRun && recentErrors > 0:
|
|
return patrolCoverageFactor{
|
|
name: "Patrol coverage incomplete",
|
|
description: fmt.Sprintf("Patrol coverage is incomplete: recent activity was limited to %s and ended with errors, so overall health is not fully verified.", limitedActivityLabel),
|
|
impact: 35,
|
|
}, true
|
|
case !hasSuccessfulFullRun && limitedActivityRuns == len(relevant):
|
|
return patrolCoverageFactor{
|
|
name: "Patrol coverage incomplete",
|
|
description: fmt.Sprintf("Patrol coverage is incomplete: recent activity was limited to %s, so overall infrastructure health is not fully verified.", limitedActivityLabel),
|
|
impact: 20,
|
|
}, true
|
|
case recentErrors > 0:
|
|
return patrolCoverageFactor{
|
|
name: "Recent Patrol errors",
|
|
description: "Recent Patrol runs encountered errors, so the current health summary may be incomplete.",
|
|
impact: 10,
|
|
}, true
|
|
default:
|
|
return patrolCoverageFactor{}, false
|
|
}
|
|
}
|
|
|
|
func normalizePatrolRunType(run PatrolRunRecord) string {
|
|
return strings.ToLower(strings.TrimSpace(run.Type))
|
|
}
|
|
|
|
func isFullPatrolRun(run PatrolRunRecord) bool {
|
|
switch normalizePatrolRunType(run) {
|
|
case "", "full", "patrol":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func isScopedPatrolRun(run PatrolRunRecord) bool {
|
|
return normalizePatrolRunType(run) == "scoped"
|
|
}
|
|
|
|
func isVerificationPatrolRun(run PatrolRunRecord) bool {
|
|
return normalizePatrolRunType(run) == "verification"
|
|
}
|
|
|
|
func describeLimitedPatrolActivity(runs []PatrolRunRecord) string {
|
|
hasScoped := false
|
|
hasVerification := false
|
|
hasOther := false
|
|
|
|
for _, run := range runs {
|
|
if isFullPatrolRun(run) {
|
|
continue
|
|
}
|
|
switch {
|
|
case isScopedPatrolRun(run):
|
|
hasScoped = true
|
|
case isVerificationPatrolRun(run):
|
|
hasVerification = true
|
|
default:
|
|
hasOther = true
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case hasScoped && !hasVerification && !hasOther:
|
|
return "scoped runs"
|
|
case hasVerification && !hasScoped && !hasOther:
|
|
return "verification checks"
|
|
case hasScoped && hasVerification && !hasOther:
|
|
return "scoped runs and verification checks"
|
|
default:
|
|
return "targeted Patrol activity"
|
|
}
|
|
}
|
|
|
|
func isSuccessfulFullPatrolRun(run PatrolRunRecord) bool {
|
|
return isFullPatrolRun(run) &&
|
|
run.ErrorCount == 0 &&
|
|
!strings.EqualFold(strings.TrimSpace(run.Status), "error")
|
|
}
|
|
|
|
func (i *Intelligence) getResourcesAtRisk(limit int) []ResourceRiskSummary {
|
|
if i.findings == nil {
|
|
return nil
|
|
}
|
|
|
|
// Group findings by resource
|
|
byResource := make(map[string][]*Finding)
|
|
for _, f := range i.findings.GetActive(FindingSeverityInfo) {
|
|
byResource[f.ResourceID] = append(byResource[f.ResourceID], f)
|
|
}
|
|
|
|
// Calculate risk for each resource
|
|
type resourceRisk struct {
|
|
id string
|
|
name string
|
|
rtype string
|
|
score float64
|
|
top string
|
|
}
|
|
|
|
var risks []resourceRisk
|
|
for id, findings := range byResource {
|
|
score := 0.0
|
|
var topFinding *Finding
|
|
for _, f := range findings {
|
|
switch f.Severity {
|
|
case FindingSeverityCritical:
|
|
score += 30
|
|
case FindingSeverityWarning:
|
|
score += 15
|
|
case FindingSeverityWatch:
|
|
score += 5
|
|
case FindingSeverityInfo:
|
|
score += 2
|
|
}
|
|
if topFinding == nil || severityOrder(f.Severity) < severityOrder(topFinding.Severity) {
|
|
topFinding = f
|
|
}
|
|
}
|
|
|
|
if score > 0 && topFinding != nil {
|
|
risks = append(risks, resourceRisk{
|
|
id: id,
|
|
name: topFinding.ResourceName,
|
|
rtype: topFinding.ResourceType,
|
|
score: score,
|
|
top: topFinding.Title,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sort by risk score descending
|
|
sort.Slice(risks, func(a, b int) bool {
|
|
return risks[a].score > risks[b].score
|
|
})
|
|
|
|
if len(risks) > limit {
|
|
risks = risks[:limit]
|
|
}
|
|
|
|
// Convert to summaries
|
|
var summaries []ResourceRiskSummary
|
|
for _, r := range risks {
|
|
health := HealthScore{
|
|
Score: 100 - r.score,
|
|
Grade: scoreToGrade(100 - r.score),
|
|
}
|
|
summaries = append(summaries, ResourceRiskSummary{
|
|
ResourceID: r.id,
|
|
ResourceName: r.name,
|
|
ResourceType: r.rtype,
|
|
Health: health,
|
|
TopIssue: r.top,
|
|
})
|
|
}
|
|
|
|
return summaries
|
|
}
|
|
|
|
func (i *Intelligence) detectCurrentAnomalies(resourceID string) []AnomalyReport {
|
|
if i.anomalyDetector != nil {
|
|
return i.anomalyDetector(resourceID)
|
|
}
|
|
// This would be called with current metrics from state
|
|
// For now, return empty - will be integrated with patrol
|
|
return nil
|
|
}
|
|
|
|
func (i *Intelligence) getRecentChangesForResource(resourceID string, limit int) []unifiedresources.ResourceChange {
|
|
resourceID = strings.TrimSpace(resourceID)
|
|
if resourceID == "" || limit <= 0 {
|
|
return nil
|
|
}
|
|
|
|
since := time.Now().Add(-24 * time.Hour)
|
|
|
|
i.mu.RLock()
|
|
resourceTimelineStore := i.resourceTimelineStore
|
|
changesDetector := i.changes
|
|
i.mu.RUnlock()
|
|
|
|
if resourceTimelineStore != nil {
|
|
if recent, err := resourceTimelineStore.GetRecentChanges(resourceID, since, limit); err == nil && len(recent) > 0 {
|
|
return recent
|
|
}
|
|
}
|
|
|
|
if changesDetector == nil {
|
|
return nil
|
|
}
|
|
|
|
recent := changesDetector.GetChangesForResource(resourceID, limit)
|
|
if len(recent) == 0 {
|
|
return nil
|
|
}
|
|
|
|
converted := make([]unifiedresources.ResourceChange, 0, len(recent))
|
|
for _, change := range recent {
|
|
converted = append(converted, memory.ResourceChangeFromMemoryChange(change))
|
|
}
|
|
return converted
|
|
}
|
|
|
|
func (i *Intelligence) formatBaselinesForContext(resourceID string) string {
|
|
i.mu.RLock()
|
|
store := i.baselines
|
|
i.mu.RUnlock()
|
|
|
|
if store == nil {
|
|
return ""
|
|
}
|
|
|
|
rb, ok := store.GetResourceBaseline(resourceID)
|
|
if !ok || len(rb.Metrics) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var lines []string
|
|
lines = append(lines, "\n## Learned Baselines")
|
|
lines = append(lines, "Normal operating ranges for this resource:")
|
|
|
|
for metric, mb := range rb.Metrics {
|
|
lines = append(lines, fmt.Sprintf("- %s: mean %.1f, stddev %.1f (samples: %d)",
|
|
metric, mb.Mean, mb.StdDev, mb.SampleCount))
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func (i *Intelligence) formatAnomaliesForContext(anomalies []AnomalyReport) string {
|
|
if len(anomalies) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var lines []string
|
|
lines = append(lines, "\n## Current Anomalies")
|
|
lines = append(lines, "Metrics deviating from normal:")
|
|
|
|
for _, a := range anomalies {
|
|
lines = append(lines, fmt.Sprintf("- %s: %s", a.Metric, a.Description))
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func (i *Intelligence) formatAnomalyDescription(_ string, value float64, bl *baseline.MetricBaseline, zScore float64) string {
|
|
direction := "above"
|
|
if zScore < 0 {
|
|
direction = "below"
|
|
}
|
|
return fmt.Sprintf("%.1f is %.1f std devs %s baseline (mean: %.1f)",
|
|
value, absFloatIntel(zScore), direction, bl.Mean)
|
|
}
|
|
|
|
// absFloatIntel is a local helper (service.go has its own)
|
|
func absFloatIntel(f float64) float64 {
|
|
if f < 0 {
|
|
return -f
|
|
}
|
|
return f
|
|
}
|