1use 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#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
54pub struct CollectorTiming {
55 pub name: String,
57 pub start_offset_ms: f64,
60 pub elapsed_ms: f64,
62 pub success: bool,
64}
65
66#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
68pub struct MachineStats {
69 pub networkd: networkd::NetworkdState,
71 pub pid1: Option<pid1::Pid1Stats>,
73 pub system_state: system::SystemdSystemState,
75 pub units: units::SystemdUnitStats,
77 pub version: system::SystemdVersion,
79 pub dbus_stats: Option<dbus_stats::DBusStats>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub boot_blame: Option<boot::BootBlameStats>,
84 pub verify_stats: Option<verify::VerifyStats>,
86}
87
88#[derive(serde::Serialize, serde::Deserialize, Debug, Default, PartialEq)]
90pub struct MonitordStats {
91 pub networkd: networkd::NetworkdState,
93 pub pid1: Option<pid1::Pid1Stats>,
95 pub system_state: system::SystemdSystemState,
97 pub units: units::SystemdUnitStats,
99 pub version: system::SystemdVersion,
101 pub dbus_stats: Option<dbus_stats::DBusStats>,
103 pub machines: HashMap<String, MachineStats>,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub boot_blame: Option<boot::BootBlameStats>,
108 pub verify_stats: Option<verify::VerifyStats>,
110 pub stat_collection_run_time_ms: f64,
112 pub collector_timings: Vec<CollectorTiming>,
117}
118
119pub 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
145type TimedCollectorOutput = (String, anyhow::Result<()>, Duration, Duration);
147
148fn 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
190async 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
204pub 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 let run_span = tracing::info_span!("stat_collector_run");
238 let run_guard = run_span.enter();
239 info!("Starting stat collection run");
240
241 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 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 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 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 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 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 drop(run_guard);
416
417 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 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 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 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}