Skip to content

Commit 94a194f

Browse files
committed
Add timer next trigger
This metric shows next time a timer will fire. Care has been taken to handle real-time and monotonic timers correctly. Real-time timers return their value directly as a Unix timestamp as microseconds. The only special case is when the corresponding service is running, the timestamp is the max unit64 value. We filter those values out. Monotonic timers return the next time a service will run as the number of microseconds since the system boot time. Some computation is needed to get the next time as a standard Unix timestamp.
1 parent 8b490c0 commit 94a194f

File tree

1 file changed

+71
-0
lines changed

1 file changed

+71
-0
lines changed

systemd/systemd.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ package systemd
1616
import (
1717
"context"
1818
"fmt"
19+
"io"
1920
"log/slog"
2021
"math"
22+
"os"
2123
"strconv"
2224

2325
// Register pprof-over-http handlers
@@ -53,6 +55,8 @@ var (
5355
errConvertStringPropertyMsg = "couldn't convert unit's %s property %v to string"
5456
errUnitMetricsMsg = "couldn't get unit's metrics: %s"
5557
infoUnitNoHandler = "no unit type handler for %s"
58+
59+
bootTime = getBootTime()
5660
)
5761

5862
type Collector struct {
@@ -72,6 +76,7 @@ type Collector struct {
7276
unitInactiveExitTimeDesc *prometheus.Desc
7377
nRestartsDesc *prometheus.Desc
7478
timerLastTriggerDesc *prometheus.Desc
79+
timerNextTriggerDesc *prometheus.Desc
7580
socketAcceptedConnectionsDesc *prometheus.Desc
7681
socketCurrentConnectionsDesc *prometheus.Desc
7782
socketRefusedConnectionsDesc *prometheus.Desc
@@ -88,6 +93,28 @@ type Collector struct {
8893
unitExcludePattern *regexp.Regexp
8994
}
9095

96+
func getBootTime() uint64 {
97+
f, err := os.Open("/proc/uptime")
98+
if err != nil {
99+
panic(fmt.Sprintf("could not open file /proc/uptime: %s", err))
100+
}
101+
defer f.Close()
102+
data, err := io.ReadAll(f)
103+
if err != nil {
104+
panic(fmt.Sprintf("could not read file /proc/uptime: %s", err))
105+
}
106+
107+
fields := strings.Fields(string(data))
108+
uptimeSeconds, err := strconv.ParseFloat(fields[0], 64)
109+
if err != nil {
110+
panic(fmt.Sprintf("could not parse file /proc/uptime: %s", err))
111+
}
112+
113+
now := time.Now()
114+
bootTime := now.Add(-time.Duration(uptimeSeconds) * time.Second)
115+
return uint64(bootTime.Unix())
116+
}
117+
91118
// NewCollector returns a new Collector exposing systemd statistics.
92119
func NewCollector(logger *slog.Logger) (*Collector, error) {
93120
systemdBootMonotonic := prometheus.NewDesc(
@@ -161,6 +188,9 @@ func NewCollector(logger *slog.Logger) (*Collector, error) {
161188
timerLastTriggerDesc := prometheus.NewDesc(
162189
prometheus.BuildFQName(namespace, "", "timer_last_trigger_seconds"),
163190
"Seconds since epoch of last trigger.", []string{"name"}, nil)
191+
timerNextTriggerDesc := prometheus.NewDesc(
192+
prometheus.BuildFQName(namespace, "", "timer_next_trigger_seconds"),
193+
"Seconds since epoch of next trigger.", []string{"name"}, nil)
164194
socketAcceptedConnectionsDesc := prometheus.NewDesc(
165195
prometheus.BuildFQName(namespace, "", "socket_accepted_connections_total"),
166196
"Total number of accepted socket connections", []string{"name"}, nil)
@@ -242,6 +272,7 @@ func NewCollector(logger *slog.Logger) (*Collector, error) {
242272
unitInactiveExitTimeDesc: unitInactiveExitTimeDesc,
243273
nRestartsDesc: nRestartsDesc,
244274
timerLastTriggerDesc: timerLastTriggerDesc,
275+
timerNextTriggerDesc: timerNextTriggerDesc,
245276
socketAcceptedConnectionsDesc: socketAcceptedConnectionsDesc,
246277
socketCurrentConnectionsDesc: socketCurrentConnectionsDesc,
247278
socketRefusedConnectionsDesc: socketRefusedConnectionsDesc,
@@ -278,6 +309,7 @@ func (c *Collector) Describe(desc chan<- *prometheus.Desc) {
278309
desc <- c.unitTasksMaxDesc
279310
desc <- c.nRestartsDesc
280311
desc <- c.timerLastTriggerDesc
312+
desc <- c.timerNextTriggerDesc
281313
desc <- c.socketAcceptedConnectionsDesc
282314
desc <- c.socketCurrentConnectionsDesc
283315
desc <- c.socketRefusedConnectionsDesc
@@ -690,6 +722,45 @@ func (c *Collector) collectTimerTriggerTime(conn *dbus.Conn, ch chan<- prometheu
690722
ch <- prometheus.MustNewConstMetric(
691723
c.timerLastTriggerDesc, prometheus.GaugeValue,
692724
float64(val)/1e6, unit.Name)
725+
726+
nextRealtimeValue, err := conn.GetUnitTypePropertyContext(c.ctx, unit.Name, "Timer", "NextElapseUSecRealtime")
727+
if err != nil {
728+
return fmt.Errorf(errGetPropertyMsg, "NextElapseUSecRealtime", err)
729+
}
730+
val, ok = nextRealtimeValue.Value.Value().(uint64)
731+
if !ok {
732+
return fmt.Errorf(errConvertUint64PropertyMsg, "NextElapseUSecRealtime", nextRealtimeValue.Value.Value())
733+
}
734+
if val != 0 {
735+
// This special value happens when the service is currently active.
736+
if val == math.MaxUint64 {
737+
return nil
738+
}
739+
ch <- prometheus.MustNewConstMetric(
740+
c.timerNextTriggerDesc, prometheus.GaugeValue,
741+
float64(val)/1e6, unit.Name)
742+
return nil
743+
}
744+
745+
nextMonotonicValue, err := conn.GetUnitTypePropertyContext(c.ctx, unit.Name, "Timer", "NextElapseUSecMonotonic")
746+
if err != nil {
747+
return fmt.Errorf(errGetPropertyMsg, "NextElapseUSecMonotonic", err)
748+
}
749+
val, ok = nextMonotonicValue.Value.Value().(uint64)
750+
if !ok {
751+
return fmt.Errorf(errConvertUint64PropertyMsg, "NextElapseUSecMonotonic", nextMonotonicValue.Value.Value())
752+
}
753+
if val != 0 {
754+
// Monotonic value is a number of microseconds until next activation.
755+
// It counts seconds from the boot time.
756+
// We transform it to an absolute date.
757+
val := float64(bootTime) + (float64(val) / 1e6)
758+
ch <- prometheus.MustNewConstMetric(
759+
c.timerNextTriggerDesc, prometheus.GaugeValue,
760+
val, unit.Name)
761+
return nil
762+
}
763+
693764
return nil
694765
}
695766

0 commit comments

Comments
 (0)