package cluster import ( "math" "sort" "strings" "time" "pxmon/internal/history" ) type AvailabilityVM struct { Name string `json:"name"` Availability float64 `json:"availability_pct"` Samples int `json:"samples"` Running int `json:"running_samples"` } type AvailabilityReport struct { Cluster string `json:"cluster"` Range string `json:"range"` Samples int `json:"samples"` UpSamples int `json:"up_samples"` Availability float64 `json:"availability_pct"` VMs []AvailabilityVM `json:"vms,omitempty"` } func (s *Service) AvailabilityReport(selector string, since time.Time, vmFilter string) (AvailabilityReport, error) { c, err := s.Get(selector) if err != nil { return AvailabilityReport{}, err } store := history.NewAvailabilityStore(s.DataDir()) snaps, err := store.Load(c.ID, since) if err != nil { return AvailabilityReport{}, err } rep := AvailabilityReport{Cluster: c.Name, Samples: len(snaps)} if len(snaps) == 0 { return rep, nil } vmFilter = strings.TrimSpace(vmFilter) totalUp := 0 type acc struct{ samples, running int } vmap := map[string]*acc{} for _, snap := range snaps { if snap.ClusterUp { totalUp++ } for vm, st := range snap.VMStates { if vmFilter != "" && !strings.EqualFold(vmFilter, vm) { continue } a := vmap[vm] if a == nil { a = &acc{} vmap[vm] = a } a.samples++ if strings.EqualFold(strings.TrimSpace(st), "running") { a.running++ } } } rep.UpSamples = totalUp rep.Availability = 100 * float64(totalUp) / float64(len(snaps)) for vm, a := range vmap { if a.samples == 0 { continue } rep.VMs = append(rep.VMs, AvailabilityVM{ Name: vm, Samples: a.samples, Running: a.running, Availability: 100 * float64(a.running) / float64(a.samples), }) } sort.Slice(rep.VMs, func(i, j int) bool { return rep.VMs[i].Name < rep.VMs[j].Name }) return rep, nil } type CapacityForecastItem struct { Mount string `json:"mount"` UsedPct float64 `json:"used_pct"` SlopeBytesSec float64 `json:"slope_bytes_per_sec"` DaysTo90 float64 `json:"days_to_90_pct"` DaysTo95 float64 `json:"days_to_95_pct"` } type CapacityForecastReport struct { Cluster string `json:"cluster"` Samples int `json:"samples"` Items []CapacityForecastItem `json:"items,omitempty"` } func (s *Service) CapacityForecast(selector string, since time.Time) (CapacityForecastReport, error) { c, err := s.Get(selector) if err != nil { return CapacityForecastReport{}, err } store := history.NewCapacityStore(s.DataDir()) snaps, err := store.Load(c.ID, since) if err != nil { return CapacityForecastReport{}, err } rep := CapacityForecastReport{Cluster: c.Name, Samples: len(snaps)} if len(snaps) < 2 { return rep, nil } type point struct { ts time.Time used float64 tot float64 } byMount := map[string][]point{} for _, snap := range snaps { for _, d := range snap.Disks { if d.TotalBytes == 0 { continue } byMount[d.Mount] = append(byMount[d.Mount], point{ts: snap.Timestamp, used: float64(d.UsedBytes), tot: float64(d.TotalBytes)}) } } for mnt, pts := range byMount { if len(pts) < 2 { continue } sort.Slice(pts, func(i, j int) bool { return pts[i].ts.Before(pts[j].ts) }) first := pts[0] last := pts[len(pts)-1] dt := last.ts.Sub(first.ts).Seconds() if dt <= 0 { continue } slope := (last.used - first.used) / dt usedPct := 100 * last.used / last.tot d90 := daysToTarget(last.used, last.tot*0.90, slope) d95 := daysToTarget(last.used, last.tot*0.95, slope) rep.Items = append(rep.Items, CapacityForecastItem{ Mount: mnt, UsedPct: usedPct, SlopeBytesSec: slope, DaysTo90: d90, DaysTo95: d95, }) } sort.Slice(rep.Items, func(i, j int) bool { return rep.Items[i].UsedPct > rep.Items[j].UsedPct }) return rep, nil } func daysToTarget(current, target, slope float64) float64 { if target <= current { return 0 } if slope <= 0 { return math.Inf(1) } return (target - current) / slope / 86400 }