diff --git a/collector/systemd_linux.go b/collector/systemd_linux.go index cbb951d1..f90b49a5 100644 --- a/collector/systemd_linux.go +++ b/collector/systemd_linux.go @@ -18,6 +18,7 @@ package collector import ( "fmt" "regexp" + "strings" "github.com/coreos/go-systemd/dbus" "github.com/prometheus/client_golang/prometheus" @@ -35,6 +36,7 @@ type systemdCollector struct { unitDesc *prometheus.Desc systemRunningDesc *prometheus.Desc summaryDesc *prometheus.Desc + timerLastTriggerDesc *prometheus.Desc unitWhitelistPattern *regexp.Regexp unitBlacklistPattern *regexp.Regexp } @@ -61,6 +63,9 @@ func NewSystemdCollector() (Collector, error) { summaryDesc := prometheus.NewDesc( prometheus.BuildFQName(namespace, subsystem, "units"), "Summary of systemd unit states", []string{"state"}, nil) + timerLastTriggerDesc := prometheus.NewDesc( + prometheus.BuildFQName(namespace, subsystem, "timer_last_trigger_seconds"), + "Seconds since epoch of last trigger.", []string{"name"}, nil) unitWhitelistPattern := regexp.MustCompile(fmt.Sprintf("^(?:%s)$", *unitWhitelist)) unitBlacklistPattern := regexp.MustCompile(fmt.Sprintf("^(?:%s)$", *unitBlacklist)) @@ -68,6 +73,7 @@ func NewSystemdCollector() (Collector, error) { unitDesc: unitDesc, systemRunningDesc: systemRunningDesc, summaryDesc: summaryDesc, + timerLastTriggerDesc: timerLastTriggerDesc, unitWhitelistPattern: unitWhitelistPattern, unitBlacklistPattern: unitBlacklistPattern, }, nil @@ -84,6 +90,7 @@ func (c *systemdCollector) Update(ch chan<- prometheus.Metric) error { units := filterUnits(allUnits, c.unitWhitelistPattern, c.unitBlacklistPattern) c.collectUnitStatusMetrics(ch, units) + c.collectTimers(ch, units) systemState, err := c.getSystemState() if err != nil { @@ -94,7 +101,7 @@ func (c *systemdCollector) Update(ch chan<- prometheus.Metric) error { return nil } -func (c *systemdCollector) collectUnitStatusMetrics(ch chan<- prometheus.Metric, units []dbus.UnitStatus) { +func (c *systemdCollector) collectUnitStatusMetrics(ch chan<- prometheus.Metric, units []unit) { for _, unit := range units { for _, stateName := range unitStatesName { isActive := 0.0 @@ -108,6 +115,19 @@ func (c *systemdCollector) collectUnitStatusMetrics(ch chan<- prometheus.Metric, } } +func (c *systemdCollector) collectTimers(ch chan<- prometheus.Metric, units []unit) error { + for _, unit := range units { + if !strings.HasSuffix(unit.Name, ".timer") { + continue + } + + ch <- prometheus.MustNewConstMetric( + c.timerLastTriggerDesc, prometheus.GaugeValue, + float64(unit.lastTriggerUsec)/1e6, unit.Name) + } + return nil +} + func (c *systemdCollector) collectSummaryMetrics(ch chan<- prometheus.Metric, summary map[string]float64) { for stateName, count := range summary { ch <- prometheus.MustNewConstMetric( @@ -130,22 +150,45 @@ func (c *systemdCollector) newDbus() (*dbus.Conn, error) { return dbus.New() } -func (c *systemdCollector) getAllUnits() ([]dbus.UnitStatus, error) { +type unit struct { + dbus.UnitStatus + lastTriggerUsec uint64 +} + +func (c *systemdCollector) getAllUnits() ([]unit, error) { conn, err := c.newDbus() if err != nil { return nil, fmt.Errorf("couldn't get dbus connection: %s", err) } - allUnits, err := conn.ListUnits() - conn.Close() + defer conn.Close() + allUnits, err := conn.ListUnits() if err != nil { - return []dbus.UnitStatus{}, err + return nil, err + } + + result := make([]unit, 0, len(allUnits)) + for _, status := range allUnits { + unit := unit{ + UnitStatus: status, + } + + if strings.HasSuffix(unit.Name, ".timer") { + lastTriggerValue, err := conn.GetUnitTypeProperty(unit.Name, "Timer", "LastTriggerUSec") + if err != nil { + return nil, fmt.Errorf("couldn't get unit '%s' LastTriggerUSec: %s", unit.Name, err) + } + + unit.lastTriggerUsec = lastTriggerValue.Value.Value().(uint64) + } + + result = append(result, unit) } - return allUnits, nil + return result, nil } -func summarizeUnits(units []dbus.UnitStatus) map[string]float64 { +func summarizeUnits(units []unit) map[string]float64 { summarized := make(map[string]float64) for _, unitStateName := range unitStatesName { @@ -159,8 +202,8 @@ func summarizeUnits(units []dbus.UnitStatus) map[string]float64 { return summarized } -func filterUnits(units []dbus.UnitStatus, whitelistPattern, blacklistPattern *regexp.Regexp) []dbus.UnitStatus { - filtered := make([]dbus.UnitStatus, 0, len(units)) +func filterUnits(units []unit, whitelistPattern, blacklistPattern *regexp.Regexp) []unit { + filtered := make([]unit, 0, len(units)) for _, unit := range units { if whitelistPattern.MatchString(unit.Name) && !blacklistPattern.MatchString(unit.Name) { filtered = append(filtered, unit) diff --git a/collector/systemd_linux_test.go b/collector/systemd_linux_test.go index 0555d465..a68f6cdc 100644 --- a/collector/systemd_linux_test.go +++ b/collector/systemd_linux_test.go @@ -22,61 +22,69 @@ import ( ) // Creates mock UnitLists -func getUnitListFixtures() [][]dbus.UnitStatus { - fixture1 := []dbus.UnitStatus{ +func getUnitListFixtures() [][]unit { + fixture1 := []unit{ { - Name: "foo", - Description: "foo desc", - LoadState: "loaded", - ActiveState: "active", - SubState: "running", - Followed: "", - Path: "/org/freedesktop/systemd1/unit/foo", - JobId: 0, - JobType: "", - JobPath: "/", + UnitStatus: dbus.UnitStatus{ + Name: "foo", + Description: "foo desc", + LoadState: "loaded", + ActiveState: "active", + SubState: "running", + Followed: "", + Path: "/org/freedesktop/systemd1/unit/foo", + JobId: 0, + JobType: "", + JobPath: "/", + }, }, { - Name: "bar", - Description: "bar desc", - LoadState: "not-found", - ActiveState: "inactive", - SubState: "dead", - Followed: "", - Path: "/org/freedesktop/systemd1/unit/bar", - JobId: 0, - JobType: "", - JobPath: "/", + UnitStatus: dbus.UnitStatus{ + Name: "bar", + Description: "bar desc", + LoadState: "not-found", + ActiveState: "inactive", + SubState: "dead", + Followed: "", + Path: "/org/freedesktop/systemd1/unit/bar", + JobId: 0, + JobType: "", + JobPath: "/", + }, }, { - Name: "foobar", - Description: "bar desc", - LoadState: "not-found", - ActiveState: "inactive", - SubState: "dead", - Followed: "", - Path: "/org/freedesktop/systemd1/unit/bar", - JobId: 0, - JobType: "", - JobPath: "/", + UnitStatus: dbus.UnitStatus{ + Name: "foobar", + Description: "bar desc", + LoadState: "not-found", + ActiveState: "inactive", + SubState: "dead", + Followed: "", + Path: "/org/freedesktop/systemd1/unit/bar", + JobId: 0, + JobType: "", + JobPath: "/", + }, }, { - Name: "baz", - Description: "bar desc", - LoadState: "not-found", - ActiveState: "inactive", - SubState: "dead", - Followed: "", - Path: "/org/freedesktop/systemd1/unit/bar", - JobId: 0, - JobType: "", - JobPath: "/", + UnitStatus: dbus.UnitStatus{ + Name: "baz", + Description: "bar desc", + LoadState: "not-found", + ActiveState: "inactive", + SubState: "dead", + Followed: "", + Path: "/org/freedesktop/systemd1/unit/bar", + JobId: 0, + JobType: "", + JobPath: "/", + }, }, } - fixture2 := []dbus.UnitStatus{} + fixture2 := []unit{} - return [][]dbus.UnitStatus{fixture1, fixture2} + return [][]unit{fixture1, fixture2} } func TestSystemdCollectorDoesntCrash(t *testing.T) {