Add aggregate configuration support and update documentation
- 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.
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user