diff --git a/go.mod b/go.mod index 2cb2ce8..e546cb3 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 5148d14..e086706 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/upscollector.go b/upscollector.go index ef28d61..0a25a5a 100644 --- a/upscollector.go +++ b/upscollector.go @@ -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()) } diff --git a/upscollector_test.go b/upscollector_test.go index 4331fd3..31329a1 100644 --- a/upscollector_test.go +++ b/upscollector_test.go @@ -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 {