Files
ITWorx-Pulse-Public/internal/disk/performance.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

188 lines
6.2 KiB
Go

package disk
import (
"errors"
"math"
"sort"
"strings"
"time"
)
type RawPerformanceSample struct {
ObservedAt time.Time `json:"observedAt"`
ReadBytesPerSecond *float64 `json:"readBytesPerSecond,omitempty"`
WriteBytesPerSecond *float64 `json:"writeBytesPerSecond,omitempty"`
ReadIOPS *float64 `json:"readIops,omitempty"`
WriteIOPS *float64 `json:"writeIops,omitempty"`
ReadLatencyMs *float64 `json:"readLatencyMs,omitempty"`
WriteLatencyMs *float64 `json:"writeLatencyMs,omitempty"`
}
type PerformanceSample struct {
ObservedAt time.Time `json:"observedAt"`
ReadBytesPerSecond *float64 `json:"readBytesPerSecond,omitempty"`
WriteBytesPerSecond *float64 `json:"writeBytesPerSecond,omitempty"`
ReadIOPS *float64 `json:"readIops,omitempty"`
WriteIOPS *float64 `json:"writeIops,omitempty"`
ReadLatencyMs *float64 `json:"readLatencyMs,omitempty"`
WriteLatencyMs *float64 `json:"writeLatencyMs,omitempty"`
}
type RawPerformance struct {
Available bool `json:"available"`
Current RawPerformanceSample `json:"current"`
History []RawPerformanceSample `json:"history,omitempty"`
}
type Performance struct {
State string `json:"state"`
Current PerformanceSample `json:"current"`
History []PerformanceSample `json:"history,omitempty"`
}
type RawTemperature struct {
Available bool `json:"available"`
Celsius float64 `json:"celsius"`
ObservedAt time.Time `json:"observedAt"`
}
type Temperature struct {
State string `json:"state"`
Celsius *float64 `json:"celsius,omitempty"`
Status string `json:"status"`
ObservedAt *time.Time `json:"observedAt,omitempty"`
}
type RawSpin struct {
Supported bool `json:"supported"`
State string `json:"state"`
}
type Spin struct {
State string `json:"state"`
}
type PerformancePolicy struct {
MaxHistory int
TemperatureWarningCelsius float64
TemperatureCriticalCelsius float64
TemperatureRecoveryCelsius float64
}
func (p PerformancePolicy) withDefaults() PerformancePolicy {
if p.MaxHistory == 0 {
p.MaxHistory = 120
}
if p.TemperatureWarningCelsius == 0 {
p.TemperatureWarningCelsius = 50
}
if p.TemperatureCriticalCelsius == 0 {
p.TemperatureCriticalCelsius = 55
}
if p.TemperatureRecoveryCelsius == 0 {
p.TemperatureRecoveryCelsius = 45
}
return p
}
func (p PerformancePolicy) Validate() error {
if p.MaxHistory < 1 || p.MaxHistory > 1000 || p.TemperatureRecoveryCelsius >= p.TemperatureWarningCelsius || p.TemperatureWarningCelsius >= p.TemperatureCriticalCelsius {
return errors.New("disk performance policy is outside safe bounds")
}
return nil
}
func normalizePerformance(raw *RawPerformance, now time.Time, policy PerformancePolicy) (*Performance, error) {
if raw == nil {
return nil, nil
}
policy = policy.withDefaults()
if err := policy.Validate(); err != nil {
return nil, err
}
result := &Performance{State: "unknown", History: []PerformanceSample{}}
if !raw.Available {
result.State = "unsupported"
return result, nil
}
if len(raw.History) > policy.MaxHistory {
return nil, errors.New("disk performance history exceeds bounds")
}
current, err := normalizeSample(raw.Current, now)
if err != nil {
return nil, err
}
result.State = "available"
result.Current = current
for _, item := range raw.History {
sample, sampleErr := normalizeSample(item, now)
if sampleErr != nil {
return nil, sampleErr
}
result.History = append(result.History, sample)
}
sort.Slice(result.History, func(i, j int) bool { return result.History[i].ObservedAt.After(result.History[j].ObservedAt) })
return result, nil
}
func normalizeSample(raw RawPerformanceSample, now time.Time) (PerformanceSample, error) {
observed := raw.ObservedAt
if observed.IsZero() {
observed = now
}
if observed.After(now.Add(time.Minute)) {
return PerformanceSample{}, errors.New("disk performance observation is materially in the future")
}
for _, value := range []*float64{raw.ReadBytesPerSecond, raw.WriteBytesPerSecond, raw.ReadIOPS, raw.WriteIOPS, raw.ReadLatencyMs, raw.WriteLatencyMs} {
if value != nil && (*value < 0 || math.IsNaN(*value) || math.IsInf(*value, 0)) {
return PerformanceSample{}, errors.New("disk performance value is invalid")
}
}
return PerformanceSample{ObservedAt: observed.UTC(), ReadBytesPerSecond: raw.ReadBytesPerSecond, WriteBytesPerSecond: raw.WriteBytesPerSecond, ReadIOPS: raw.ReadIOPS, WriteIOPS: raw.WriteIOPS, ReadLatencyMs: raw.ReadLatencyMs, WriteLatencyMs: raw.WriteLatencyMs}, nil
}
func normalizeTemperature(raw *RawTemperature, now time.Time, policy PerformancePolicy) (*Temperature, error) {
if raw == nil {
return nil, nil
}
result := &Temperature{State: "unknown", Status: "unknown"}
if !raw.Available {
return result, nil
}
if raw.Celsius < -50 || raw.Celsius > 150 || math.IsNaN(raw.Celsius) || math.IsInf(raw.Celsius, 0) {
return nil, errors.New("disk temperature is invalid")
}
observed := raw.ObservedAt
if observed.IsZero() {
observed = now
}
if observed.After(now.Add(time.Minute)) {
return nil, errors.New("disk temperature observation is materially in the future")
}
result.State = "available"
result.Celsius = &raw.Celsius
value := observed.UTC()
result.ObservedAt = &value
result.Status = temperatureStatus("normal", raw.Celsius, policy)
return result, nil
}
func temperatureStatus(previous string, celsius float64, policy PerformancePolicy) string {
policy = policy.withDefaults()
if celsius >= policy.TemperatureCriticalCelsius {
return "critical"
}
if celsius >= policy.TemperatureWarningCelsius {
return "attention"
}
if (previous == "attention" || previous == "critical") && celsius >= policy.TemperatureRecoveryCelsius {
return previous
}
return "normal"
}
func TemperatureStatus(previous string, celsius float64, policy PerformancePolicy) string {
return temperatureStatus(strings.ToLower(previous), celsius, policy)
}
func normalizeSpin(raw *RawSpin) (*Spin, error) {
if raw == nil {
return nil, nil
}
if !raw.Supported {
return &Spin{State: "unsupported"}, nil
}
state := strings.ToLower(strings.TrimSpace(raw.State))
switch state {
case "spinning", "idle", "standby", "unknown":
return &Spin{State: state}, nil
default:
return &Spin{State: "unknown"}, nil
}
}