- Introduced AggregateConfig to manage aggregation settings in the gateway configuration. - Added validation for reserved alias 'agg' and included tests for aggregate alias handling. - Updated config.example.yaml to demonstrate aggregate configuration options. - Enhanced README.md to include information about the new aggregation endpoint and its usage. - Modified gateway.go to integrate the new aggregate handler for processing aggregation requests.
273 lines
5.9 KiB
Go
273 lines
5.9 KiB
Go
package aggregate
|
||
|
||
import (
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
// BuildTraffic builds traffic matrix from successful fetches only.
|
||
func BuildTraffic(results []ServerFetchResult) []TrafficRow {
|
||
perUser := map[string]map[string]TrafficServerStats{}
|
||
|
||
for _, fr := range results {
|
||
if !fr.OK {
|
||
continue
|
||
}
|
||
for _, u := range fr.Users {
|
||
if perUser[u.Username] == nil {
|
||
perUser[u.Username] = map[string]TrafficServerStats{}
|
||
}
|
||
perUser[u.Username][fr.Alias] = TrafficServerStats{
|
||
TotalOctets: u.TotalOctets,
|
||
CurrentConnections: u.CurrentConnections,
|
||
Revision: fr.Revision,
|
||
}
|
||
}
|
||
}
|
||
|
||
names := make([]string, 0, len(perUser))
|
||
for n := range perUser {
|
||
names = append(names, n)
|
||
}
|
||
sort.Strings(names)
|
||
|
||
rows := make([]TrafficRow, 0, len(names))
|
||
for _, name := range names {
|
||
rows = append(rows, TrafficRow{
|
||
Username: name,
|
||
Servers: perUser[name],
|
||
})
|
||
}
|
||
return rows
|
||
}
|
||
|
||
// BuildUniqueIPs builds IP × server visibility per user.
|
||
func BuildUniqueIPs(results []ServerFetchResult) []UniqueIPsRow {
|
||
type ipKey struct {
|
||
user string
|
||
ip string
|
||
}
|
||
active := map[ipKey]map[string]struct{}{}
|
||
recent := map[ipKey]map[string]struct{}{}
|
||
|
||
for _, fr := range results {
|
||
if !fr.OK {
|
||
continue
|
||
}
|
||
for _, u := range fr.Users {
|
||
for _, ip := range u.ActiveUniqueIPsList {
|
||
ip = strings.TrimSpace(ip)
|
||
if ip == "" {
|
||
continue
|
||
}
|
||
k := ipKey{user: u.Username, ip: ip}
|
||
if active[k] == nil {
|
||
active[k] = map[string]struct{}{}
|
||
}
|
||
active[k][fr.Alias] = struct{}{}
|
||
}
|
||
for _, ip := range u.RecentUniqueIPsList {
|
||
ip = strings.TrimSpace(ip)
|
||
if ip == "" {
|
||
continue
|
||
}
|
||
k := ipKey{user: u.Username, ip: ip}
|
||
if recent[k] == nil {
|
||
recent[k] = map[string]struct{}{}
|
||
}
|
||
recent[k][fr.Alias] = struct{}{}
|
||
}
|
||
}
|
||
}
|
||
|
||
users := map[string][]ipKey{}
|
||
for k := range active {
|
||
users[k.user] = append(users[k.user], k)
|
||
}
|
||
for k := range recent {
|
||
found := false
|
||
for _, x := range users[k.user] {
|
||
if x == k {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
users[k.user] = append(users[k.user], k)
|
||
}
|
||
}
|
||
|
||
userNames := make([]string, 0, len(users))
|
||
for u := range users {
|
||
userNames = append(userNames, u)
|
||
}
|
||
sort.Strings(userNames)
|
||
|
||
out := make([]UniqueIPsRow, 0, len(userNames))
|
||
for _, uname := range userNames {
|
||
keys := users[uname]
|
||
sort.Slice(keys, func(i, j int) bool { return keys[i].ip < keys[j].ip })
|
||
ips := make([]IPAssignments, 0, len(keys))
|
||
for _, k := range keys {
|
||
a := sortedKeys(active[k])
|
||
r := sortedKeys(recent[k])
|
||
var primary *string
|
||
if len(a) == 1 {
|
||
s := a[0]
|
||
primary = &s
|
||
}
|
||
ips = append(ips, IPAssignments{
|
||
IP: k.ip,
|
||
ActiveOnServers: a,
|
||
RecentOnServers: r,
|
||
PrimaryServer: primary,
|
||
})
|
||
}
|
||
out = append(out, UniqueIPsRow{Username: uname, IPs: ips})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func sortedKeys(m map[string]struct{}) []string {
|
||
if len(m) == 0 {
|
||
return nil
|
||
}
|
||
s := make([]string, 0, len(m))
|
||
for k := range m {
|
||
s = append(s, k)
|
||
}
|
||
sort.Strings(s)
|
||
return s
|
||
}
|
||
|
||
// BuildUsers merged rows with totals and optional links from first successful server per user.
|
||
func BuildUsers(results []ServerFetchResult, includeLinks bool, minTotalOctets uint64) []UsersRow {
|
||
type acc struct {
|
||
byServer map[string]TrafficServerStats
|
||
links *UserLinks
|
||
total uint64
|
||
act uint64
|
||
rec uint64
|
||
}
|
||
m := map[string]*acc{}
|
||
|
||
for _, fr := range results {
|
||
if !fr.OK {
|
||
continue
|
||
}
|
||
for _, u := range fr.Users {
|
||
a := m[u.Username]
|
||
if a == nil {
|
||
a = &acc{byServer: map[string]TrafficServerStats{}}
|
||
m[u.Username] = a
|
||
}
|
||
a.byServer[fr.Alias] = TrafficServerStats{
|
||
TotalOctets: u.TotalOctets,
|
||
CurrentConnections: u.CurrentConnections,
|
||
Revision: fr.Revision,
|
||
}
|
||
a.total += u.TotalOctets
|
||
if u.ActiveUniqueIPs > a.act {
|
||
a.act = u.ActiveUniqueIPs
|
||
}
|
||
if u.RecentUniqueIPs > a.rec {
|
||
a.rec = u.RecentUniqueIPs
|
||
}
|
||
if includeLinks && u.Links != nil && a.links == nil {
|
||
a.links = u.Links
|
||
}
|
||
}
|
||
}
|
||
|
||
names := make([]string, 0, len(m))
|
||
for n := range m {
|
||
if m[n].total >= minTotalOctets {
|
||
names = append(names, n)
|
||
}
|
||
}
|
||
sort.Strings(names)
|
||
|
||
rows := make([]UsersRow, 0, len(names))
|
||
for _, name := range names {
|
||
a := m[name]
|
||
rows = append(rows, UsersRow{
|
||
Username: name,
|
||
TotalOctets: a.total,
|
||
ByServer: a.byServer,
|
||
Links: a.links,
|
||
ActiveUniqueIPs: a.act,
|
||
RecentUniqueIPs: a.rec,
|
||
})
|
||
}
|
||
return rows
|
||
}
|
||
|
||
// BuildSummary computes fleet totals and top users.
|
||
func BuildSummary(results []ServerFetchResult, topN int) SummaryData {
|
||
if topN <= 0 {
|
||
topN = 10
|
||
}
|
||
if topN > 1000 {
|
||
topN = 1000
|
||
}
|
||
|
||
sumByUser := map[string]uint64{}
|
||
var fleetOctets, fleetConn uint64
|
||
ok, fail := 0, 0
|
||
serverRows := make([]ServerFetchResult, 0, len(results))
|
||
|
||
for _, fr := range results {
|
||
sr := ServerFetchResult{
|
||
Alias: fr.Alias,
|
||
OK: fr.OK,
|
||
HTTPStatus: fr.HTTPStatus,
|
||
LatencyMs: fr.LatencyMs,
|
||
Error: fr.Error,
|
||
Revision: fr.Revision,
|
||
}
|
||
serverRows = append(serverRows, sr)
|
||
if !fr.OK {
|
||
fail++
|
||
continue
|
||
}
|
||
ok++
|
||
for _, u := range fr.Users {
|
||
fleetOctets += u.TotalOctets
|
||
fleetConn += u.CurrentConnections
|
||
sumByUser[u.Username] += u.TotalOctets
|
||
}
|
||
}
|
||
|
||
type pair struct {
|
||
name string
|
||
n uint64
|
||
}
|
||
pairs := make([]pair, 0, len(sumByUser))
|
||
for n, v := range sumByUser {
|
||
pairs = append(pairs, pair{name: n, n: v})
|
||
}
|
||
sort.Slice(pairs, func(i, j int) bool {
|
||
if pairs[i].n != pairs[j].n {
|
||
return pairs[i].n > pairs[j].n
|
||
}
|
||
return pairs[i].name < pairs[j].name
|
||
})
|
||
if len(pairs) > topN {
|
||
pairs = pairs[:topN]
|
||
}
|
||
top := make([]TopUser, 0, len(pairs))
|
||
for _, p := range pairs {
|
||
top = append(top, TopUser{Username: p.name, TotalOctets: p.n})
|
||
}
|
||
|
||
return SummaryData{
|
||
Servers: serverRows,
|
||
ServersTotal: len(results),
|
||
ServersOK: ok,
|
||
ServersFailed: fail,
|
||
FleetTotalOctets: fleetOctets,
|
||
FleetTotalConnections: fleetConn,
|
||
TopUsers: top,
|
||
}
|
||
}
|