package cluster import ( "bufio" "encoding/json" "fmt" "os" "path/filepath" "strings" "time" ) type ChangeRecord struct { At time.Time `json:"at"` Action string `json:"action"` Target string `json:"target"` Details string `json:"details,omitempty"` } func (s *Service) changeHistoryPath() string { return filepath.Join(s.DataDir(), "history", "changes.log") } func (s *Service) AppendChange(action, target, details string) error { if s == nil { return nil } rec := ChangeRecord{ At: s.now().UTC(), Action: strings.TrimSpace(action), Target: strings.TrimSpace(target), Details: strings.TrimSpace(details), } if rec.Action == "" { return nil } path := s.changeHistoryPath() if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return err } f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) if err != nil { return err } defer f.Close() b, err := json.Marshal(rec) if err != nil { return err } _, err = f.Write(append(b, '\n')) return err } func (s *Service) ListChanges(limit int) ([]ChangeRecord, error) { path := s.changeHistoryPath() f, err := os.Open(path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } defer f.Close() capHint := 32 if limit > capHint { capHint = limit } items := make([]ChangeRecord, 0, capHint) sc := bufio.NewScanner(f) sc.Buffer(make([]byte, 0, 64*1024), 2*1024*1024) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" { continue } var rec ChangeRecord if err := json.Unmarshal([]byte(line), &rec); err != nil { continue } items = append(items, rec) } if err := sc.Err(); err != nil { return nil, fmt.Errorf("scan change history: %w", err) } if limit > 0 && len(items) > limit { items = items[len(items)-limit:] } return items, nil }