package history import ( "bufio" "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "time" ) type AvailabilitySnapshot struct { Timestamp time.Time `json:"timestamp"` ClusterUp bool `json:"cluster_up"` VMStates map[string]string `json:"vm_states,omitempty"` Error string `json:"error,omitempty"` } type AvailabilityStore struct { dir string } func NewAvailabilityStore(baseDir string) *AvailabilityStore { if strings.TrimSpace(baseDir) == "" { baseDir = "." } return &AvailabilityStore{dir: filepath.Join(baseDir, "history", "availability")} } func (s *AvailabilityStore) Path(clusterID string) string { return filepath.Join(s.dir, sanitizeClusterID(clusterID)+".jsonl") } func (s *AvailabilityStore) Append(clusterID string, snap AvailabilitySnapshot) error { if strings.TrimSpace(clusterID) == "" { return errors.New("empty cluster id") } if snap.Timestamp.IsZero() { snap.Timestamp = time.Now().UTC() } if err := os.MkdirAll(s.dir, 0o700); err != nil { return err } f, err := os.OpenFile(s.Path(clusterID), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) if err != nil { return err } defer f.Close() b, err := json.Marshal(snap) if err != nil { return err } _, err = f.Write(append(b, '\n')) return err } func (s *AvailabilityStore) Load(clusterID string, since time.Time) ([]AvailabilitySnapshot, error) { if strings.TrimSpace(clusterID) == "" { return nil, errors.New("empty cluster id") } f, err := os.Open(s.Path(clusterID)) if err != nil { if errors.Is(err, os.ErrNotExist) { return []AvailabilitySnapshot{}, nil } return nil, err } defer f.Close() sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 0, 64*1024), 8*1024*1024) out := make([]AvailabilitySnapshot, 0, 512) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" { continue } var snap AvailabilitySnapshot if err := json.Unmarshal([]byte(line), &snap); err != nil { continue } if !since.IsZero() && snap.Timestamp.Before(since) { continue } out = append(out, snap) } if err := sc.Err(); err != nil { return nil, fmt.Errorf("scan availability: %w", err) } return out, nil }