Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
88 lines
2.1 KiB
Go
88 lines
2.1 KiB
Go
package groups
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/v10networks/nmon/internal/p95"
|
|
"github.com/v10networks/nmon/internal/store"
|
|
)
|
|
|
|
// slot is the group's oriented traffic in one 5-minute window.
|
|
type slot struct {
|
|
TS int64 // window start, Unix ms
|
|
In, Out float64
|
|
// AnyIn and AnyOut say at least one member contributed a sample.
|
|
AnyIn, AnyOut bool
|
|
// FullIn and FullOut say every contributing member had a sample, so the
|
|
// sum is not understated.
|
|
FullIn, FullOut bool
|
|
}
|
|
|
|
// orient maps a member's raw directions to the group's: "toward" keeps them,
|
|
// "away" swaps them, since "in" on the port is then traffic leaving the group's
|
|
// subject.
|
|
func orient(m store.GroupMember) (inDir, outDir string) {
|
|
if m.Direction == store.DirAway {
|
|
return p95.DirOut, p95.DirIn
|
|
}
|
|
return p95.DirIn, p95.DirOut
|
|
}
|
|
|
|
// index turns sorted points into a timestamp lookup.
|
|
func index(rates map[p95.Key][]p95.Point) map[p95.Key]map[int64]float64 {
|
|
out := make(map[p95.Key]map[int64]float64, len(rates))
|
|
for k, pts := range rates {
|
|
m := make(map[int64]float64, len(pts))
|
|
for _, p := range pts {
|
|
m[p.TS] = p.V
|
|
}
|
|
out[k] = m
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sum computes the slots of [from, to). A window with no member in force has
|
|
// no slot: no membership is not missing data.
|
|
func (p plan) sum(rates map[p95.Key][]p95.Point, from, to time.Time) []slot {
|
|
idx := index(rates)
|
|
var out []slot
|
|
for t := from; t.Before(to); t = t.Add(p95.Window) {
|
|
members := p.eligibleAt(t)
|
|
if len(members) == 0 {
|
|
continue
|
|
}
|
|
s := slot{TS: t.UnixMilli(), FullIn: true, FullOut: true}
|
|
for _, m := range members {
|
|
inDir, outDir := orient(m)
|
|
if v, ok := idx[p95.Key{Iface: m.InterfaceID, Dir: p95.DirIn}][s.TS]; ok {
|
|
s.add(inDir, v)
|
|
} else {
|
|
s.miss(inDir)
|
|
}
|
|
if v, ok := idx[p95.Key{Iface: m.InterfaceID, Dir: p95.DirOut}][s.TS]; ok {
|
|
s.add(outDir, v)
|
|
} else {
|
|
s.miss(outDir)
|
|
}
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *slot) add(dir string, v float64) {
|
|
if dir == p95.DirIn {
|
|
s.In, s.AnyIn = s.In+v, true
|
|
return
|
|
}
|
|
s.Out, s.AnyOut = s.Out+v, true
|
|
}
|
|
|
|
func (s *slot) miss(dir string) {
|
|
if dir == p95.DirIn {
|
|
s.FullIn = false
|
|
return
|
|
}
|
|
s.FullOut = false
|
|
}
|