249 lines
6.7 KiB
Go
249 lines
6.7 KiB
Go
// Package ai provides AI-powered infrastructure monitoring and investigation.
|
|
package ai
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// PatrolHistoryPersistence interface for saving/loading patrol run history
|
|
type PatrolHistoryPersistence interface {
|
|
SavePatrolRunHistory(runs []PatrolRunRecord) error
|
|
LoadPatrolRunHistory() ([]PatrolRunRecord, error)
|
|
}
|
|
|
|
// PatrolHistoryPersistenceAdapter bridges ConfigPersistence to PatrolHistoryPersistence interface
|
|
type PatrolHistoryPersistenceAdapter struct {
|
|
config *config.ConfigPersistence
|
|
}
|
|
|
|
// NewPatrolHistoryPersistenceAdapter creates a new adapter
|
|
func NewPatrolHistoryPersistenceAdapter(cfg *config.ConfigPersistence) *PatrolHistoryPersistenceAdapter {
|
|
return &PatrolHistoryPersistenceAdapter{config: cfg}
|
|
}
|
|
|
|
// SavePatrolRunHistory saves patrol run history to disk via ConfigPersistence
|
|
func (a *PatrolHistoryPersistenceAdapter) SavePatrolRunHistory(runs []PatrolRunRecord) error {
|
|
// Convert from ai.PatrolRunRecord to config.PatrolRunRecord
|
|
records := make([]config.PatrolRunRecord, len(runs))
|
|
for i, r := range runs {
|
|
records[i] = config.PatrolRunRecord{
|
|
ID: r.ID,
|
|
StartedAt: r.StartedAt,
|
|
CompletedAt: r.CompletedAt,
|
|
DurationMs: int64(r.Duration),
|
|
Type: r.Type,
|
|
ResourcesChecked: r.ResourcesChecked,
|
|
NodesChecked: r.NodesChecked,
|
|
GuestsChecked: r.GuestsChecked,
|
|
DockerChecked: r.DockerChecked,
|
|
StorageChecked: r.StorageChecked,
|
|
HostsChecked: r.HostsChecked,
|
|
PBSChecked: r.PBSChecked,
|
|
NewFindings: r.NewFindings,
|
|
ExistingFindings: r.ExistingFindings,
|
|
ResolvedFindings: r.ResolvedFindings,
|
|
AutoFixCount: r.AutoFixCount,
|
|
FindingsSummary: r.FindingsSummary,
|
|
FindingIDs: r.FindingIDs,
|
|
ErrorCount: r.ErrorCount,
|
|
Status: r.Status,
|
|
AIAnalysis: r.AIAnalysis,
|
|
InputTokens: r.InputTokens,
|
|
OutputTokens: r.OutputTokens,
|
|
}
|
|
}
|
|
return a.config.SavePatrolRunHistory(records)
|
|
}
|
|
|
|
// LoadPatrolRunHistory loads patrol run history from disk via ConfigPersistence
|
|
func (a *PatrolHistoryPersistenceAdapter) LoadPatrolRunHistory() ([]PatrolRunRecord, error) {
|
|
data, err := a.config.LoadPatrolRunHistory()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert from config.PatrolRunRecord to ai.PatrolRunRecord
|
|
runs := make([]PatrolRunRecord, len(data.Runs))
|
|
for i, r := range data.Runs {
|
|
runs[i] = PatrolRunRecord{
|
|
ID: r.ID,
|
|
StartedAt: r.StartedAt,
|
|
CompletedAt: r.CompletedAt,
|
|
Duration: time.Duration(r.DurationMs),
|
|
Type: r.Type,
|
|
ResourcesChecked: r.ResourcesChecked,
|
|
NodesChecked: r.NodesChecked,
|
|
GuestsChecked: r.GuestsChecked,
|
|
DockerChecked: r.DockerChecked,
|
|
StorageChecked: r.StorageChecked,
|
|
HostsChecked: r.HostsChecked,
|
|
PBSChecked: r.PBSChecked,
|
|
NewFindings: r.NewFindings,
|
|
ExistingFindings: r.ExistingFindings,
|
|
ResolvedFindings: r.ResolvedFindings,
|
|
AutoFixCount: r.AutoFixCount,
|
|
FindingsSummary: r.FindingsSummary,
|
|
FindingIDs: r.FindingIDs,
|
|
ErrorCount: r.ErrorCount,
|
|
Status: r.Status,
|
|
AIAnalysis: r.AIAnalysis,
|
|
InputTokens: r.InputTokens,
|
|
OutputTokens: r.OutputTokens,
|
|
}
|
|
}
|
|
return runs, nil
|
|
}
|
|
|
|
// PatrolRunHistoryStore provides thread-safe storage for patrol run history with optional persistence
|
|
type PatrolRunHistoryStore struct {
|
|
mu sync.RWMutex
|
|
runs []PatrolRunRecord
|
|
maxRuns int
|
|
persistence PatrolHistoryPersistence
|
|
// Debounce save operations
|
|
saveTimer *time.Timer
|
|
savePending bool
|
|
saveDebounce time.Duration
|
|
}
|
|
|
|
// NewPatrolRunHistoryStore creates a new patrol run history store
|
|
func NewPatrolRunHistoryStore(maxRuns int) *PatrolRunHistoryStore {
|
|
if maxRuns <= 0 {
|
|
maxRuns = MaxPatrolRunHistory
|
|
}
|
|
return &PatrolRunHistoryStore{
|
|
runs: make([]PatrolRunRecord, 0, maxRuns),
|
|
maxRuns: maxRuns,
|
|
saveDebounce: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
// SetPersistence sets the persistence layer and loads existing history
|
|
func (s *PatrolRunHistoryStore) SetPersistence(p PatrolHistoryPersistence) error {
|
|
s.mu.Lock()
|
|
s.persistence = p
|
|
s.mu.Unlock()
|
|
|
|
// Load existing history from disk
|
|
if p != nil {
|
|
runs, err := p.LoadPatrolRunHistory()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(runs) > 0 {
|
|
s.mu.Lock()
|
|
s.runs = runs
|
|
// Trim to max if loaded more than maxRuns
|
|
if len(s.runs) > s.maxRuns {
|
|
s.runs = s.runs[:s.maxRuns]
|
|
}
|
|
s.mu.Unlock()
|
|
log.Info().Int("count", len(runs)).Msg("Loaded patrol run history from disk")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Add adds a new patrol run to the history
|
|
func (s *PatrolRunHistoryStore) Add(run PatrolRunRecord) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
// Prepend (newest first)
|
|
s.runs = append([]PatrolRunRecord{run}, s.runs...)
|
|
|
|
// Trim to max
|
|
if len(s.runs) > s.maxRuns {
|
|
s.runs = s.runs[:s.maxRuns]
|
|
}
|
|
|
|
// Schedule save
|
|
s.scheduleSave()
|
|
}
|
|
|
|
// GetAll returns all runs (newest first)
|
|
func (s *PatrolRunHistoryStore) GetAll() []PatrolRunRecord {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
result := make([]PatrolRunRecord, len(s.runs))
|
|
copy(result, s.runs)
|
|
return result
|
|
}
|
|
|
|
// GetRecent returns at most n recent runs
|
|
func (s *PatrolRunHistoryStore) GetRecent(n int) []PatrolRunRecord {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
if n <= 0 || n > len(s.runs) {
|
|
n = len(s.runs)
|
|
}
|
|
|
|
result := make([]PatrolRunRecord, n)
|
|
copy(result, s.runs[:n])
|
|
return result
|
|
}
|
|
|
|
// Count returns the number of runs in history
|
|
func (s *PatrolRunHistoryStore) Count() int {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return len(s.runs)
|
|
}
|
|
|
|
// scheduleSave schedules a debounced save operation
|
|
func (s *PatrolRunHistoryStore) scheduleSave() {
|
|
if s.persistence == nil {
|
|
return
|
|
}
|
|
|
|
// Cancel existing timer if pending
|
|
if s.saveTimer != nil {
|
|
s.saveTimer.Stop()
|
|
}
|
|
|
|
s.savePending = true
|
|
s.saveTimer = time.AfterFunc(s.saveDebounce, func() {
|
|
s.mu.Lock()
|
|
if !s.savePending {
|
|
s.mu.Unlock()
|
|
return
|
|
}
|
|
s.savePending = false
|
|
// Copy runs while locked
|
|
runs := make([]PatrolRunRecord, len(s.runs))
|
|
copy(runs, s.runs)
|
|
persistence := s.persistence
|
|
s.mu.Unlock()
|
|
|
|
// Save outside lock
|
|
if persistence != nil {
|
|
if err := persistence.SavePatrolRunHistory(runs); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save patrol run history")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// FlushPersistence immediately saves any pending changes
|
|
func (s *PatrolRunHistoryStore) FlushPersistence() error {
|
|
s.mu.Lock()
|
|
if s.saveTimer != nil {
|
|
s.saveTimer.Stop()
|
|
}
|
|
s.savePending = false
|
|
runs := make([]PatrolRunRecord, len(s.runs))
|
|
copy(runs, s.runs)
|
|
persistence := s.persistence
|
|
s.mu.Unlock()
|
|
|
|
if persistence != nil {
|
|
return persistence.SavePatrolRunHistory(runs)
|
|
}
|
|
return nil
|
|
}
|