chore: publish pxmon v0.2.0

This commit is contained in:
2026-06-16 21:52:10 +04:00
commit 6b703db02b
69 changed files with 25886 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
package history
import (
"math"
"sort"
"strings"
"time"
)
// NodeSamplePoint is one aggregated sample for the node's primary uplink
// interface at a single point in time. TotalMbps uses max(Rx, Tx) which is
// the same convention a provider uses for 95th-percentile transit billing.
type NodeSamplePoint struct {
Timestamp time.Time
TotalMbps float64 // max(Rx, Tx) for billing P95
RxMbps float64
TxMbps float64
Interface string
}
// isVirtualIface returns true for interfaces that do not represent real
// uplink traffic and should be excluded from P95/billing calculations.
// Virtual interfaces (bridges, taps, veth pairs, docker, loopback) either
// carry no real traffic or mirror the traffic that already flows through
// the physical uplink — double-counting them inflates totals.
func isVirtualIface(name string) bool {
n := strings.ToLower(name)
if n == "lo" || n == "" {
return true
}
prefixes := []string{
"lo", "docker", "br-", "br", "veth", "vnet", "tap", "virbr",
"cni", "flannel", "wg", "tun", "tailscale", "zt", "ipsec",
"kube", "cilium", "ovs", "podman", "dummy",
}
for _, p := range prefixes {
if strings.HasPrefix(n, p) {
return true
}
}
return false
}
// PrimaryInterface picks the physical uplink interface with the highest
// average max(Rx, Tx) over the given snapshots. Virtual interfaces are
// ignored. Returns empty string when no physical candidate exists.
func PrimaryInterface(snapshots []NetworkSnapshot) string {
type acc struct {
sum float64
count int
}
totals := make(map[string]*acc, 16)
for _, snap := range snapshots {
for _, iface := range snap.Interfaces {
if isVirtualIface(iface.Interface) {
continue
}
v := math.Max(iface.RxMbps, iface.TxMbps)
a, ok := totals[iface.Interface]
if !ok {
a = &acc{}
totals[iface.Interface] = a
}
a.sum += v
a.count++
}
}
best := ""
bestAvg := -1.0
for name, a := range totals {
if a.count == 0 {
continue
}
avg := a.sum / float64(a.count)
if avg > bestAvg {
bestAvg = avg
best = name
}
}
return best
}
// AggregateNodeSeries builds a time series for a single interface. If
// ifaceName is empty, PrimaryInterface is used to auto-select the physical
// uplink. TotalMbps uses max(Rx, Tx), matching provider billing convention.
func AggregateNodeSeries(snapshots []NetworkSnapshot, ifaceName string) []NodeSamplePoint {
if ifaceName == "" {
ifaceName = PrimaryInterface(snapshots)
}
out := make([]NodeSamplePoint, 0, len(snapshots))
for _, snap := range snapshots {
for _, iface := range snap.Interfaces {
if iface.Interface != ifaceName {
continue
}
rx := iface.RxMbps
tx := iface.TxMbps
out = append(out, NodeSamplePoint{
Timestamp: snap.Timestamp,
Interface: ifaceName,
RxMbps: rx,
TxMbps: tx,
TotalMbps: math.Max(rx, tx),
})
break
}
}
sort.Slice(out, func(i, j int) bool {
return out[i].Timestamp.Before(out[j].Timestamp)
})
return out
}
// PercentileMbps returns the given percentile (0..100) of the TotalMbps
// field across the series using nearest-rank (inclusive) computation.
// Returns 0 if the series is empty.
func PercentileMbps(points []NodeSamplePoint, percentile float64) float64 {
if len(points) == 0 {
return 0
}
if percentile < 0 {
percentile = 0
}
if percentile > 100 {
percentile = 100
}
values := make([]float64, 0, len(points))
for _, p := range points {
values = append(values, p.TotalMbps)
}
sort.Float64s(values)
if len(values) == 1 {
return values[0]
}
rank := (percentile / 100.0) * float64(len(values)-1)
lo := int(math.Floor(rank))
hi := int(math.Ceil(rank))
if lo == hi {
return values[lo]
}
frac := rank - float64(lo)
return values[lo]*(1-frac) + values[hi]*frac
}
// TopInterfaceByTraffic returns the physical interface with the highest
// average max(Rx, Tx) across the sampled period, together with that
// average throughput in Mbps. Virtual interfaces are ignored.
func TopInterfaceByTraffic(snapshots []NetworkSnapshot) (string, float64) {
type acc struct {
sum float64
count int
}
totals := make(map[string]*acc, 16)
for _, snap := range snapshots {
for _, iface := range snap.Interfaces {
if isVirtualIface(iface.Interface) {
continue
}
v := math.Max(iface.RxMbps, iface.TxMbps)
a, ok := totals[iface.Interface]
if !ok {
a = &acc{}
totals[iface.Interface] = a
}
a.sum += v
a.count++
}
}
name := ""
bestAvg := 0.0
for k, a := range totals {
if a.count == 0 {
continue
}
avg := a.sum / float64(a.count)
if avg > bestAvg {
bestAvg = avg
name = k
}
}
return name, bestAvg
}
// RangeShortcut is a common time-window selector.
type RangeShortcut string
const (
RangeLive RangeShortcut = "live"
RangeHour RangeShortcut = "1h"
RangeDay RangeShortcut = "1d"
RangeMonth RangeShortcut = "1mo"
RangeAll RangeShortcut = "all"
)
// Since returns an absolute start time for the given range shortcut,
// relative to now. RangeAll and RangeLive return zero (no lower bound).
func (r RangeShortcut) Since(now time.Time) time.Time {
switch r {
case RangeHour:
return now.Add(-time.Hour)
case RangeDay:
return now.Add(-24 * time.Hour)
case RangeMonth:
return now.Add(-30 * 24 * time.Hour)
default:
return time.Time{}
}
}
// ParseRangeShortcut accepts user-provided range strings.
func ParseRangeShortcut(raw string) (RangeShortcut, bool) {
switch raw {
case "live", "now":
return RangeLive, true
case "1h", "hour":
return RangeHour, true
case "1d", "day", "24h":
return RangeDay, true
case "1mo", "30d", "month":
return RangeMonth, true
case "all", "":
return RangeAll, true
}
return "", false
}
// Label returns a human-friendly label for the range.
func (r RangeShortcut) Label() string {
switch r {
case RangeLive:
return "live"
case RangeHour:
return "last 1h"
case RangeDay:
return "last 24h"
case RangeMonth:
return "last 30d"
case RangeAll:
return "all time"
}
return string(r)
}