Files
telemt-api/internal/aggregate/merge.go
T
Denozordec 54306ec6c4
Publish telemt-api gateway Docker image / test (push) Failing after 27s
Publish telemt-api gateway Docker image / build-and-push (push) Has been skipped
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.
2026-03-30 00:51:19 +07:00

273 lines
5.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
}