Skip to main content

monitord/
lib.rs

1//! # monitord Crate
2//!
3//! `monitord` is a library to gather statistics about systemd.
4
5use std::sync::Arc;
6
7use std::collections::HashMap;
8use std::time::Duration;
9use std::time::Instant;
10
11use thiserror::Error;
12use tokio::sync::RwLock;
13use tracing::debug;
14use tracing::error;
15use tracing::info;
16use tracing::warn;
17use tracing::Instrument;
18
19#[derive(Error, Debug)]
20pub enum MonitordError {
21    #[error("D-Bus connection error: {0}")]
22    ZbusError(#[from] zbus::Error),
23}
24
25pub mod boot;
26pub mod config;
27pub(crate) mod dbus;
28pub mod dbus_stats;
29pub mod json;
30pub mod logging;
31pub mod machines;
32pub mod networkd;
33pub mod pid1;
34pub mod system;
35pub mod timer;
36pub mod unit_constants;
37pub mod units;
38pub mod varlink;
39pub mod varlink_networkd;
40pub mod varlink_units;
41pub mod verify;
42
43pub const DEFAULT_DBUS_ADDRESS: &str = "unix:path=/run/dbus/system_bus_socket";
44
45/// Per-collector timing for a single stat collection run.
46///
47/// `start_offset_ms` is the wall time between the top of the collection cycle and
48/// the moment this collector's future was first polled. A non-trivial offset
49/// indicates the spawn/scheduling loop or the runtime is delaying first poll,
50/// which means collectors are not starting in parallel as intended.
51///
52/// `elapsed_ms` is the wall time between first poll and completion.
53#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
54pub struct CollectorTiming {
55    /// Name of the collector (e.g. "units", "pid1", "dbus_stats")
56    pub name: String,
57    /// Milliseconds from top of the run until the spawned future's first poll.
58    /// Should be small (< a few ms) when collectors are truly running in parallel.
59    pub start_offset_ms: f64,
60    /// Milliseconds from first poll to future completion.
61    pub elapsed_ms: f64,
62    /// Whether the collector returned Ok.
63    pub success: bool,
64}
65
66/// Stats collected for a single systemd-nspawn container or VM managed by systemd-machined
67#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
68pub struct MachineStats {
69    /// systemd-networkd interface states inside the container
70    pub networkd: networkd::NetworkdState,
71    /// PID 1 process stats from procfs (using the container's leader PID)
72    pub pid1: Option<pid1::Pid1Stats>,
73    /// Overall systemd system state (e.g. running, degraded) inside the container
74    pub system_state: system::SystemdSystemState,
75    /// Aggregated systemd unit counts and per-service/timer stats inside the container
76    pub units: units::SystemdUnitStats,
77    /// systemd version running inside the container
78    pub version: system::SystemdVersion,
79    /// D-Bus daemon/broker statistics inside the container
80    pub dbus_stats: Option<dbus_stats::DBusStats>,
81    /// Boot blame statistics: slowest units at boot with activation times in seconds
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub boot_blame: Option<boot::BootBlameStats>,
84    /// Unit verification error statistics
85    pub verify_stats: Option<verify::VerifyStats>,
86}
87
88/// Root struct containing all enabled monitord metrics for the host system and containers
89#[derive(serde::Serialize, serde::Deserialize, Debug, Default, PartialEq)]
90pub struct MonitordStats {
91    /// systemd-networkd interface states and managed interface count
92    pub networkd: networkd::NetworkdState,
93    /// PID 1 (systemd) process stats from procfs: CPU, memory, FDs, tasks
94    pub pid1: Option<pid1::Pid1Stats>,
95    /// Overall systemd manager state (e.g. running, degraded, initializing)
96    pub system_state: system::SystemdSystemState,
97    /// Aggregated systemd unit counts by type/state and per-service/timer detailed metrics
98    pub units: units::SystemdUnitStats,
99    /// Installed systemd version (major.minor.revision.os)
100    pub version: system::SystemdVersion,
101    /// D-Bus daemon/broker statistics (connections, bus names, match rules, per-peer accounting)
102    pub dbus_stats: Option<dbus_stats::DBusStats>,
103    /// Per-container stats keyed by machine name, collected via systemd-machined
104    pub machines: HashMap<String, MachineStats>,
105    /// Boot blame statistics: slowest units at boot with activation times in seconds
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub boot_blame: Option<boot::BootBlameStats>,
108    /// Unit verification error statistics
109    pub verify_stats: Option<verify::VerifyStats>,
110    /// End-to-end duration of the last stat collection run in milliseconds.
111    pub stat_collection_run_time_ms: f64,
112    /// Per-collector timings from the last run, sorted slowest first. Empty
113    /// before the first run completes. Callers compute parallelism ratio
114    /// (sum of `elapsed_ms` / `stat_collection_run_time_ms`) and identify the
115    /// gating collector (first entry) directly from this vector.
116    pub collector_timings: Vec<CollectorTiming>,
117}
118
119/// Print statistics in the format set in configuration
120pub fn print_stats(
121    key_prefix: &str,
122    output_format: &config::MonitordOutputFormat,
123    stats: &MonitordStats,
124) {
125    match output_format {
126        config::MonitordOutputFormat::Json => println!(
127            "{}",
128            serde_json::to_string(&stats).expect("Invalid JSON serialization")
129        ),
130        config::MonitordOutputFormat::JsonFlat => println!(
131            "{}",
132            json::flatten(stats, key_prefix).expect("Invalid JSON serialization")
133        ),
134        config::MonitordOutputFormat::JsonPretty => println!(
135            "{}",
136            serde_json::to_string_pretty(&stats).expect("Invalid JSON serialization")
137        ),
138    }
139}
140
141fn set_stat_collection_run_time(stats: &mut MonitordStats, elapsed_runtime: Duration) {
142    stats.stat_collection_run_time_ms = elapsed_runtime.as_secs_f64() * 1000.0;
143}
144
145/// Output produced by every spawned collector future after wrapping with timing.
146type TimedCollectorOutput = (String, anyhow::Result<()>, Duration, Duration);
147
148/// Spawn a collector future onto the join set with timing instrumentation.
149///
150/// The wrapping closure records the moment the future is first polled (relative
151/// to `collect_start`) and the elapsed wall time until it completes. Both
152/// durations and the collector name are returned alongside the original result.
153///
154/// `tokio::task::JoinSet::spawn` runs the future on a new task, and tracing
155/// spans do not cross task boundaries automatically — without explicitly
156/// capturing and re-attaching the caller's span here, every collector (and
157/// anything it spawns in turn, e.g. units.rs's per-unit tasks) would show up
158/// as an unrelated root trace instead of a child of the current collection run.
159fn spawn_timed<F>(
160    join_set: &mut tokio::task::JoinSet<TimedCollectorOutput>,
161    name: &'static str,
162    collect_start: Instant,
163    fut: F,
164) where
165    F: std::future::Future<Output = anyhow::Result<()>> + Send + 'static,
166{
167    let parent_span = tracing::Span::current();
168    let span = tracing::debug_span!(
169        parent: &parent_span,
170        "collector",
171        name,
172        elapsed_ms = tracing::field::Empty,
173        success = tracing::field::Empty,
174    );
175    let recording_span = span.clone();
176    join_set.spawn(
177        async move {
178            let task_first_poll = Instant::now();
179            let start_offset = task_first_poll.duration_since(collect_start);
180            let result = fut.await;
181            let elapsed = task_first_poll.elapsed();
182            recording_span.record("elapsed_ms", elapsed.as_secs_f64() * 1000.0);
183            recording_span.record("success", result.is_ok());
184            (name.to_string(), result, start_offset, elapsed)
185        }
186        .instrument(span),
187    );
188}
189
190/// Reuse an existing D-Bus connection or create a new system bus connection.
191async fn get_or_create_dbus_connection(
192    config: &config::Config,
193    maybe_connection: Option<zbus::Connection>,
194) -> Result<zbus::Connection, MonitordError> {
195    match maybe_connection {
196        Some(conn) => Ok(conn),
197        None => Ok(zbus::connection::Builder::system()?
198            .method_timeout(std::time::Duration::from_secs(config.monitord.dbus_timeout))
199            .build()
200            .await?),
201    }
202}
203
204/// Main statistic collection function running what's required by configuration in parallel
205/// Takes an optional locked stats struct to update and to output stats to STDOUT or not.
206/// Takes an optional D-Bus connection. Returns `Some(connection)` if the
207/// collection cycle completed without errors (meaning the connection is reusable),
208/// `None` if errors occurred.
209pub async fn stat_collector(
210    config: config::Config,
211    maybe_locked_stats: Option<Arc<RwLock<MonitordStats>>>,
212    output_stats: bool,
213    maybe_connection: Option<zbus::Connection>,
214) -> Result<Option<zbus::Connection>, MonitordError> {
215    let mut collect_interval_ms: u128 = 0;
216    if config.monitord.daemon {
217        collect_interval_ms = (config.monitord.daemon_stats_refresh_secs * 1000).into();
218    }
219
220    let config = Arc::new(config);
221    let locked_monitord_stats: Arc<RwLock<MonitordStats>> =
222        maybe_locked_stats.unwrap_or(Arc::new(RwLock::new(MonitordStats::default())));
223    let locked_machine_stats: Arc<RwLock<MachineStats>> =
224        Arc::new(RwLock::new(MachineStats::default()));
225    let cached_machine_connections: Arc<tokio::sync::Mutex<machines::MachineConnections>> =
226        Arc::new(tokio::sync::Mutex::new(HashMap::new()));
227    std::env::set_var("DBUS_SYSTEM_BUS_ADDRESS", &config.monitord.dbus_address);
228    let sdc = get_or_create_dbus_connection(&config, maybe_connection).await?;
229    let mut join_set: tokio::task::JoinSet<TimedCollectorOutput> = tokio::task::JoinSet::new();
230    let mut had_error;
231
232    loop {
233        let collect_start_time = Instant::now();
234        // Kept alive for the whole iteration so its reported duration covers
235        // the full run (spawn + drain), not just the synchronous spawn phase
236        // below where `run_guard` is held.
237        let run_span = tracing::info_span!("stat_collector_run");
238        let run_guard = run_span.enter();
239        info!("Starting stat collection run");
240
241        // Always collect systemd version
242
243        spawn_timed(
244            &mut join_set,
245            "version",
246            collect_start_time,
247            crate::system::update_version(sdc.clone(), locked_machine_stats.clone()),
248        );
249
250        // Collect pid1 procfs stats
251        if config.pid1.enabled {
252            spawn_timed(
253                &mut join_set,
254                "pid1",
255                collect_start_time,
256                crate::pid1::update_pid1_stats(1, locked_machine_stats.clone()),
257            );
258        }
259
260        // Run networkd collector if enabled
261        if config.networkd.enabled {
262            let config_clone = Arc::clone(&config);
263            let sdc_clone = sdc.clone();
264            let stats_clone = locked_machine_stats.clone();
265            spawn_timed(&mut join_set, "networkd", collect_start_time, async move {
266                if config_clone.varlink.enabled {
267                    let socket_path = crate::varlink_networkd::NETWORK_SOCKET_PATH.to_string();
268                    match crate::varlink_networkd::get_networkd_state(&socket_path).await {
269                        Ok(networkd_stats) => {
270                            let mut machine_stats = stats_clone.write().await;
271                            machine_stats.networkd = networkd_stats;
272                            return Ok(());
273                        }
274                        Err(err) => {
275                            warn!(
276                                "Varlink networkd stats failed, falling back to file-based: {:?}",
277                                err
278                            );
279                        }
280                    }
281                }
282                crate::networkd::update_networkd_stats(
283                    config_clone.networkd.link_state_dir.clone(),
284                    None,
285                    sdc_clone,
286                    stats_clone,
287                )
288                .await
289            });
290        }
291
292        // Run system running (SystemState) state collector
293        if config.system_state.enabled {
294            spawn_timed(
295                &mut join_set,
296                "system_state",
297                collect_start_time,
298                crate::system::update_system_stats(sdc.clone(), locked_machine_stats.clone()),
299            );
300        }
301
302        // Run service collectors if there are services listed in config
303        if config.units.enabled {
304            let config_clone = Arc::clone(&config);
305            let sdc_clone = sdc.clone();
306            let stats_clone = locked_machine_stats.clone();
307            spawn_timed(&mut join_set, "units", collect_start_time, async move {
308                if config_clone.varlink.enabled {
309                    let socket_path = crate::varlink_units::METRICS_SOCKET_PATH.to_string();
310                    match crate::varlink_units::update_unit_stats(
311                        Arc::clone(&config_clone),
312                        stats_clone.clone(),
313                        socket_path,
314                    )
315                    .await
316                    {
317                        Ok(()) => {
318                            // Timer properties are not yet exposed via varlink; collect via D-Bus.
319                            match crate::timer::collect_all_timers_dbus(&sdc_clone, &config_clone)
320                                .await
321                            {
322                                Ok(timer_stats) => {
323                                    let mut ms = stats_clone.write().await;
324                                    ms.units.timer_stats = timer_stats.timer_stats;
325                                    ms.units.timer_persistent_units =
326                                        timer_stats.timer_persistent_units;
327                                    ms.units.timer_remain_after_elapse =
328                                        timer_stats.timer_remain_after_elapse;
329                                }
330                                Err(err) => {
331                                    warn!("Varlink timer stats (D-Bus fallback) failed: {:?}", err);
332                                }
333                            }
334                            if config_clone.units.unit_files {
335                                let unit_files = crate::units::collect_unit_files_stats("").await;
336                                let mut ms = stats_clone.write().await;
337                                ms.units.unit_files = unit_files;
338                            }
339                            return Ok(());
340                        }
341                        Err(err) => {
342                            warn!(
343                                "Varlink units stats failed, falling back to D-Bus: {:?}",
344                                err
345                            );
346                        }
347                    }
348                }
349                crate::units::update_unit_stats(config_clone, sdc_clone, stats_clone, String::new())
350                    .await
351            });
352        }
353
354        if config.machines.enabled {
355            spawn_timed(
356                &mut join_set,
357                "machines",
358                collect_start_time,
359                crate::machines::update_machines_stats(
360                    Arc::clone(&config),
361                    sdc.clone(),
362                    locked_monitord_stats.clone(),
363                    cached_machine_connections.clone(),
364                ),
365            );
366        }
367
368        if config.dbus_stats.enabled {
369            spawn_timed(
370                &mut join_set,
371                "dbus_stats",
372                collect_start_time,
373                crate::dbus_stats::update_dbus_stats(
374                    Arc::clone(&config),
375                    sdc.clone(),
376                    locked_machine_stats.clone(),
377                ),
378            );
379        }
380
381        if config.boot_blame.enabled {
382            spawn_timed(
383                &mut join_set,
384                "boot_blame",
385                collect_start_time,
386                crate::boot::update_boot_blame_stats(
387                    Arc::clone(&config),
388                    sdc.clone(),
389                    locked_machine_stats.clone(),
390                ),
391            );
392        }
393
394        if config.verify.enabled {
395            spawn_timed(
396                &mut join_set,
397                "verify",
398                collect_start_time,
399                crate::verify::update_verify_stats(
400                    sdc.clone(),
401                    locked_machine_stats.clone(),
402                    config.verify.allowlist.clone(),
403                    config.verify.blocklist.clone(),
404                ),
405            );
406        }
407
408        if join_set.len() == 1 {
409            warn!("No collectors except systemd version scheduled to run. Exiting");
410        }
411
412        // All collectors above were spawned with `run_span` captured as their
413        // parent; the guard must be dropped before the first `.await` below
414        // since span guards are not valid to hold across an await point.
415        drop(run_guard);
416
417        // Drain join_set, collect per-collector timings + log per-collector failures
418        had_error = false;
419        let mut timings: Vec<CollectorTiming> = Vec::new();
420        while let Some(res) = join_set.join_next().await {
421            match res {
422                Ok((name, collector_result, start_offset, elapsed)) => {
423                    let success = collector_result.is_ok();
424                    if let Err(e) = collector_result {
425                        had_error = true;
426                        error!("Collector '{}' failure: {:?}", name, e);
427                    }
428                    timings.push(CollectorTiming {
429                        name,
430                        start_offset_ms: start_offset.as_secs_f64() * 1000.0,
431                        elapsed_ms: elapsed.as_secs_f64() * 1000.0,
432                        success,
433                    });
434                }
435                Err(e) => {
436                    had_error = true;
437                    error!("Join error: {:?}", e);
438                }
439            }
440        }
441
442        let elapsed_runtime = collect_start_time.elapsed();
443        let elapsed_runtime_ms = elapsed_runtime.as_millis();
444
445        // Sort timings by elapsed desc so the slowest collector is first in the JSON output
446        timings.sort_by(|a, b| {
447            b.elapsed_ms
448                .partial_cmp(&a.elapsed_ms)
449                .unwrap_or(std::cmp::Ordering::Equal)
450        });
451
452        // Per-collector lines log at debug! to keep daemon-mode noise low.
453        // The same data is on MonitordStats::collector_timings for callers that need it.
454        for t in &timings {
455            debug!(
456                "collector '{}' start_offset={:.1}ms elapsed={:.1}ms{}",
457                t.name,
458                t.start_offset_ms,
459                t.elapsed_ms,
460                if t.success { "" } else { " (FAILED)" },
461            );
462        }
463
464        {
465            // Update monitord stats with machine stats
466            let mut monitord_stats = locked_monitord_stats.write().await;
467            let machine_stats = locked_machine_stats.read().await;
468            monitord_stats.pid1 = machine_stats.pid1.clone();
469            monitord_stats.networkd = machine_stats.networkd.clone();
470            monitord_stats.system_state = machine_stats.system_state;
471            monitord_stats.version = machine_stats.version.clone();
472            monitord_stats.units = machine_stats.units.clone();
473            monitord_stats.dbus_stats = machine_stats.dbus_stats.clone();
474            monitord_stats.boot_blame = machine_stats.boot_blame.clone();
475            monitord_stats.verify_stats = machine_stats.verify_stats.clone();
476            set_stat_collection_run_time(&mut monitord_stats, elapsed_runtime);
477            monitord_stats.collector_timings = timings;
478        }
479
480        info!("stat collection run took {}ms", elapsed_runtime_ms);
481        if output_stats {
482            let monitord_stats = locked_monitord_stats.read().await;
483            print_stats(
484                &config.monitord.key_prefix,
485                &config.monitord.output_format,
486                &monitord_stats,
487            );
488        }
489        if !config.monitord.daemon {
490            break;
491        }
492        let sleep_time_ms = collect_interval_ms - elapsed_runtime_ms;
493        info!("stat collection sleeping for {}s 😴", sleep_time_ms / 1000);
494        tokio::time::sleep(Duration::from_millis(
495            sleep_time_ms
496                .try_into()
497                .expect("Sleep time does not fit into a u64 :O"),
498        ))
499        .await;
500    }
501    Ok(if had_error { None } else { Some(sdc) })
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507
508    #[test]
509    fn test_stat_collection_run_time_ms_conversion() {
510        let mut stats = MonitordStats::default();
511        set_stat_collection_run_time(&mut stats, Duration::from_millis(5));
512        assert_eq!(stats.stat_collection_run_time_ms, 5.0);
513
514        set_stat_collection_run_time(&mut stats, Duration::from_micros(500));
515        assert!((stats.stat_collection_run_time_ms - 0.5).abs() < f64::EPSILON);
516    }
517}