metrics(power): export UPS battery charge and load percent

This commit is contained in:
Brad Stein 2026-04-03 17:50:05 -03:00
parent 3dda295cd2
commit e016b5d096
5 changed files with 49 additions and 0 deletions

View File

@ -15,6 +15,8 @@ type Sample struct {
OnBattery bool
LowBattery bool
RuntimeSecond int
BatteryCharge float64
LoadPercent float64
ThresholdSec int
Trigger bool
BreachCount int
@ -100,6 +102,10 @@ func (e *Exporter) serveMetrics(w http.ResponseWriter, _ *http.Request) {
b.WriteString("# TYPE hecate_ups_low_battery gauge\n")
b.WriteString("# HELP hecate_ups_runtime_seconds Battery runtime remaining reported by UPS.\n")
b.WriteString("# TYPE hecate_ups_runtime_seconds gauge\n")
b.WriteString("# HELP hecate_ups_battery_charge_percent Battery charge percentage reported by UPS.\n")
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_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")
@ -123,6 +129,8 @@ func (e *Exporter) serveMetrics(w http.ResponseWriter, _ *http.Request) {
b.WriteString(fmt.Sprintf("hecate_ups_on_battery%s %d\n", labels, boolNum(s.OnBattery)))
b.WriteString(fmt.Sprintf("hecate_ups_low_battery%s %d\n", labels, boolNum(s.LowBattery)))
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_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

@ -16,6 +16,8 @@ func TestExporterEmitsCoreMetrics(t *testing.T) {
OnBattery: true,
LowBattery: false,
RuntimeSecond: 412,
BatteryCharge: 81.5,
LoadPercent: 27.4,
ThresholdSec: 354,
Trigger: true,
BreachCount: 2,
@ -34,6 +36,8 @@ func TestExporterEmitsCoreMetrics(t *testing.T) {
"hecate_shutdown_triggers_total 1",
"hecate_ups_on_battery{source=\"db-ups\",target=\"atlasups@localhost\"",
"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_threshold_seconds{source=\"db-ups\",target=\"atlasups@localhost\"",
}
for _, m := range mustContain {

View File

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

View File

@ -5,6 +5,7 @@ import (
"context"
"fmt"
"os/exec"
"regexp"
"strconv"
"strings"
)
@ -13,6 +14,8 @@ type Sample struct {
OnBattery bool
LowBattery bool
RuntimeSeconds int
BatteryCharge float64
LoadPercent float64
RawStatus string
}
@ -82,5 +85,29 @@ func parseNUT(raw string) (Sample, error) {
out.RuntimeSeconds = runtime
}
}
if chargeRaw := kv["battery.charge"]; chargeRaw != "" {
if charge, ok := parseNumber(chargeRaw); ok {
out.BatteryCharge = charge
}
}
if loadRaw := kv["ups.load"]; loadRaw != "" {
if load, ok := parseNumber(loadRaw); ok {
out.LoadPercent = load
}
}
return out, nil
}
var parseNumberCleaner = regexp.MustCompile(`[^0-9.+-]`)
func parseNumber(raw string) (float64, bool) {
cleaned := strings.TrimSpace(parseNumberCleaner.ReplaceAllString(raw, ""))
if cleaned == "" {
return 0, false
}
v, err := strconv.ParseFloat(cleaned, 64)
if err != nil {
return 0, false
}
return v, true
}

View File

@ -4,6 +4,8 @@ import "testing"
func TestParseNUT(t *testing.T) {
raw := `battery.runtime: 384
battery.charge: 72
ups.load: 19
ups.status: OB LB
`
s, err := parseNUT(raw)
@ -19,4 +21,10 @@ ups.status: OB LB
if s.RuntimeSeconds != 384 {
t.Fatalf("expected runtime 384, got %d", s.RuntimeSeconds)
}
if s.BatteryCharge != 72 {
t.Fatalf("expected battery charge 72, got %.2f", s.BatteryCharge)
}
if s.LoadPercent != 19 {
t.Fatalf("expected load 19, got %.2f", s.LoadPercent)
}
}