metrics(power): export UPS nominal watts for real draw calculations

This commit is contained in:
Brad Stein 2026-04-03 20:43:27 -03:00
parent e016b5d096
commit 138f816093
5 changed files with 18 additions and 1 deletions

View File

@ -17,6 +17,7 @@ type Sample struct {
RuntimeSecond int
BatteryCharge float64
LoadPercent float64
PowerNominalW float64
ThresholdSec int
Trigger bool
BreachCount int
@ -106,6 +107,8 @@ func (e *Exporter) serveMetrics(w http.ResponseWriter, _ *http.Request) {
b.WriteString("# TYPE hecate_ups_battery_charge_percent gauge\n")
b.WriteString("# HELP hecate_ups_load_percent UPS output load percentage.\n")
b.WriteString("# TYPE hecate_ups_load_percent gauge\n")
b.WriteString("# HELP hecate_ups_power_nominal_watts UPS nominal power rating in watts.\n")
b.WriteString("# TYPE hecate_ups_power_nominal_watts gauge\n")
b.WriteString("# HELP hecate_ups_threshold_seconds Red-line threshold for runtime-based shutdown.\n")
b.WriteString("# TYPE hecate_ups_threshold_seconds gauge\n")
b.WriteString("# HELP hecate_ups_trigger_active Whether this UPS source currently breaches shutdown trigger conditions.\n")
@ -131,6 +134,7 @@ func (e *Exporter) serveMetrics(w http.ResponseWriter, _ *http.Request) {
b.WriteString(fmt.Sprintf("hecate_ups_runtime_seconds%s %d\n", labels, s.RuntimeSecond))
b.WriteString(fmt.Sprintf("hecate_ups_battery_charge_percent%s %.2f\n", labels, s.BatteryCharge))
b.WriteString(fmt.Sprintf("hecate_ups_load_percent%s %.2f\n", labels, s.LoadPercent))
b.WriteString(fmt.Sprintf("hecate_ups_power_nominal_watts%s %.2f\n", labels, s.PowerNominalW))
b.WriteString(fmt.Sprintf("hecate_ups_threshold_seconds%s %d\n", labels, s.ThresholdSec))
b.WriteString(fmt.Sprintf("hecate_ups_trigger_active%s %d\n", labels, boolNum(s.Trigger)))
b.WriteString(fmt.Sprintf("hecate_ups_breach_count%s %d\n", labels, s.BreachCount))

View File

@ -18,6 +18,7 @@ func TestExporterEmitsCoreMetrics(t *testing.T) {
RuntimeSecond: 412,
BatteryCharge: 81.5,
LoadPercent: 27.4,
PowerNominalW: 510,
ThresholdSec: 354,
Trigger: true,
BreachCount: 2,
@ -38,6 +39,7 @@ func TestExporterEmitsCoreMetrics(t *testing.T) {
"hecate_ups_runtime_seconds{source=\"db-ups\",target=\"atlasups@localhost\"",
"hecate_ups_battery_charge_percent{source=\"db-ups\",target=\"atlasups@localhost\"",
"hecate_ups_load_percent{source=\"db-ups\",target=\"atlasups@localhost\"",
"hecate_ups_power_nominal_watts{source=\"db-ups\",target=\"atlasups@localhost\"",
"hecate_ups_threshold_seconds{source=\"db-ups\",target=\"atlasups@localhost\"",
}
for _, m := range mustContain {

View File

@ -131,6 +131,7 @@ func (d *Daemon) Run(ctx context.Context) error {
RuntimeSecond: sample.RuntimeSeconds,
BatteryCharge: sample.BatteryCharge,
LoadPercent: sample.LoadPercent,
PowerNominalW: sample.NominalPowerW,
ThresholdSec: threshold,
Trigger: trigger,
BreachCount: breachCount[target.Name],

View File

@ -16,6 +16,7 @@ type Sample struct {
RuntimeSeconds int
BatteryCharge float64
LoadPercent float64
NominalPowerW float64
RawStatus string
}
@ -95,6 +96,11 @@ func parseNUT(raw string) (Sample, error) {
out.LoadPercent = load
}
}
if nominalRaw := kv["ups.realpower.nominal"]; nominalRaw != "" {
if nominal, ok := parseNumber(nominalRaw); ok {
out.NominalPowerW = nominal
}
}
return out, nil
}

View File

@ -3,9 +3,10 @@ package ups
import "testing"
func TestParseNUT(t *testing.T) {
raw := `battery.runtime: 384
raw := `battery.runtime: 384
battery.charge: 72
ups.load: 19
ups.realpower.nominal: 510
ups.status: OB LB
`
s, err := parseNUT(raw)
@ -27,4 +28,7 @@ ups.status: OB LB
if s.LoadPercent != 19 {
t.Fatalf("expected load 19, got %.2f", s.LoadPercent)
}
if s.NominalPowerW != 510 {
t.Fatalf("expected nominal power 510, got %.2f", s.NominalPowerW)
}
}