Pulse/internal/ai/patrol_history_persistence.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
}