117 lines
2.6 KiB
Go
117 lines
2.6 KiB
Go
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)
|
|
}
|