Files
nginx-logtail/cmd/collector/store_test.go
2026-03-14 20:07:32 +01:00

180 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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])
}
}