package activity import ( "encoding/json" "os" "path/filepath" "sort" "sync" "time" ) type Event struct { ID string `json:"id"` Kind string `json:"kind"` Severity string `json:"severity"` Message string `json:"message"` Actor string `json:"actor"` IP string `json:"ip"` Path string `json:"path"` Method string `json:"method"` CreatedAt time.Time `json:"created_at"` Meta map[string]string `json:"meta,omitempty"` } type Store struct { path string mu sync.Mutex } func NewStore(path string) *Store { return &Store{path: path} } func (s *Store) Append(event Event, retentionSeconds int64) error { s.mu.Lock() defer s.mu.Unlock() events, err := s.readLocked() if err != nil { return err } if event.CreatedAt.IsZero() { event.CreatedAt = time.Now().UTC() } if event.ID == "" { event.ID = event.CreatedAt.Format("20060102T150405.000000000") } events = append(events, event) events = pruneByRetention(events, retentionSeconds) return s.writeLocked(events) } func (s *Store) List(limit int, retentionSeconds int64) ([]Event, error) { s.mu.Lock() defer s.mu.Unlock() events, err := s.readLocked() if err != nil { return nil, err } events = pruneByRetention(events, retentionSeconds) if err := s.writeLocked(events); err != nil { return nil, err } sort.Slice(events, func(i, j int) bool { return events[i].CreatedAt.After(events[j].CreatedAt) }) if limit > 0 && len(events) > limit { return events[:limit], nil } return events, nil } func pruneByRetention(events []Event, retentionSeconds int64) []Event { if retentionSeconds <= 0 { return events } cutoff := time.Now().UTC().Add(-time.Duration(retentionSeconds) * time.Second) out := make([]Event, 0, len(events)) for _, event := range events { if event.CreatedAt.IsZero() || event.CreatedAt.After(cutoff) { out = append(out, event) } } return out } func (s *Store) readLocked() ([]Event, error) { data, err := os.ReadFile(s.path) if err != nil { if os.IsNotExist(err) { return []Event{}, nil } return nil, err } if len(data) == 0 { return []Event{}, nil } var events []Event if err := json.Unmarshal(data, &events); err != nil { return []Event{}, nil } return events, nil } func (s *Store) writeLocked(events []Event) error { if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil { return err } data, err := json.MarshalIndent(events, "", " ") if err != nil { return err } return os.WriteFile(s.path, data, 0644) }