chore: publish pxmon v0.2.0
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user