package stats import ( "log/slog" "sort" "sync" ) // Stats counts records per NSID across four buckets: // - seen: every create/update commit that reached us // - blocked: dropped by the NSID blocklist // - indexed: passed blocklist, had prose, sent to Meili // - notext: passed blocklist, extraction produced no text // // seen == blocked + indexed + notext. type Stats struct { mu sync.Mutex seen map[string]uint64 blocked map[string]uint64 indexed map[string]uint64 notext map[string]uint64 } func New() *Stats { return &Stats{ seen: map[string]uint64{}, blocked: map[string]uint64{}, indexed: map[string]uint64{}, notext: map[string]uint64{}, } } func (s *Stats) Seen(nsid string) { s.inc(s.seen, nsid) } func (s *Stats) Blocked(nsid string) { s.inc(s.blocked, nsid) } func (s *Stats) Indexed(nsid string) { s.inc(s.indexed, nsid) } func (s *Stats) NoText(nsid string) { s.inc(s.notext, nsid) } func (s *Stats) inc(m map[string]uint64, nsid string) { s.mu.Lock() m[nsid]++ s.mu.Unlock() } type Row struct { NSID string Count uint64 } // TopN returns the highest-count entries in a bucket. func (s *Stats) TopN(bucket string, n int) []Row { s.mu.Lock() defer s.mu.Unlock() var m map[string]uint64 switch bucket { case "seen": m = s.seen case "blocked": m = s.blocked case "indexed": m = s.indexed case "notext": m = s.notext default: return nil } rows := make([]Row, 0, len(m)) for k, v := range m { rows = append(rows, Row{NSID: k, Count: v}) } sort.Slice(rows, func(i, j int) bool { return rows[i].Count > rows[j].Count }) if len(rows) > n { rows = rows[:n] } return rows } func (s *Stats) totals() (seen, blocked, indexed, notext uint64) { s.mu.Lock() defer s.mu.Unlock() for _, v := range s.seen { seen += v } for _, v := range s.blocked { blocked += v } for _, v := range s.indexed { indexed += v } for _, v := range s.notext { notext += v } return } // LogSummary emits a compact per-bucket top-N to the logger. func (s *Stats) LogSummary(log *slog.Logger, n int) { seen, blocked, indexed, notext := s.totals() log.Info("stats totals", "seen", seen, "blocked", blocked, "indexed", indexed, "no_text", notext) for _, bucket := range []string{"indexed", "notext", "seen", "blocked"} { rows := s.TopN(bucket, n) for i, r := range rows { log.Info("stats top", "bucket", bucket, "rank", i+1, "nsid", r.NSID, "count", r.Count) } } }