Collector implementation

This commit is contained in:
2026-03-14 20:07:22 +01:00
parent 4393ae2726
commit 6ca296b2e8
16 changed files with 3052 additions and 0 deletions

179
cmd/collector/store_test.go Normal file
View File

@@ -0,0 +1,179 @@
package main
import (
"fmt"
"testing"
"time"
pb "git.ipng.ch/ipng/nginx-logtail/proto/logtailpb"
)
func makeStore() *Store {
return NewStore("test")
}
func ingestN(s *Store, website, prefix, uri, status string, n int) {
for i := 0; i < n; i++ {
s.ingest(LogRecord{website, prefix, uri, status})
}
}
func TestIngestAndRotate(t *testing.T) {
s := makeStore()
ingestN(s, "example.com", "1.2.3.0/24", "/", "200", 100)
ingestN(s, "other.com", "5.6.7.0/24", "/api", "429", 50)
s.rotate(time.Now())
s.mu.RLock()
defer s.mu.RUnlock()
if s.fineFilled != 1 {
t.Fatalf("fineFilled = %d, want 1", s.fineFilled)
}
snap := s.fineRing[(s.fineHead-1+fineRingSize)%fineRingSize]
if len(snap.Entries) != 2 {
t.Fatalf("snapshot has %d entries, want 2", len(snap.Entries))
}
if snap.Entries[0].Count != 100 {
t.Errorf("top entry count = %d, want 100", snap.Entries[0].Count)
}
}
func TestLiveMapCap(t *testing.T) {
s := makeStore()
// Ingest liveMapCap+100 distinct keys; only liveMapCap should be tracked
for i := 0; i < liveMapCap+100; i++ {
s.ingest(LogRecord{
Website: fmt.Sprintf("site%d.com", i),
ClientPrefix: "1.2.3.0/24",
URI: "/",
Status: "200",
})
}
if s.liveLen != liveMapCap {
t.Errorf("liveLen = %d, want %d", s.liveLen, liveMapCap)
}
}
func TestQueryTopN(t *testing.T) {
s := makeStore()
ingestN(s, "busy.com", "1.0.0.0/24", "/", "200", 300)
ingestN(s, "medium.com", "2.0.0.0/24", "/", "200", 100)
ingestN(s, "quiet.com", "3.0.0.0/24", "/", "200", 10)
s.rotate(time.Now())
entries := s.QueryTopN(nil, pb.GroupBy_WEBSITE, 2, pb.Window_W1M)
if len(entries) != 2 {
t.Fatalf("got %d entries, want 2", len(entries))
}
if entries[0].Label != "busy.com" {
t.Errorf("top entry = %q, want busy.com", entries[0].Label)
}
if entries[0].Count != 300 {
t.Errorf("top count = %d, want 300", entries[0].Count)
}
}
func TestQueryTopNWithFilter(t *testing.T) {
s := makeStore()
ingestN(s, "example.com", "1.0.0.0/24", "/api", "429", 200)
ingestN(s, "example.com", "2.0.0.0/24", "/api", "200", 500)
ingestN(s, "other.com", "3.0.0.0/24", "/", "429", 100)
s.rotate(time.Now())
status429 := int32(429)
entries := s.QueryTopN(&pb.Filter{HttpResponse: &status429}, pb.GroupBy_WEBSITE, 10, pb.Window_W1M)
if len(entries) != 2 {
t.Fatalf("got %d entries, want 2", len(entries))
}
// example.com has 200 × 429, other.com has 100 × 429
if entries[0].Label != "example.com" || entries[0].Count != 200 {
t.Errorf("unexpected top: %+v", entries[0])
}
}
func TestQueryTrend(t *testing.T) {
s := makeStore()
now := time.Now()
// Rotate 3 buckets with different counts
ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 10)
s.rotate(now.Add(-2 * time.Minute))
ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 20)
s.rotate(now.Add(-1 * time.Minute))
ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 30)
s.rotate(now)
points := s.QueryTrend(nil, pb.Window_W5M)
if len(points) != 3 {
t.Fatalf("got %d points, want 3", len(points))
}
// Points are oldest-first; counts should be 10, 20, 30
if points[0].Count != 10 || points[1].Count != 20 || points[2].Count != 30 {
t.Errorf("unexpected counts: %v", points)
}
}
func TestCoarseRingPopulated(t *testing.T) {
s := makeStore()
now := time.Now()
// Rotate coarseEvery fine buckets to trigger one coarse bucket
for i := 0; i < coarseEvery; i++ {
ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 10)
s.rotate(now.Add(time.Duration(i) * time.Minute))
}
s.mu.RLock()
defer s.mu.RUnlock()
if s.coarseFilled != 1 {
t.Fatalf("coarseFilled = %d, want 1", s.coarseFilled)
}
coarse := s.coarseRing[(s.coarseHead-1+coarseRingSize)%coarseRingSize]
if len(coarse.Entries) == 0 {
t.Fatal("coarse snapshot is empty")
}
// 5 fine buckets × 10 counts = 50
if coarse.Entries[0].Count != 50 {
t.Errorf("coarse count = %d, want 50", coarse.Entries[0].Count)
}
}
func TestSubscribeBroadcast(t *testing.T) {
s := makeStore()
ch := s.Subscribe()
ingestN(s, "x.com", "1.0.0.0/24", "/", "200", 5)
s.rotate(time.Now())
select {
case snap := <-ch:
if len(snap.Entries) != 1 {
t.Errorf("got %d entries, want 1", len(snap.Entries))
}
case <-time.After(time.Second):
t.Fatal("no snapshot received within 1s")
}
s.Unsubscribe(ch)
}
func TestTopKOrdering(t *testing.T) {
m := map[string]int64{
"a": 5,
"b": 100,
"c": 1,
"d": 50,
}
entries := topKFromMap(m, 3)
if len(entries) != 3 {
t.Fatalf("got %d entries, want 3", len(entries))
}
if entries[0].Label != "b" || entries[0].Count != 100 {
t.Errorf("wrong top: %+v", entries[0])
}
if entries[1].Label != "d" || entries[1].Count != 50 {
t.Errorf("wrong second: %+v", entries[1])
}
}