apcupsd_exporter: bump apcupsd, rework metrics to be more idiomatic

Signed-off-by: Matt Layher <mdlayher@gmail.com>
This commit is contained in:
Matt Layher 2020-04-27 13:16:08 -04:00
parent 4fd6f2de88
commit 9c346341fd
No known key found for this signature in database
GPG Key ID: 77BFE531397EDE94
4 changed files with 152 additions and 206 deletions

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.14
require (
github.com/golang/protobuf v1.4.0 // indirect
github.com/mdlayher/apcupsd v0.0.0-20190314144147-eb3dd99a75fe
github.com/mdlayher/apcupsd v0.0.0-20200427150358-149abe0674c2
github.com/prometheus/client_golang v1.5.1
github.com/prometheus/procfs v0.0.11 // indirect
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect

4
go.sum
View File

@ -42,8 +42,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/apcupsd v0.0.0-20190314144147-eb3dd99a75fe h1:yMrL+YorbzaBpj/h3BbLMP+qeslPZYMbzcpHFBNy1Yk=
github.com/mdlayher/apcupsd v0.0.0-20190314144147-eb3dd99a75fe/go.mod h1:y3mw3VG+t0m20OMqpG8RQqw8cDXvShVb+L8Z8FEnebw=
github.com/mdlayher/apcupsd v0.0.0-20200427150358-149abe0674c2 h1:/6kamK5dQLpKSHNH47TqdeDLorFi1rpIeajupArlxxI=
github.com/mdlayher/apcupsd v0.0.0-20200427150358-149abe0674c2/go.mod h1:izgPry3rVq8ix9cave/cFB9ZtlrWMa0bUOEOO4Q85x0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=

View File

@ -18,6 +18,8 @@ type StatusSource interface {
// A UPSCollector is a Prometheus collector for metrics regarding an APC UPS.
type UPSCollector struct {
Info *prometheus.Desc
UPSLoadPercent *prometheus.Desc
BatteryChargePercent *prometheus.Desc
LineVolts *prometheus.Desc
@ -40,11 +42,16 @@ var _ prometheus.Collector = &UPSCollector{}
// NewUPSCollector creates a new UPSCollector.
func NewUPSCollector(ss StatusSource) *UPSCollector {
var (
labels = []string{"hostname", "ups_name", "model"}
)
labels := []string{"ups"}
return &UPSCollector{
Info: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "info"),
"Metadata about a given UPS.",
[]string{"ups", "hostname", "model"},
nil,
),
UPSLoadPercent: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "", "ups_load_percent"),
"Current UPS load percentage.",
@ -147,141 +154,11 @@ func NewUPSCollector(ss StatusSource) *UPSCollector {
}
}
// collect begins a metrics collection task for all metrics related to an APC
// UPS.
func (c *UPSCollector) collect(ch chan<- prometheus.Metric) (*prometheus.Desc, error) {
s, err := c.ss.Status()
if err != nil {
return c.BatteryVolts, err
}
labels := []string{
s.Hostname,
s.UPSName,
s.Model,
}
ch <- prometheus.MustNewConstMetric(
c.UPSLoadPercent,
prometheus.GaugeValue,
s.LoadPercent,
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryChargePercent,
prometheus.GaugeValue,
s.BatteryChargePercent,
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.LineVolts,
prometheus.GaugeValue,
s.LineVoltage,
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.LineNominalVolts,
prometheus.GaugeValue,
s.NominalInputVoltage,
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryVolts,
prometheus.GaugeValue,
s.BatteryVoltage,
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryNominalVolts,
prometheus.GaugeValue,
s.NominalBatteryVoltage,
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryNumberTransfersTotal,
prometheus.CounterValue,
float64(s.NumberTransfers),
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryTimeLeftSeconds,
prometheus.GaugeValue,
s.TimeLeft.Seconds(),
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryTimeOnSeconds,
prometheus.GaugeValue,
s.TimeOnBattery.Seconds(),
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryCumulativeTimeOnSecondsTotal,
prometheus.CounterValue,
s.CumulativeTimeOnBattery.Seconds(),
labels...,
)
collectTimestamp(
ch,
c.LastTransferOnBattery,
s.XOnBattery,
labels...,
)
collectTimestamp(
ch,
c.LastTransferOffBattery,
s.XOffBattery,
labels...,
)
collectTimestamp(
ch,
c.LastSelftest,
s.LastSelftest,
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.NominalPowerWatts,
prometheus.GaugeValue,
float64(s.NominalPower),
labels...,
)
return nil, nil
}
// collectTimestamp collects timestamp metrics.
// Timestamps that are zero (time.IsZero() == true) are ignored, as such a timestamp indicates
// 'information not available', which is best expressed in Prometheus by not having the metric at all.
func collectTimestamp(ch chan<- prometheus.Metric, desc *prometheus.Desc, time time.Time, labelValues ...string) {
if time.IsZero() {
return
}
ch <- prometheus.MustNewConstMetric(
desc,
prometheus.GaugeValue,
float64(time.Unix()),
labelValues...,
)
}
// Describe sends the descriptors of each metric over to the provided channel.
// The corresponding metric values are sent separately.
func (c *UPSCollector) Describe(ch chan<- *prometheus.Desc) {
ds := []*prometheus.Desc{
c.Info,
c.UPSLoadPercent,
c.BatteryChargePercent,
c.LineVolts,
@ -302,9 +179,123 @@ func (c *UPSCollector) Describe(ch chan<- *prometheus.Desc) {
// Collect sends the metric values for each metric created by the UPSCollector
// to the provided prometheus Metric channel.
func (c *UPSCollector) Collect(ch chan<- prometheus.Metric) {
if desc, err := c.collect(ch); err != nil {
log.Printf("[ERROR] failed collecting UPS metric %v: %v", desc, err)
ch <- prometheus.NewInvalidMetric(desc, err)
s, err := c.ss.Status()
if err != nil {
log.Printf("failed collecting UPS metrics: %v", err)
ch <- prometheus.NewInvalidMetric(c.Info, err)
return
}
ch <- prometheus.MustNewConstMetric(
c.Info,
prometheus.GaugeValue,
1,
s.UPSName, s.Hostname, s.Model,
)
ch <- prometheus.MustNewConstMetric(
c.UPSLoadPercent,
prometheus.GaugeValue,
s.LoadPercent,
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryChargePercent,
prometheus.GaugeValue,
s.BatteryChargePercent,
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.LineVolts,
prometheus.GaugeValue,
s.LineVoltage,
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.LineNominalVolts,
prometheus.GaugeValue,
s.NominalInputVoltage,
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryVolts,
prometheus.GaugeValue,
s.BatteryVoltage,
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryNominalVolts,
prometheus.GaugeValue,
s.NominalBatteryVoltage,
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryNumberTransfersTotal,
prometheus.CounterValue,
float64(s.NumberTransfers),
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryTimeLeftSeconds,
prometheus.GaugeValue,
s.TimeLeft.Seconds(),
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryTimeOnSeconds,
prometheus.GaugeValue,
s.TimeOnBattery.Seconds(),
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.BatteryCumulativeTimeOnSecondsTotal,
prometheus.CounterValue,
s.CumulativeTimeOnBattery.Seconds(),
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.LastTransferOnBattery,
prometheus.GaugeValue,
timestamp(s.XOnBattery),
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.LastTransferOffBattery,
prometheus.GaugeValue,
timestamp(s.XOffBattery),
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.LastSelftest,
prometheus.GaugeValue,
timestamp(s.LastSelftest),
s.UPSName,
)
ch <- prometheus.MustNewConstMetric(
c.NominalPowerWatts,
prometheus.GaugeValue,
float64(s.NominalPower),
s.UPSName,
)
}
func timestamp(t time.Time) float64 {
if t.IsZero() {
return 0
}
return float64(t.Unix())
}

View File

@ -2,7 +2,6 @@ package apcupsdexporter
import (
"regexp"
"strings"
"testing"
"time"
@ -20,17 +19,14 @@ func TestUPSCollector(t *testing.T) {
ss: &testStatusSource{
s: &apcupsd.Status{},
},
matches: []*regexp.Regexp{
regexp.MustCompile(`apcupsd_battery_charge_percent{hostname="",model="",ups_name=""} 0`),
},
},
{
desc: "full",
ss: &testStatusSource{
s: &apcupsd.Status{
Hostname: "a",
Model: "b",
UPSName: "c",
Hostname: "foo",
Model: "APC UPS",
UPSName: "bar",
BatteryChargePercent: 100.0,
CumulativeTimeOnBattery: 30 * time.Second,
@ -49,20 +45,21 @@ func TestUPSCollector(t *testing.T) {
},
},
matches: []*regexp.Regexp{
regexp.MustCompile(`apcupsd_battery_charge_percent{hostname="a",model="b",ups_name="c"} 100`),
regexp.MustCompile(`apcupsd_battery_cumulative_time_on_seconds_total{hostname="a",model="b",ups_name="c"} 30`),
regexp.MustCompile(`apcupsd_battery_nominal_volts{hostname="a",model="b",ups_name="c"} 12`),
regexp.MustCompile(`apcupsd_battery_time_left_seconds{hostname="a",model="b",ups_name="c"} 120`),
regexp.MustCompile(`apcupsd_battery_time_on_seconds{hostname="a",model="b",ups_name="c"} 10`),
regexp.MustCompile(`apcupsd_battery_volts{hostname="a",model="b",ups_name="c"} 13.2`),
regexp.MustCompile(`apcupsd_battery_number_transfers_total{hostname="a",model="b",ups_name="c"} 1`),
regexp.MustCompile(`apcupsd_line_nominal_volts{hostname="a",model="b",ups_name="c"} 120`),
regexp.MustCompile(`apcupsd_line_volts{hostname="a",model="b",ups_name="c"} 121.1`),
regexp.MustCompile(`apcupsd_ups_load_percent{hostname="a",model="b",ups_name="c"} 16`),
regexp.MustCompile(`apcupsd_last_transfer_on_battery{hostname="a",model="b",ups_name="c"} 100001`),
regexp.MustCompile(`apcupsd_last_transfer_off_battery{hostname="a",model="b",ups_name="c"} 100002`),
regexp.MustCompile(`apcupsd_last_selftest{hostname="a",model="b",ups_name="c"} 100003`),
regexp.MustCompile(`apcupsd_nominal_power_watts{hostname="a",model="b",ups_name="c"} 50`),
regexp.MustCompile(`apcupsd_battery_charge_percent{ups="bar"} 100`),
regexp.MustCompile(`apcupsd_battery_cumulative_time_on_seconds_total{ups="bar"} 30`),
regexp.MustCompile(`apcupsd_battery_nominal_volts{ups="bar"} 12`),
regexp.MustCompile(`apcupsd_battery_time_left_seconds{ups="bar"} 120`),
regexp.MustCompile(`apcupsd_battery_time_on_seconds{ups="bar"} 10`),
regexp.MustCompile(`apcupsd_battery_volts{ups="bar"} 13.2`),
regexp.MustCompile(`apcupsd_battery_number_transfers_total{ups="bar"} 1`),
regexp.MustCompile(`apcupsd_info{hostname="foo",model="APC UPS",ups="bar"} 1`),
regexp.MustCompile(`apcupsd_line_nominal_volts{ups="bar"} 120`),
regexp.MustCompile(`apcupsd_line_volts{ups="bar"} 121.1`),
regexp.MustCompile(`apcupsd_ups_load_percent{ups="bar"} 16`),
regexp.MustCompile(`apcupsd_last_transfer_on_battery{ups="bar"} 100001`),
regexp.MustCompile(`apcupsd_last_transfer_off_battery{ups="bar"} 100002`),
regexp.MustCompile(`apcupsd_last_selftest{ups="bar"} 100003`),
regexp.MustCompile(`apcupsd_nominal_power_watts{ups="bar"} 50`),
},
},
}
@ -72,56 +69,14 @@ func TestUPSCollector(t *testing.T) {
out := testCollector(t, NewUPSCollector(tt.ss))
for _, m := range tt.matches {
name := metricName(t, m.String())
t.Run(name, func(t *testing.T) {
if !m.Match(out) {
t.Fatalf("\toutput failed to match regex (regexp: %v)", m)
}
})
if !m.Match(out) {
t.Fatalf("output failed to match regex (regexp: %v)", m)
}
}
})
}
}
// TestZeroTimesAreIgnored tests that times with a zero value (time.IsZero() == true)
// are not collected.
func TestZeroTimesAreIgnored(t *testing.T) {
ss := &testStatusSource{
s: &apcupsd.Status{
XOnBattery: time.Unix(123456, 0),
XOffBattery: time.Time{},
},
}
out := testCollector(t, NewUPSCollector(ss))
// Test that in general timestamps are collected.
if !regexp.MustCompile(`apcupsd_last_transfer_on_battery.* 123456`).Match(out) {
t.Error("non-zero timestamp is not reported properly")
}
// Test that zero timestamps, however, are ignored.
if regexp.MustCompile(`apcupsd_last_transfer_off_battery`).Match(out) {
t.Error("Zero time is reported")
}
}
func metricName(t *testing.T, metric string) string {
ss := strings.Split(metric, " ")
if len(ss) != 2 {
t.Fatalf("malformed metric: %v", metric)
}
if !strings.Contains(ss[0], "{") {
return ss[0]
}
ss = strings.Split(ss[0], "{")
if len(ss) != 2 {
t.Fatalf("malformed metric: %v", metric)
}
return ss[0]
}
var _ StatusSource = &testStatusSource{}
type testStatusSource struct {