Add aggregate configuration support and update documentation
Publish telemt-api gateway Docker image / test (push) Failing after 27s
Publish telemt-api gateway Docker image / build-and-push (push) Has been skipped

- 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:
Denozordec
2026-03-30 00:51:19 +07:00
parent 89638d29bb
commit 54306ec6c4
12 changed files with 1031 additions and 6 deletions
+272
View File
@@ -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,
}
}