mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
164 lines
5.1 KiB
Go
164 lines
5.1 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
// IOMetrics is an alias for models.IOMetrics.
|
|
type IOMetrics = models.IOMetrics
|
|
|
|
type counterBaseline struct {
|
|
value int64
|
|
observedAt time.Time
|
|
initialized bool
|
|
}
|
|
|
|
type counterHistory struct {
|
|
diskRead counterBaseline
|
|
diskWrite counterBaseline
|
|
diskBusy counterBaseline
|
|
networkIn counterBaseline
|
|
networkOut counterBaseline
|
|
lastObservedAt time.Time
|
|
sourceUptime uint64
|
|
}
|
|
|
|
// RateTracker converts cumulative byte counters into adjacent-sample rates.
|
|
// Each counter has an independent baseline because Proxmox may omit only part
|
|
// of an otherwise valid status payload.
|
|
type RateTracker struct {
|
|
mu sync.RWMutex
|
|
history map[string]*counterHistory
|
|
}
|
|
|
|
// NewRateTracker creates a new rate tracker.
|
|
func NewRateTracker() *RateTracker {
|
|
return &RateTracker{history: make(map[string]*counterHistory)}
|
|
}
|
|
|
|
// CalculateRates calculates disk and network rates in bytes per second.
|
|
// A negative result means the upstream counter was absent, the sample was
|
|
// out-of-order, or no earlier observation exists. A returned zero is a valid
|
|
// observed idle/reset interval.
|
|
func (rt *RateTracker) CalculateRates(guestID string, current IOMetrics) (diskReadRate, diskWriteRate, netInRate, netOutRate float64) {
|
|
diskReadRate, diskWriteRate, _, netInRate, netOutRate = rt.calculateRates(guestID, current)
|
|
return
|
|
}
|
|
|
|
// CalculateRatesWithBusy also calculates disk busy percent from a cumulative
|
|
// millisecond counter.
|
|
func (rt *RateTracker) CalculateRatesWithBusy(guestID string, current IOMetrics) (diskReadRate, diskWriteRate, diskBusyPct, netInRate, netOutRate float64) {
|
|
return rt.calculateRates(guestID, current)
|
|
}
|
|
|
|
func (rt *RateTracker) calculateRates(guestID string, current IOMetrics) (diskReadRate, diskWriteRate, diskBusyPct, netInRate, netOutRate float64) {
|
|
rt.mu.Lock()
|
|
defer rt.mu.Unlock()
|
|
|
|
if current.Timestamp.IsZero() {
|
|
return -1, -1, -1, -1, -1
|
|
}
|
|
|
|
history := rt.history[guestID]
|
|
if history == nil {
|
|
history = &counterHistory{}
|
|
rt.history[guestID] = history
|
|
}
|
|
if current.SourceUptime > 0 {
|
|
if history.sourceUptime > 0 && current.SourceUptime < history.sourceUptime {
|
|
history.resetCounterEpoch()
|
|
}
|
|
history.sourceUptime = current.SourceUptime
|
|
}
|
|
|
|
presence := current.Presence.Effective()
|
|
diskReadRate = calculateCounterRate(&history.diskRead, current.DiskRead, observationTime(current.ObservedAt.DiskRead, current.Timestamp), presence.DiskRead)
|
|
diskWriteRate = calculateCounterRate(&history.diskWrite, current.DiskWrite, observationTime(current.ObservedAt.DiskWrite, current.Timestamp), presence.DiskWrite)
|
|
diskBusyRate := calculateCounterRate(&history.diskBusy, current.DiskBusy, observationTime(current.ObservedAt.DiskBusy, current.Timestamp), presence.DiskBusy)
|
|
netInRate = calculateCounterRate(&history.networkIn, current.NetworkIn, observationTime(current.ObservedAt.NetworkIn, current.Timestamp), presence.NetworkIn)
|
|
netOutRate = calculateCounterRate(&history.networkOut, current.NetworkOut, observationTime(current.ObservedAt.NetworkOut, current.Timestamp), presence.NetworkOut)
|
|
|
|
if diskBusyRate < 0 {
|
|
diskBusyPct = -1
|
|
} else {
|
|
// DiskBusy is cumulative busy milliseconds, so ms/s divided by ten is
|
|
// the percentage of wall time spent busy.
|
|
diskBusyPct = diskBusyRate / 10
|
|
if diskBusyPct > 100 {
|
|
diskBusyPct = 100
|
|
}
|
|
}
|
|
|
|
if current.Timestamp.After(history.lastObservedAt) {
|
|
history.lastObservedAt = current.Timestamp
|
|
}
|
|
return
|
|
}
|
|
|
|
func (history *counterHistory) resetCounterEpoch() {
|
|
history.diskRead = counterBaseline{}
|
|
history.diskWrite = counterBaseline{}
|
|
history.diskBusy = counterBaseline{}
|
|
history.networkIn = counterBaseline{}
|
|
history.networkOut = counterBaseline{}
|
|
}
|
|
|
|
func observationTime(counterTime, fallback time.Time) time.Time {
|
|
if counterTime.IsZero() {
|
|
return fallback
|
|
}
|
|
return counterTime
|
|
}
|
|
|
|
func calculateCounterRate(baseline *counterBaseline, value int64, observedAt time.Time, present bool) float64 {
|
|
if !present {
|
|
return -1
|
|
}
|
|
if !baseline.initialized {
|
|
baseline.value = value
|
|
baseline.observedAt = observedAt
|
|
baseline.initialized = true
|
|
return -1
|
|
}
|
|
if !observedAt.After(baseline.observedAt) {
|
|
return -1
|
|
}
|
|
|
|
elapsed := observedAt.Sub(baseline.observedAt).Seconds()
|
|
previous := baseline.value
|
|
baseline.value = value
|
|
baseline.observedAt = observedAt
|
|
|
|
if value < previous {
|
|
// A guest restart, migration reconnect, or counter wrap starts a new
|
|
// cumulative epoch. Rebase without inventing a negative or huge rate.
|
|
return 0
|
|
}
|
|
return float64(value-previous) / elapsed
|
|
}
|
|
|
|
// Clear removes all tracked data.
|
|
func (rt *RateTracker) Clear() {
|
|
rt.mu.Lock()
|
|
defer rt.mu.Unlock()
|
|
rt.history = make(map[string]*counterHistory)
|
|
}
|
|
|
|
// Cleanup removes resources that have not supplied any newer sample since the
|
|
// cutoff. Idle and partial samples still refresh resource observation time.
|
|
func (rt *RateTracker) Cleanup(cutoff time.Time) (removed int) {
|
|
rt.mu.Lock()
|
|
defer rt.mu.Unlock()
|
|
|
|
for guestID, history := range rt.history {
|
|
if history.lastObservedAt.Before(cutoff) {
|
|
delete(rt.history, guestID)
|
|
removed++
|
|
}
|
|
}
|
|
return removed
|
|
}
|