package cluster import ( "errors" "fmt" "math" "strings" "pxmon/internal/history" ) type InterfaceP95Snapshot struct { ClusterName string `json:"cluster"` ClusterID string `json:"cluster_id"` Interface string `json:"interface"` Range history.RangeShortcut `json:"range"` Samples int `json:"samples"` P95Mbps float64 `json:"p95_mbps"` AvgMbps float64 `json:"avg_mbps"` MaxMbps float64 `json:"max_mbps"` Series []history.NodeSamplePoint `json:"series,omitempty"` } func (s *Service) CollectInterfaceP95(selector, iface string, rng history.RangeShortcut) (InterfaceP95Snapshot, error) { iface = strings.TrimSpace(iface) if iface == "" { return InterfaceP95Snapshot{}, errors.New("interface is required") } c, err := s.Get(selector) if err != nil { return InterfaceP95Snapshot{}, err } store := s.NetworkStore() if store == nil { return InterfaceP95Snapshot{}, errors.New("history store not configured") } snaps, err := store.Load(c.ID, rng.Since(s.now())) if err != nil { return InterfaceP95Snapshot{}, err } series := history.AggregateNodeSeries(snaps, iface) out := InterfaceP95Snapshot{ ClusterName: c.Name, ClusterID: c.ID, Interface: iface, Range: rng, Samples: len(series), P95Mbps: history.PercentileMbps(series, 95), Series: series, } if len(series) == 0 { return out, nil } var sum, maxV float64 for _, p := range series { sum += p.TotalMbps if p.TotalMbps > maxV { maxV = p.TotalMbps } } out.AvgMbps = sum / float64(len(series)) out.MaxMbps = maxV return out, nil } func (s *Service) RenderInterfaceP95GraphPNG(snap InterfaceP95Snapshot) ([]byte, error) { if len(snap.Series) == 0 { return history.RenderNodeNetworkPNG([]history.NodeSamplePoint{}, history.ChartOptions{ Title: fmt.Sprintf("%s: %s", snap.ClusterName, snap.Interface), Subtitle: "No samples", }) } subtitle := fmt.Sprintf("P95 %.1f Mbps | max %.1f Mbps | avg %.1f Mbps", snap.P95Mbps, snap.MaxMbps, snap.AvgMbps) if math.IsNaN(snap.P95Mbps) { subtitle = "No samples" } return history.RenderNodeNetworkPNG(snap.Series, history.ChartOptions{ Title: fmt.Sprintf("%s: %s", snap.ClusterName, snap.Interface), Subtitle: subtitle, Percentile: 95, }) }