diff --git a/model/model.go b/model/model.go index e5f5143..162f279 100644 --- a/model/model.go +++ b/model/model.go @@ -2,6 +2,7 @@ package model import ( + "sort" "strings" "time" ) @@ -287,6 +288,42 @@ type Stat struct { Conns int64 `json:"conns"` BytesIn int64 `json:"bytesIn"` // client -> target BytesOut int64 `json:"bytesOut"` // target -> client + // Sources are the busiest client addresses behind these numbers (at most MaxSources), + // busiest first. Addresses beyond that are only counted in the totals above. + Sources []SourceStat `json:"sources,omitempty"` +} + +// MaxSources is how many client addresses an edge names per port in one report. +const MaxSources = 10 + +// SourceStat is one client address's traffic on a port since the last report. +type SourceStat struct { + IP string `json:"ip"` + Conns int64 `json:"conns"` + BytesIn int64 `json:"bytesIn"` + BytesOut int64 `json:"bytesOut"` +} + +// TopSources keeps the n busiest of a port's client addresses (by bytes, then connections), busiest first. +func TopSources(all map[string]*SourceStat, n int) []SourceStat { + out := make([]SourceStat, 0, len(all)) + for _, s := range all { + out = append(out, *s) + } + sort.Slice(out, func(i, j int) bool { + a, b := out[i], out[j] + if ta, tb := a.BytesIn+a.BytesOut, b.BytesIn+b.BytesOut; ta != tb { + return ta > tb + } + if a.Conns != b.Conns { + return a.Conns > b.Conns + } + return a.IP < b.IP + }) + if len(out) > n { + out = out[:n] + } + return out } // LogEntry is one access-log line. @@ -325,16 +362,19 @@ type ContainerPort struct { // EdgeReport is what blacktip posts back. type EdgeReport struct { - IPs []string `json:"ips"` - PublicKey string `json:"publicKey"` // the edge's WireGuard public key - Status map[string]MappingStatus `json:"status"` - Stats map[string]Stat `json:"stats"` // deltas since last report - Containers []Container `json:"containers"` - MailChecks map[string]MailCheck `json:"mailChecks"` // by mail-egress ID - Version string `json:"version,omitempty"` - AutoUpdate bool `json:"autoUpdate,omitempty"` - UpdateMsg string `json:"updateMsg,omitempty"` // outcome of the update asked for by UpdateID - UpdateID string `json:"updateId,omitempty"` + IPs []string `json:"ips"` + PublicKey string `json:"publicKey"` // the edge's WireGuard public key + Status map[string]MappingStatus `json:"status"` + Stats map[string]Stat `json:"stats"` // deltas since last report + // StatsSeconds is how long Stats covers (the time since the previous report that carried them), + // so a rate can be worked out however irregular the reports are. + StatsSeconds float64 `json:"statsSeconds,omitempty"` + Containers []Container `json:"containers"` + MailChecks map[string]MailCheck `json:"mailChecks"` // by mail-egress ID + Version string `json:"version,omitempty"` + AutoUpdate bool `json:"autoUpdate,omitempty"` + UpdateMsg string `json:"updateMsg,omitempty"` // outcome of the update asked for by UpdateID + UpdateID string `json:"updateId,omitempty"` // progress of that update, for the dashboard's progress bar and log UpdateStage string `json:"updateStage,omitempty"` UpdatePct int `json:"updatePct,omitempty"` diff --git a/model/model_test.go b/model/model_test.go index bdb18fc..742e2b4 100644 --- a/model/model_test.go +++ b/model/model_test.go @@ -23,3 +23,19 @@ func TestRedirectLocation(t *testing.T) { t.Error("IsRedirect is wrong") } } + +func TestTopSources(t *testing.T) { + all := map[string]*SourceStat{ + "a": {IP: "a", Conns: 1, BytesIn: 10, BytesOut: 5}, + "b": {IP: "b", Conns: 9, BytesIn: 1}, + "c": {IP: "c", Conns: 2, BytesIn: 100}, + "d": {IP: "d", Conns: 3, BytesIn: 1}, + } + got := TopSources(all, 3) + if len(got) != 3 || got[0].IP != "c" || got[1].IP != "a" || got[2].IP != "b" { + t.Fatalf("order: %+v", got) // c most bytes, a next, then b beats d on connections + } + if got := TopSources(all, 10); len(got) != 4 { + t.Fatalf("all: %+v", got) + } +}