@@ -16,8 +16,10 @@ package systemd
1616import (
1717 "context"
1818 "fmt"
19+ "io"
1920 "log/slog"
2021 "math"
22+ "os"
2123 "strconv"
2224
2325 // Register pprof-over-http handlers
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
5862type 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.
92119func 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