model: per-port top source addresses and the span a report's stats cover

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 16:53:56 -06:00
1 parent 6663b268c8
commit cb07e08e7b
2 files changed
+66 -10

No files matched your search

+50 -10
View File
@@ -2,6 +2,7 @@
package model package model
import ( import (
"sort"
"strings" "strings"
"time" "time"
) )
@@ -287,6 +288,42 @@ type Stat struct {
Conns int64 `json:"conns"` Conns int64 `json:"conns"`
BytesIn int64 `json:"bytesIn"` // client -> target BytesIn int64 `json:"bytesIn"` // client -> target
BytesOut int64 `json:"bytesOut"` // target -> client 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. // LogEntry is one access-log line.
@@ -325,16 +362,19 @@ type ContainerPort struct {
// EdgeReport is what blacktip posts back. // EdgeReport is what blacktip posts back.
type EdgeReport struct { type EdgeReport struct {
IPs []string `json:"ips"` IPs []string `json:"ips"`
PublicKey string `json:"publicKey"` // the edge's WireGuard public key PublicKey string `json:"publicKey"` // the edge's WireGuard public key
Status map[string]MappingStatus `json:"status"` Status map[string]MappingStatus `json:"status"`
Stats map[string]Stat `json:"stats"` // deltas since last report Stats map[string]Stat `json:"stats"` // deltas since last report
Containers []Container `json:"containers"` // StatsSeconds is how long Stats covers (the time since the previous report that carried them),
MailChecks map[string]MailCheck `json:"mailChecks"` // by mail-egress ID // so a rate can be worked out however irregular the reports are.
Version string `json:"version,omitempty"` StatsSeconds float64 `json:"statsSeconds,omitempty"`
AutoUpdate bool `json:"autoUpdate,omitempty"` Containers []Container `json:"containers"`
UpdateMsg string `json:"updateMsg,omitempty"` // outcome of the update asked for by UpdateID MailChecks map[string]MailCheck `json:"mailChecks"` // by mail-egress ID
UpdateID string `json:"updateId,omitempty"` 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 // progress of that update, for the dashboard's progress bar and log
UpdateStage string `json:"updateStage,omitempty"` UpdateStage string `json:"updateStage,omitempty"`
UpdatePct int `json:"updatePct,omitempty"` UpdatePct int `json:"updatePct,omitempty"`
+16
View File
@@ -23,3 +23,19 @@ func TestRedirectLocation(t *testing.T) {
t.Error("IsRedirect is wrong") 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)
}
}