Traffic analytics: per-minute history, busiest visitors, charts, spike and daily-amount alerts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
39659f0144
commit
f7ec3e431a
10 files changed
+649
-6
No files matched your search
+159
@@ -0,0 +1,159 @@
|
||||
package hammerhead
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.veillax.com/Veillax/reef/model"
|
||||
)
|
||||
|
||||
func TestSpiking(t *testing.T) {
|
||||
const mb = 1 << 20
|
||||
cases := []struct {
|
||||
name string
|
||||
recent int64
|
||||
base float64
|
||||
history int64
|
||||
factor int
|
||||
want bool
|
||||
}{
|
||||
{"ten times usual", 200 * mb, 10 * mb, 180, 5, true},
|
||||
{"just under the factor", 49 * mb, 10 * mb, 180, 5, false},
|
||||
{"off", 500 * mb, 10 * mb, 180, 0, false},
|
||||
{"too little history", 500 * mb, 10 * mb, 30, 5, false},
|
||||
{"tiny in absolute terms", 5 * mb, 0.01 * mb, 180, 5, false},
|
||||
{"from nothing to a lot", 50 * mb, 0, 180, 5, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := spiking(c.recent, c.base, c.history, c.factor); got != c.want {
|
||||
t.Errorf("%s: spiking = %v, want %v", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrafficStorage(t *testing.T) {
|
||||
s := newFx(t).s
|
||||
now := time.Now()
|
||||
st := func(in, out int64, src ...model.SourceStat) model.Stat {
|
||||
return model.Stat{Conns: 1, BytesIn: in, BytesOut: out, Sources: src}
|
||||
}
|
||||
s.db.addTrafficAt("m0", st(100, 900, model.SourceStat{IP: "203.0.113.1", Conns: 1, BytesIn: 100, BytesOut: 900}), now)
|
||||
s.db.addTrafficAt("m0", st(10, 10, model.SourceStat{IP: "203.0.113.1", Conns: 1, BytesIn: 10, BytesOut: 10}, model.SourceStat{IP: "203.0.113.2", Conns: 1, BytesIn: 1}), now)
|
||||
s.db.addTrafficAt("m0", st(1, 1), now.Add(-3*time.Minute))
|
||||
s.db.addTrafficAt("m1", st(7, 7), now)
|
||||
|
||||
pts := s.db.trafficSeries([]string{"m0"}, "minute", 60, now)
|
||||
if len(pts) != 60 || pts[59].BytesIn != 110 || pts[59].BytesOut != 910 || pts[59].Conns != 2 || pts[56].BytesIn != 1 {
|
||||
t.Fatalf("minute series: last=%+v 3 back=%+v", pts[59], pts[56])
|
||||
}
|
||||
if pts[59].Time != now.Unix()/60*60 || pts[58].Time != pts[59].Time-60 {
|
||||
t.Errorf("bucket times: %d %d", pts[58].Time, pts[59].Time)
|
||||
}
|
||||
all := s.db.trafficSeries([]string{"m0", "m1"}, "hour", 24, now)
|
||||
if all[23].BytesIn+all[22].BytesIn < 118 || len(all) != 24 {
|
||||
t.Errorf("hour series over two ports: %+v", all[22:])
|
||||
}
|
||||
if got := s.db.trafficSeries(nil, "hour", 3, now); len(got) != 3 || got[2].BytesIn != 0 {
|
||||
t.Errorf("no ports: %+v", got)
|
||||
}
|
||||
|
||||
top := s.db.topSources([]string{"m0"}, 24, 10, now)
|
||||
if len(top) != 2 || top[0].IP != "203.0.113.1" || top[0].BytesOut != 910 || top[0].Conns != 2 {
|
||||
t.Fatalf("top sources: %+v", top)
|
||||
}
|
||||
if got := s.db.topSources([]string{"m0"}, 24, 1, now); len(got) != 1 {
|
||||
t.Errorf("limit: %+v", got)
|
||||
}
|
||||
if got := s.db.topSources([]string{"m1"}, 24, 10, now); len(got) != 0 {
|
||||
t.Errorf("another port's sources leaked: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrafficRetention(t *testing.T) {
|
||||
s := newFx(t).s
|
||||
now := time.Now()
|
||||
old := model.Stat{Conns: 1, BytesIn: 5, Sources: []model.SourceStat{{IP: "203.0.113.9", Conns: 1, BytesIn: 5}}}
|
||||
s.db.addTrafficAt("m0", old, now.Add(-trafficMinutelyKeep-time.Hour)) // too old for minutes, fine for hours and sources
|
||||
s.db.addTrafficAt("m0", old, now.Add(-trafficSourcesKeep-time.Hour)) // too old for sources too
|
||||
s.db.addTrafficAt("m0", old, now.Add(-trafficHourlyKeep-time.Hour)) // gone everywhere
|
||||
s.db.addTrafficAt("m0", old, now)
|
||||
s.db.purge(30, 365)
|
||||
count := func(table string) (n int) {
|
||||
s.db.sql.QueryRow(`SELECT COUNT(*) FROM ` + table).Scan(&n)
|
||||
return
|
||||
}
|
||||
if n := count("traffic_minutely"); n != 1 {
|
||||
t.Errorf("minutely rows after purge: %d, want 1", n)
|
||||
}
|
||||
if n := count("traffic_sources"); n != 2 {
|
||||
t.Errorf("source rows after purge: %d, want 2", n)
|
||||
}
|
||||
if n := count("traffic_hourly"); n != 3 {
|
||||
t.Errorf("hourly rows after purge: %d, want 3", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecentTraffic(t *testing.T) {
|
||||
s := newFx(t).s
|
||||
now := time.Now()
|
||||
const mb = 1 << 20
|
||||
for h := 3 * 60; h > spikeWindow; h-- { // three hours of 1 MB a minute
|
||||
s.db.addTrafficAt("m0", model.Stat{BytesIn: mb}, now.Add(-time.Duration(h)*time.Minute))
|
||||
}
|
||||
s.db.addTrafficAt("m0", model.Stat{BytesIn: 100 * mb}, now)
|
||||
recent, base, hist := s.db.recentTraffic("m0", now)
|
||||
if recent != 100*mb || hist < 170 || base < 4.9*mb || base > 5.1*mb { // 1 MB a minute is 5 MB per window
|
||||
t.Fatalf("recent=%d base=%.0f history=%d", recent, base, hist)
|
||||
}
|
||||
if !spiking(recent, base, hist, 5) {
|
||||
t.Error("100 MB over a 5 MB norm should spike")
|
||||
}
|
||||
if r, b, h := s.db.recentTraffic("nothing", now); r != 0 || b != 0 || h != 0 {
|
||||
t.Errorf("unknown port: %d %f %d", r, b, h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrafficAlertsFireAndClear(t *testing.T) {
|
||||
f := newFx(t)
|
||||
s := f.s
|
||||
const mb = 1 << 20
|
||||
now := time.Now()
|
||||
s.mu.Lock()
|
||||
m := s.st.Mappings[0]
|
||||
o := s.findOrg(m.OrgID)
|
||||
o.TrafficSpikeFactor, o.TrafficDailyMB = 4, 500
|
||||
id := m.ID
|
||||
s.mu.Unlock()
|
||||
for min := 3 * 60; min > spikeWindow; min-- {
|
||||
s.db.addTrafficAt(id, model.Stat{BytesIn: mb}, now.Add(-time.Duration(min)*time.Minute))
|
||||
}
|
||||
|
||||
problems := func() map[string]problem {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.problemsLocked(now)
|
||||
}
|
||||
if p := problems(); p["spike:"+id].msg != "" || p["quota:"+id].msg != "" {
|
||||
t.Fatalf("steady traffic raised a problem: %+v", p)
|
||||
}
|
||||
s.db.addTrafficAt(id, model.Stat{BytesOut: 300 * mb}, now)
|
||||
p := problems()
|
||||
if !strings.Contains(p["spike:"+id].msg, "usual") || p["spike:"+id].kind != "traffic" {
|
||||
t.Fatalf("no spike problem: %+v", p)
|
||||
}
|
||||
if p["quota:"+id].msg != "" {
|
||||
t.Errorf("under the daily amount, yet: %+v", p["quota:"+id])
|
||||
}
|
||||
s.db.addTrafficAt(id, model.Stat{BytesOut: 300 * mb}, now)
|
||||
if q := problems()["quota:"+id]; !strings.Contains(q.msg, "over the") {
|
||||
t.Fatalf("no quota problem: %+v", q)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
o.TrafficSpikeFactor, o.TrafficDailyMB = 0, 0
|
||||
s.mu.Unlock()
|
||||
if p := problems(); p["spike:"+id].msg != "" || p["quota:"+id].msg != "" {
|
||||
t.Errorf("alerts off, yet: %+v", p)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user