Skip to main content

monitord/
timer.rs

1//! # timers module
2//!
3//! All timer related logic goes here. This will be hitting timer specific
4//! dbus / varlink etc.
5
6use struct_field_names_as_array::FieldNamesAsArray;
7use thiserror::Error;
8use tracing::error;
9
10#[derive(Error, Debug)]
11pub enum MonitordTimerError {
12    #[error("Timer D-Bus error: {0}")]
13    ZbusError(#[from] zbus::Error),
14}
15
16#[derive(
17    serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, FieldNamesAsArray, PartialEq,
18)]
19
20/// Per-timer unit metrics from the org.freedesktop.systemd1.Timer D-Bus interface.
21/// Ref: <https://www.freedesktop.org/software/systemd/man/org.freedesktop.systemd1.html>
22pub struct TimerStats {
23    /// AccuracySec timer property in microseconds; systemd may coalesce timer firings within this window to save wakeups
24    pub accuracy_usec: u64,
25    /// Whether FixedRandomDelay= is set; when true, the random delay is stable across reboots for this timer
26    pub fixed_random_delay: bool,
27    /// Realtime timestamp (usec since epoch) when this timer last triggered its service unit
28    pub last_trigger_usec: u64,
29    /// Monotonic timestamp (usec since boot) when this timer last triggered its service unit
30    pub last_trigger_usec_monotonic: u64,
31    /// Monotonic timestamp (usec since boot) when this timer will next elapse
32    pub next_elapse_usec_monotonic: u64,
33    /// Realtime timestamp (usec since epoch) when this timer will next elapse
34    pub next_elapse_usec_realtime: u64,
35    /// Whether Persistent= is set; when true, missed timer runs (e.g. during downtime) are triggered on next boot
36    pub persistent: bool,
37    /// RandomizedDelaySec property in microseconds; a random delay up to this value is added before each trigger
38    pub randomized_delay_usec: u64,
39    /// Whether RemainAfterElapse= is set; when true, the timer stays loaded after all triggers have elapsed
40    pub remain_after_elapse: bool,
41    /// Realtime timestamp (usec since epoch) of the most recent state change of the triggered service unit
42    pub service_unit_last_state_change_usec: u64,
43    /// Monotonic timestamp (usec since boot) of the most recent state change of the triggered service unit
44    pub service_unit_last_state_change_usec_monotonic: u64,
45}
46
47pub const TIMER_STATS_FIELD_NAMES: &[&str] = &TimerStats::FIELD_NAMES_AS_ARRAY;
48
49#[tracing::instrument(level = "debug", skip(connection))]
50pub async fn collect_timer_stats(
51    connection: &zbus::Connection,
52    unit: &crate::units::ListedUnit,
53) -> Result<TimerStats, MonitordTimerError> {
54    let mut timer_stats = TimerStats::default();
55
56    let pt = crate::dbus::zbus_timer::TimerProxy::builder(connection)
57        .cache_properties(zbus::proxy::CacheProperties::No)
58        .path(unit.unit_object_path.clone())?
59        .build()
60        .await?;
61    // Get service unit name to check when it last ran to ensure
62    // we are triggers the configured service with times set
63    let service_unit = pt.unit().await?;
64    let mut service_unit_last_state_change_usec: Result<u64, zbus::Error> = Ok(0);
65    let mut service_unit_last_state_change_usec_monotonic: Result<u64, zbus::Error> = Ok(0);
66    if service_unit.is_empty() {
67        error!("{}: No service unit name found for timer.", unit.name);
68    } else {
69        // Get the object path of the service unit
70        let mp = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
71            .cache_properties(zbus::proxy::CacheProperties::No)
72            .build()
73            .await?;
74        let service_unit_path = mp.get_unit(&service_unit).await?;
75        // Create a UnitProxy with the unit path to async get the two counters we want
76        let up = crate::dbus::zbus_unit::UnitProxy::builder(connection)
77            .cache_properties(zbus::proxy::CacheProperties::No)
78            .path(service_unit_path)?
79            .build()
80            .await?;
81
82        (
83            service_unit_last_state_change_usec,
84            service_unit_last_state_change_usec_monotonic,
85        ) = tokio::join!(
86            up.state_change_timestamp(),
87            up.state_change_timestamp_monotonic(),
88        );
89    }
90    timer_stats.service_unit_last_state_change_usec = service_unit_last_state_change_usec?;
91    timer_stats.service_unit_last_state_change_usec_monotonic =
92        service_unit_last_state_change_usec_monotonic?;
93
94    // Use tokio::join! without tokio::spawn to avoid per-task allocation overhead.
95    // These all share the same D-Bus connection so spawn adds no parallelism benefit.
96    let (
97        accuracy_usec,
98        fixed_random_delay,
99        last_trigger_usec,
100        last_trigger_usec_monotonic,
101        persistent,
102        next_elapse_usec_monotonic,
103        next_elapse_usec_realtime,
104        randomized_delay_usec,
105        remain_after_elapse,
106    ) = tokio::join!(
107        pt.accuracy_usec(),
108        pt.fixed_random_delay(),
109        pt.last_trigger_usec(),
110        pt.last_trigger_usec_monotonic(),
111        pt.persistent(),
112        pt.next_elapse_usec_monotonic(),
113        pt.next_elapse_usec_realtime(),
114        pt.randomized_delay_usec(),
115        pt.remain_after_elapse(),
116    );
117
118    timer_stats.accuracy_usec = accuracy_usec?;
119    timer_stats.fixed_random_delay = fixed_random_delay?;
120    timer_stats.last_trigger_usec = last_trigger_usec?;
121    timer_stats.last_trigger_usec_monotonic = last_trigger_usec_monotonic?;
122    timer_stats.persistent = persistent?;
123    timer_stats.next_elapse_usec_monotonic = next_elapse_usec_monotonic?;
124    timer_stats.next_elapse_usec_realtime = next_elapse_usec_realtime?;
125    timer_stats.randomized_delay_usec = randomized_delay_usec?;
126    timer_stats.remain_after_elapse = remain_after_elapse?;
127
128    Ok(timer_stats)
129}
130
131/// Collect all timer stats via D-Bus and return them ready to merge into unit stats.
132///
133/// Used when unit stats were collected via varlink (which doesn't yet expose timer
134/// properties) so that `timers.*`, `timer_persistent_units`, and
135/// `timer_remain_after_elapse` match the D-Bus output.
136pub async fn collect_all_timers_dbus(
137    connection: &zbus::Connection,
138    config: &crate::config::Config,
139) -> anyhow::Result<crate::units::SystemdUnitStats> {
140    use std::collections::HashMap;
141    use tracing::debug;
142
143    if !config.timers.enabled {
144        return Ok(crate::units::SystemdUnitStats::default());
145    }
146
147    let p = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
148        .cache_properties(zbus::proxy::CacheProperties::No)
149        .build()
150        .await?;
151    let units = p.list_units().await?;
152
153    let mut stats = crate::units::SystemdUnitStats::default();
154    let mut timer_stats_map = HashMap::new();
155
156    for unit_raw in units {
157        let unit: crate::units::ListedUnit = unit_raw.into();
158        if !unit.name.contains(".timer") {
159            continue;
160        }
161        if config.timers.blocklist.contains(&unit.name) {
162            debug!("Skipping timer stats for {} due to blocklist", &unit.name);
163            continue;
164        }
165        if !config.timers.allowlist.is_empty() && !config.timers.allowlist.contains(&unit.name) {
166            continue;
167        }
168        match collect_timer_stats(connection, &unit).await {
169            Ok(ts) => {
170                if ts.persistent {
171                    stats.timer_persistent_units += 1;
172                }
173                if ts.remain_after_elapse {
174                    stats.timer_remain_after_elapse += 1;
175                }
176                timer_stats_map.insert(unit.name.clone(), ts);
177            }
178            Err(err) => {
179                error!("Failed to get {} stats: {:#?}", &unit.name, err);
180            }
181        }
182    }
183
184    stats.timer_stats = timer_stats_map;
185    Ok(stats)
186}