1use std::collections::HashMap;
7use std::str::FromStr;
8use std::sync::Arc;
9use std::time::Instant;
10use std::time::SystemTime;
11use std::time::UNIX_EPOCH;
12
13use struct_field_names_as_array::FieldNamesAsArray;
14use thiserror::Error;
15use tokio::sync::RwLock;
16use tokio::sync::Semaphore;
17use tokio::task::JoinSet;
18use tracing::debug;
19use tracing::error;
20use tracing::warn;
21use tracing::Instrument;
22use zbus::ObjectPath;
23use zbus::OwnedObjectPath;
24
25#[derive(Error, Debug)]
26pub enum MonitordUnitsError {
27 #[error("Units D-Bus error: {0}")]
28 ZbusError(#[from] zbus::Error),
29 #[error("Integer conversion error: {0}")]
30 IntConversion(#[from] std::num::TryFromIntError),
31 #[error("System time error: {0}")]
32 SystemTimeError(#[from] std::time::SystemTimeError),
33}
34
35use crate::timer::TimerStats;
36use crate::MachineStats;
37
38pub use crate::unit_constants::is_unit_unhealthy;
40pub use crate::unit_constants::is_unit_unhealthy_for_service;
41pub use crate::unit_constants::SystemdUnitActiveState;
42pub use crate::unit_constants::SystemdUnitLoadState;
43pub use crate::unit_constants::SYSTEMD_SERVICE_SUFFIX;
44pub use crate::unit_constants::SYSTEMD_TIMER_SUFFIX;
45
46#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
51pub struct UnitsCollectionTimings {
52 pub list_units_ms: f64,
54 pub unit_files_ms: f64,
56 pub per_unit_loop_ms: f64,
59 pub timer_dbus_fetches: u64,
61 pub state_dbus_fetches: u64,
63 pub service_dbus_fetches: u64,
65 pub slowest_units: Vec<(String, f64)>,
68}
69
70#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
72pub struct UnitFilesScope {
73 pub generated: HashMap<String, u64>,
75 pub transient: HashMap<String, u64>,
77}
78
79#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
81pub struct UnitFilesStats {
82 pub root: UnitFilesScope,
83 pub user: UnitFilesScope,
84}
85
86#[derive(
87 serde::Serialize, serde::Deserialize, Clone, Debug, Default, FieldNamesAsArray, PartialEq,
88)]
89
90pub struct SystemdUnitStats {
93 pub activating_units: u64,
95 pub active_units: u64,
97 pub automount_units: u64,
99 pub device_units: u64,
101 pub failed_units: u64,
103 pub inactive_units: u64,
105 pub jobs_queued: u64,
107 pub loaded_units: u64,
109 pub masked_units: u64,
111 pub mount_units: u64,
113 pub not_found_units: u64,
115 pub path_units: u64,
117 pub scope_units: u64,
119 pub service_units: u64,
121 pub slice_units: u64,
123 pub socket_units: u64,
125 pub target_units: u64,
127 pub timer_units: u64,
129 pub timer_persistent_units: u64,
131 pub timer_remain_after_elapse: u64,
133 pub total_units: u64,
135 pub unit_files: UnitFilesStats,
137 pub service_stats: HashMap<String, ServiceStats>,
139 pub timer_stats: HashMap<String, TimerStats>,
141 pub unit_states: HashMap<String, UnitStates>,
143 pub collection_timings: UnitsCollectionTimings,
150}
151
152#[derive(
155 serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, FieldNamesAsArray, PartialEq,
156)]
157pub struct ServiceStats {
158 pub active_enter_timestamp: u64,
160 pub active_exit_timestamp: u64,
162 pub cpuusage_nsec: u64,
164 pub inactive_exit_timestamp: u64,
166 pub ioread_bytes: u64,
168 pub ioread_operations: u64,
170 pub memory_available: u64,
172 pub memory_current: u64,
174 pub nrestarts: u32,
176 pub processes: u32,
178 pub restart_usec: u64,
180 pub state_change_timestamp: u64,
182 pub status_errno: i32,
184 pub tasks_current: u64,
186 pub timeout_clean_usec: u64,
188 pub watchdog_usec: u64,
190}
191
192#[derive(
195 serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, FieldNamesAsArray, PartialEq,
196)]
197pub struct UnitStates {
198 pub active_state: SystemdUnitActiveState,
200 pub load_state: SystemdUnitLoadState,
202 pub unhealthy: bool,
206 pub time_in_state_usecs: Option<u64>,
210}
211
212#[derive(Debug)]
217pub struct ListedUnit {
218 pub name: String, pub description: String, pub load_state: String, pub active_state: String, pub sub_state: String, pub follow_unit: String, pub unit_object_path: OwnedObjectPath, pub job_id: u32, pub job_type: String, pub job_object_path: OwnedObjectPath, }
229impl
230 From<(
231 String,
232 String,
233 String,
234 String,
235 String,
236 String,
237 OwnedObjectPath,
238 u32,
239 String,
240 OwnedObjectPath,
241 )> for ListedUnit
242{
243 fn from(
244 tuple: (
245 String,
246 String,
247 String,
248 String,
249 String,
250 String,
251 OwnedObjectPath,
252 u32,
253 String,
254 OwnedObjectPath,
255 ),
256 ) -> Self {
257 ListedUnit {
258 name: tuple.0,
259 description: tuple.1,
260 load_state: tuple.2,
261 active_state: tuple.3,
262 sub_state: tuple.4,
263 follow_unit: tuple.5,
264 unit_object_path: tuple.6,
265 job_id: tuple.7,
266 job_type: tuple.8,
267 job_object_path: tuple.9,
268 }
269 }
270}
271
272pub const SERVICE_FIELD_NAMES: &[&str] = &ServiceStats::FIELD_NAMES_AS_ARRAY;
273pub const UNIT_FIELD_NAMES: &[&str] = &SystemdUnitStats::FIELD_NAMES_AS_ARRAY;
274pub const UNIT_STATES_FIELD_NAMES: &[&str] = &UnitStates::FIELD_NAMES_AS_ARRAY;
275
276#[tracing::instrument(level = "debug", skip(connection, object_path))]
288async fn parse_service(
289 connection: &zbus::Connection,
290 name: &str,
291 object_path: &OwnedObjectPath,
292 fs_root: &str,
293 host_memory: Option<crate::cgroup::HostMemory>,
294) -> Result<ServiceStats, MonitordUnitsError> {
295 debug!("Parsing service {} stats", name);
296
297 let sp = crate::dbus::zbus_service::ServiceProxy::builder(connection)
298 .cache_properties(zbus::proxy::CacheProperties::No)
299 .path(object_path.clone())
300 .build()
301 .await?;
302 let up = crate::dbus::zbus_unit::UnitProxy::builder(connection)
303 .cache_properties(zbus::proxy::CacheProperties::No)
304 .path(object_path.clone())
305 .build()
306 .await?;
307
308 let (control_group, main_pid, control_pid) =
314 tokio::join!(sp.control_group(), sp.main_pid(), sp.control_pid());
315 let (control_group, main_pid, control_pid) = (control_group?, main_pid?, control_pid?);
316 let mut extra_pids = Vec::with_capacity(2);
317 for pid in [main_pid, control_pid] {
318 if pid != 0 {
319 extra_pids.push(pid);
320 }
321 }
322
323 let cgroup_stats =
328 crate::cgroup::read_service_cgroup(fs_root, &control_group, &extra_pids, host_memory);
329
330 let (
331 active_enter_timestamp,
332 active_exit_timestamp,
333 inactive_exit_timestamp,
334 nrestarts,
335 restart_usec,
336 state_change_timestamp,
337 status_errno,
338 timeout_clean_usec,
339 watchdog_usec,
340 cgroup_stats,
341 ) = tokio::join!(
342 up.active_enter_timestamp(),
343 up.active_exit_timestamp(),
344 up.inactive_exit_timestamp(),
345 sp.nrestarts(),
346 sp.restart_usec(),
347 up.state_change_timestamp(),
348 sp.status_errno(),
349 sp.timeout_clean_usec(),
350 sp.watchdog_usec(),
351 cgroup_stats,
352 );
353
354 let (
361 cpuusage_nsec,
362 ioread_bytes,
363 ioread_operations,
364 memory_current,
365 memory_available,
366 tasks_current,
367 ) = tokio::join!(
368 async {
369 match cgroup_stats.cpu_usage_nsec {
370 Some(value) => Ok(value),
371 None => sp.cpuusage_nsec().await,
372 }
373 },
374 async {
375 match cgroup_stats.io_read_bytes {
376 Some(value) => Ok(value),
377 None => sp.ioread_bytes().await,
378 }
379 },
380 async {
381 match cgroup_stats.io_read_operations {
382 Some(value) => Ok(value),
383 None => sp.ioread_operations().await,
384 }
385 },
386 async {
387 match cgroup_stats.memory_current {
388 Some(value) => Ok(value),
389 None => sp.memory_current().await,
390 }
391 },
392 async {
393 match cgroup_stats.memory_available {
394 Some(value) => Ok(value),
395 None => sp.memory_available().await,
396 }
397 },
398 async {
399 match cgroup_stats.tasks_current {
400 Some(value) => Ok(value),
401 None => sp.tasks_current().await,
402 }
403 },
404 );
405 let (
406 cpuusage_nsec,
407 ioread_bytes,
408 ioread_operations,
409 memory_current,
410 memory_available,
411 tasks_current,
412 ) = (
413 cpuusage_nsec?,
414 ioread_bytes?,
415 ioread_operations?,
416 memory_current?,
417 memory_available?,
418 tasks_current?,
419 );
420
421 Ok(ServiceStats {
422 active_enter_timestamp: active_enter_timestamp?,
423 active_exit_timestamp: active_exit_timestamp?,
424 cpuusage_nsec,
425 inactive_exit_timestamp: inactive_exit_timestamp?,
426 ioread_bytes,
427 ioread_operations,
428 memory_current,
429 memory_available,
430 nrestarts: nrestarts?,
431 processes: cgroup_stats.processes,
432 restart_usec: restart_usec?,
433 state_change_timestamp: state_change_timestamp?,
434 status_errno: status_errno?,
435 tasks_current,
436 timeout_clean_usec: timeout_clean_usec?,
437 watchdog_usec: watchdog_usec?,
438 })
439}
440
441pub(crate) fn compute_time_in_state(now_usec: u64, state_change_timestamp: u64) -> Option<u64> {
448 if state_change_timestamp == 0 {
449 return None;
450 }
451 Some(now_usec.saturating_sub(state_change_timestamp))
452}
453
454#[tracing::instrument(level = "debug", skip(connection))]
455async fn get_time_in_state(
456 connection: Option<&zbus::Connection>,
457 unit: &ListedUnit,
458) -> Result<Option<u64>, MonitordUnitsError> {
459 match connection {
460 Some(c) => {
461 let up = crate::dbus::zbus_unit::UnitProxy::builder(c)
462 .cache_properties(zbus::proxy::CacheProperties::No)
463 .path(ObjectPath::from(unit.unit_object_path.clone()))
464 .build()
465 .await?;
466 let now: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() * 1_000_000;
467 let state_change_timestamp = match up.state_change_timestamp().await {
468 Ok(sct) => sct,
469 Err(err) => {
470 error!(
471 "Unable to get state_change_timestamp for {} - Setting to 0: {:?}",
472 &unit.name, err,
473 );
474 0
475 }
476 };
477 Ok(compute_time_in_state(now, state_change_timestamp))
478 }
479 None => {
480 error!("No zbus connection passed, but time_in_state_usecs enabled");
481 Ok(None)
482 }
483 }
484}
485
486#[tracing::instrument(level = "debug", skip(config, connection))]
493pub async fn parse_state(
494 unit: &ListedUnit,
495 config: &crate::config::UnitsConfig,
496 connection: Option<&zbus::Connection>,
497) -> Result<(bool, Option<UnitStates>), MonitordUnitsError> {
498 if config.state_stats_blocklist.contains(&unit.name) {
499 debug!("Skipping state stats for {} due to blocklist", &unit.name);
500 return Ok((false, None));
501 }
502 if !config.state_stats_allowlist.is_empty()
503 && !config.state_stats_allowlist.contains(&unit.name)
504 {
505 return Ok((false, None));
506 }
507 let active_state = SystemdUnitActiveState::from_str(&unit.active_state)
508 .unwrap_or(SystemdUnitActiveState::unknown);
509 let load_state = SystemdUnitLoadState::from_str(&unit.load_state.replace('-', "_"))
510 .unwrap_or(SystemdUnitLoadState::unknown);
511 let mut is_oneshot_service = false;
512 if config.ignore_inactive_oneshot_services
513 && unit.name.ends_with(SYSTEMD_SERVICE_SUFFIX)
514 && matches!(active_state, SystemdUnitActiveState::inactive)
515 && matches!(load_state, SystemdUnitLoadState::loaded)
516 {
517 if let Some(conn) = connection {
518 match is_oneshot_service_unit(conn, unit).await {
519 Ok(is_oneshot) => is_oneshot_service = is_oneshot,
520 Err(err) => warn!(
521 "Unable to get Service.Type for {} (assuming not oneshot): {:?}",
522 &unit.name, err
523 ),
524 }
525 }
526 }
527
528 let mut time_in_state_usecs: Option<u64> = None;
530 let mut did_dbus_fetch = false;
531 if config.state_stats_time_in_state {
532 time_in_state_usecs = get_time_in_state(connection, unit).await?;
533 did_dbus_fetch = connection.is_some();
536 }
537
538 let entry = UnitStates {
539 active_state,
540 load_state,
541 unhealthy: is_unit_unhealthy_for_service(
542 active_state,
543 load_state,
544 is_oneshot_service,
545 config.ignore_inactive_oneshot_services,
546 ),
547 time_in_state_usecs,
548 };
549 Ok((did_dbus_fetch, Some(entry)))
550}
551
552#[tracing::instrument(level = "debug", skip(connection))]
553async fn is_oneshot_service_unit(
554 connection: &zbus::Connection,
555 unit: &ListedUnit,
556) -> Result<bool, MonitordUnitsError> {
557 is_oneshot_service_at_path(connection, &unit.unit_object_path).await
558}
559
560async fn is_oneshot_service_at_path(
562 connection: &zbus::Connection,
563 object_path: &OwnedObjectPath,
564) -> Result<bool, MonitordUnitsError> {
565 let sp = crate::dbus::zbus_service::ServiceProxy::builder(connection)
566 .cache_properties(zbus::proxy::CacheProperties::No)
567 .path(ObjectPath::from(object_path.clone()))
568 .build()
569 .await?;
570 Ok(sp.type_().await? == "oneshot")
571}
572
573fn parse_unit(stats: &mut SystemdUnitStats, unit: &ListedUnit) {
575 match unit.name.rsplit('.').next() {
577 Some("automount") => stats.automount_units += 1,
578 Some("device") => stats.device_units += 1,
579 Some("mount") => stats.mount_units += 1,
580 Some("path") => stats.path_units += 1,
581 Some("scope") => stats.scope_units += 1,
582 Some("service") => stats.service_units += 1,
583 Some("slice") => stats.slice_units += 1,
584 Some("socket") => stats.socket_units += 1,
585 Some("target") => stats.target_units += 1,
586 Some("timer") => stats.timer_units += 1,
587 unknown => debug!("Found unhandled '{:?}' unit type", unknown),
588 };
589 match unit.load_state.as_str() {
591 "loaded" => stats.loaded_units += 1,
592 "masked" => stats.masked_units += 1,
593 "not-found" => stats.not_found_units += 1,
594 _ => debug!("{} is not loaded. It's {}", unit.name, unit.load_state),
595 };
596 match unit.active_state.as_str() {
598 "activating" => stats.activating_units += 1,
599 "active" => stats.active_units += 1,
600 "failed" => stats.failed_units += 1,
601 "inactive" => stats.inactive_units += 1,
602 unknown => debug!("Found unhandled '{}' unit state", unknown),
603 };
604 if unit.job_id != 0 {
606 stats.jobs_queued += 1;
607 }
608}
609
610const TRANSIENT_DIR: &str = "/run/systemd/transient";
611
612async fn count_unit_files_by_type(path: &str) -> HashMap<String, u64> {
613 let mut dir = match tokio::fs::read_dir(path).await {
614 Ok(d) => d,
615 Err(err) => {
616 debug!("Unable to read {}: {:?}", path, err);
617 return HashMap::new();
618 }
619 };
620 let mut counts = HashMap::new();
621 loop {
622 match dir.next_entry().await {
623 Ok(Some(entry)) => {
624 let file_type = match entry.file_type().await {
625 Ok(ft) => ft,
626 Err(_) => continue,
627 };
628 if !file_type.is_file() {
629 continue;
630 }
631 let name = entry.file_name();
632 let unit_type = name
633 .to_str()
634 .and_then(|n| n.rsplit('.').next())
635 .unwrap_or("unknown");
636 *counts.entry(unit_type.to_string()).or_insert(0) += 1;
637 }
638 Ok(None) => break,
639 Err(err) => {
640 warn!("Error reading entry in {}: {:?}", path, err);
641 continue;
642 }
643 }
644 }
645 counts
646}
647
648fn merge_counts(target: &mut HashMap<String, u64>, source: HashMap<String, u64>) {
649 for (unit_type, count) in source {
650 *target.entry(unit_type).or_insert(0) += count;
651 }
652}
653
654async fn enumerate_user_transient_dirs(fs_root: &str) -> Vec<String> {
656 let user_dir = format!("{fs_root}/run/user");
657 match tokio::fs::read_dir(&user_dir).await {
658 Ok(mut entries) => {
659 let mut dirs = Vec::new();
660 loop {
661 match entries.next_entry().await {
662 Ok(Some(entry)) => {
663 dirs.push(format!("{}/systemd/transient", entry.path().display()));
664 }
665 Ok(None) => break,
666 Err(err) => {
667 warn!("Error reading entry in {}: {:?}", user_dir, err);
668 continue;
669 }
670 }
671 }
672 dirs
673 }
674 Err(err) => {
675 debug!("Unable to read {}: {:?}", user_dir, err);
676 Vec::new()
677 }
678 }
679}
680
681pub async fn collect_unit_files_stats(fs_root: &str) -> UnitFilesStats {
689 let gen_path = format!("{fs_root}/run/systemd/generator");
691 let gen_early_path = format!("{fs_root}/run/systemd/generator.early");
692 let gen_late_path = format!("{fs_root}/run/systemd/generator.late");
693 let transient_path = format!("{fs_root}{TRANSIENT_DIR}");
694
695 let (gen, gen_early, gen_late, root_transient, user_dirs) = tokio::join!(
697 count_unit_files_by_type(&gen_path),
698 count_unit_files_by_type(&gen_early_path),
699 count_unit_files_by_type(&gen_late_path),
700 count_unit_files_by_type(&transient_path),
701 enumerate_user_transient_dirs(fs_root),
702 );
703
704 let mut root_generated = HashMap::new();
705 merge_counts(&mut root_generated, gen);
706 merge_counts(&mut root_generated, gen_early);
707 merge_counts(&mut root_generated, gen_late);
708
709 let user_transient_counts =
711 futures_util::future::join_all(user_dirs.iter().map(|d| count_unit_files_by_type(d))).await;
712
713 let mut user_transient = HashMap::new();
714 for counts in user_transient_counts {
715 merge_counts(&mut user_transient, counts);
716 }
717
718 UnitFilesStats {
719 root: UnitFilesScope {
720 generated: root_generated,
721 transient: root_transient,
722 },
723 user: UnitFilesScope {
724 generated: HashMap::new(),
725 transient: user_transient,
726 },
727 }
728}
729
730#[derive(Default)]
735struct PerUnitOutcome {
736 unit_name: String,
737 unit_states_entry: Option<UnitStates>,
738 state_dbus_fetch: bool,
739 service_stats_entry: Option<ServiceStats>,
740 timer_stats_entry: Option<TimerStats>,
741 duration_ms: f64,
742}
743
744#[tracing::instrument(level = "debug", skip(config, connection))]
746pub async fn parse_unit_state(
747 config: &Arc<crate::config::Config>,
748 connection: &zbus::Connection,
749 fs_root: &str,
750) -> Result<SystemdUnitStats, MonitordUnitsError> {
751 if !config.units.state_stats_allowlist.is_empty() {
752 debug!(
753 "Using unit state allowlist: {:?}",
754 config.units.state_stats_allowlist
755 );
756 }
757
758 if !config.units.state_stats_blocklist.is_empty() {
759 debug!(
760 "Using unit state blocklist: {:?}",
761 config.units.state_stats_blocklist,
762 );
763 }
764
765 let mut stats = SystemdUnitStats::default();
766
767 let p = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
768 .cache_properties(zbus::proxy::CacheProperties::No)
769 .build()
770 .await?;
771
772 let (unit_files_result, units_result) = tokio::join!(
774 async {
775 let start = Instant::now();
776 let files = if config.units.unit_files {
777 collect_unit_files_stats(fs_root).await
778 } else {
779 UnitFilesStats::default()
780 };
781 (files, start.elapsed().as_secs_f64() * 1000.0)
782 },
783 async {
784 let start = Instant::now();
785 let units = p.list_units().await;
786 (units, start.elapsed().as_secs_f64() * 1000.0)
787 },
788 );
789 let (unit_files, unit_files_ms) = unit_files_result;
790 let (units_result, list_units_ms) = units_result;
791 stats.collection_timings.unit_files_ms = unit_files_ms;
792 stats.collection_timings.list_units_ms = list_units_ms;
793 stats.unit_files = unit_files;
794
795 let units = units_result?;
796 stats.total_units = units.len() as u64;
797
798 let per_unit_loop_start = Instant::now();
799 let mut state_dbus_fetches: u64 = 0;
800 let mut service_dbus_fetches: u64 = 0;
801 let mut timer_dbus_fetches: u64 = 0;
802
803 let listed_units: Vec<ListedUnit> = units.into_iter().map(ListedUnit::from).collect();
807 for unit in &listed_units {
808 parse_unit(&mut stats, unit);
809 }
810
811 let semaphore = Arc::new(Semaphore::new(
817 config.units.per_unit_concurrency.max(1) as usize
818 ));
819 let parent_span = tracing::Span::current();
824 let host_memory = crate::cgroup::read_host_memory().await;
828 let mut join_set: JoinSet<PerUnitOutcome> = JoinSet::new();
829 for unit in listed_units {
830 let semaphore = Arc::clone(&semaphore);
831 let config = Arc::clone(config);
832 let connection = connection.clone();
833 let parent_span = parent_span.clone();
834 let fs_root = fs_root.to_string();
835 join_set.spawn(async move {
836 let _permit = semaphore
837 .acquire()
838 .await
839 .expect("semaphore closed unexpectedly");
840 let unit_collect_start = Instant::now();
841 let span =
842 tracing::debug_span!(parent: &parent_span, "unit_collect", unit = %unit.name);
843 let mut outcome = async {
844 let mut outcome = PerUnitOutcome {
845 unit_name: unit.name.clone(),
846 ..Default::default()
847 };
848
849 if config.units.state_stats {
854 match parse_state(&unit, &config.units, Some(&connection)).await {
855 Ok((did_fetch, entry)) => {
856 outcome.state_dbus_fetch = did_fetch;
857 outcome.unit_states_entry = entry;
858 }
859 Err(err) => {
860 error!("Unable to get state for {}: {:?}", unit.name, err);
861 }
862 }
863 }
864
865 if config.services.contains(&unit.name) {
867 debug!("Collecting service stats for {:?}", &unit);
868 match parse_service(
869 &connection,
870 &unit.name,
871 &unit.unit_object_path,
872 &fs_root,
873 host_memory,
874 )
875 .await
876 {
877 Ok(service_stats) => outcome.service_stats_entry = Some(service_stats),
878 Err(err) => error!(
879 "Unable to get service stats for {} {}: {:#?}",
880 &unit.name, &unit.unit_object_path, err
881 ),
882 }
883 }
884
885 if config.timers.enabled
887 && unit.name.ends_with(SYSTEMD_TIMER_SUFFIX)
888 && !config.timers.blocklist.contains(&unit.name)
889 && (config.timers.allowlist.is_empty()
890 || config.timers.allowlist.contains(&unit.name))
891 {
892 match crate::timer::collect_timer_stats(&connection, &unit).await {
893 Ok(ts) => outcome.timer_stats_entry = Some(ts),
894 Err(err) => error!("Failed to get {} stats: {:#?}", &unit.name, err),
895 }
896 }
897
898 outcome
899 }
900 .instrument(span)
901 .await;
902
903 outcome.duration_ms = unit_collect_start.elapsed().as_secs_f64() * 1000.0;
904 outcome
905 });
906 }
907
908 let mut slowest_units: Vec<(String, f64)> = Vec::new();
909 while let Some(res) = join_set.join_next().await {
910 let outcome = match res {
911 Ok(outcome) => outcome,
912 Err(err) => {
913 error!("Per-unit collection task failed to join: {:?}", err);
914 continue;
915 }
916 };
917 if let Some(entry) = outcome.unit_states_entry {
918 stats.unit_states.insert(outcome.unit_name.clone(), entry);
919 }
920 if outcome.state_dbus_fetch {
921 state_dbus_fetches += 1;
922 }
923 if let Some(service_stats) = outcome.service_stats_entry {
924 stats
925 .service_stats
926 .insert(outcome.unit_name.clone(), service_stats);
927 service_dbus_fetches += 1;
928 }
929 if let Some(ts) = outcome.timer_stats_entry {
930 if ts.persistent {
931 stats.timer_persistent_units += 1;
932 }
933 if ts.remain_after_elapse {
934 stats.timer_remain_after_elapse += 1;
935 }
936 stats.timer_stats.insert(outcome.unit_name.clone(), ts);
937 timer_dbus_fetches += 1;
938 }
939 if config.units.slowest_units_count > 0 {
940 slowest_units.push((outcome.unit_name, outcome.duration_ms));
941 }
942 }
943
944 if config.units.slowest_units_count > 0 {
945 slowest_units.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
946 slowest_units.truncate(config.units.slowest_units_count as usize);
947 stats.collection_timings.slowest_units = slowest_units;
948 }
949
950 let per_unit_loop_elapsed = per_unit_loop_start.elapsed();
951 stats.collection_timings.per_unit_loop_ms = per_unit_loop_elapsed.as_secs_f64() * 1000.0;
952 stats.collection_timings.state_dbus_fetches = state_dbus_fetches;
953 stats.collection_timings.service_dbus_fetches = service_dbus_fetches;
954 stats.collection_timings.timer_dbus_fetches = timer_dbus_fetches;
955
956 debug!("unit stats: {:?}", stats);
957 Ok(stats)
958}
959
960pub async fn update_unit_stats(
970 config: Arc<crate::config::Config>,
971 connection: zbus::Connection,
972 locked_machine_stats: Arc<RwLock<MachineStats>>,
973 fs_root: String,
974) -> anyhow::Result<()> {
975 let units_stats = parse_unit_state(&config, &connection, &fs_root).await;
976 let mut machine_stats = locked_machine_stats.write().await;
977 match units_stats {
978 Ok(units_stats) => machine_stats.units = units_stats,
979 Err(err) => error!("units stats failed: {:?}", err),
980 }
981 Ok(())
982}
983
984#[cfg(test)]
985mod tests {
986 use super::*;
987 use std::collections::HashSet;
988 use strum::IntoEnumIterator;
989
990 fn get_unit_file() -> ListedUnit {
991 ListedUnit {
992 name: String::from("apport-autoreport.timer"),
993 description: String::from(
994 "Process error reports when automatic reporting is enabled (timer based)",
995 ),
996 load_state: String::from("loaded"),
997 active_state: String::from("inactive"),
998 sub_state: String::from("dead"),
999 follow_unit: String::from(""),
1000 unit_object_path: ObjectPath::try_from(
1001 "/org/freedesktop/systemd1/unit/apport_2dautoreport_2etimer",
1002 )
1003 .expect("Unable to make an object path")
1004 .into(),
1005 job_id: 0,
1006 job_type: String::from(""),
1007 job_object_path: ObjectPath::try_from("/").unwrap().into(),
1008 }
1009 }
1010
1011 #[tokio::test]
1012 async fn test_state_parse() -> Result<(), MonitordUnitsError> {
1013 let test_unit_name = String::from("apport-autoreport.timer");
1014 let expected_stats = SystemdUnitStats {
1015 activating_units: 0,
1016 active_units: 0,
1017 automount_units: 0,
1018 device_units: 0,
1019 failed_units: 0,
1020 inactive_units: 0,
1021 jobs_queued: 0,
1022 loaded_units: 0,
1023 masked_units: 0,
1024 mount_units: 0,
1025 not_found_units: 0,
1026 path_units: 0,
1027 scope_units: 0,
1028 service_units: 0,
1029 slice_units: 0,
1030 socket_units: 0,
1031 target_units: 0,
1032 timer_units: 0,
1033 timer_persistent_units: 0,
1034 timer_remain_after_elapse: 0,
1035 total_units: 0,
1036 unit_files: UnitFilesStats::default(),
1037 service_stats: HashMap::new(),
1038 timer_stats: HashMap::new(),
1039 unit_states: HashMap::from([(
1040 test_unit_name.clone(),
1041 UnitStates {
1042 active_state: SystemdUnitActiveState::inactive,
1043 load_state: SystemdUnitLoadState::loaded,
1044 unhealthy: true,
1045 time_in_state_usecs: None,
1046 },
1047 )]),
1048 collection_timings: UnitsCollectionTimings::default(),
1049 };
1050 let mut stats = SystemdUnitStats::default();
1051 let systemd_unit = get_unit_file();
1052 let mut config = crate::config::UnitsConfig::default();
1053
1054 let (did_fetch, entry) = parse_state(&systemd_unit, &config, None).await?;
1057 if let Some(entry) = entry {
1058 stats.unit_states.insert(systemd_unit.name.clone(), entry);
1059 }
1060 assert_eq!(expected_stats, stats);
1061 assert!(!did_fetch);
1062
1063 config.state_stats_allowlist = HashSet::from([test_unit_name.clone()]);
1065
1066 let mut allowlist_stats = SystemdUnitStats::default();
1068 let (did_fetch, entry) = parse_state(&systemd_unit, &config, None).await?;
1069 if let Some(entry) = entry {
1070 allowlist_stats
1071 .unit_states
1072 .insert(systemd_unit.name.clone(), entry);
1073 }
1074 assert_eq!(expected_stats, allowlist_stats);
1075 assert!(!did_fetch);
1076
1077 config.state_stats_blocklist = HashSet::from([test_unit_name]);
1079
1080 let mut blocklist_stats = SystemdUnitStats::default();
1082 let expected_blocklist_stats = SystemdUnitStats::default();
1083 let (did_fetch, entry) = parse_state(&systemd_unit, &config, None).await?;
1084 if let Some(entry) = entry {
1085 blocklist_stats
1086 .unit_states
1087 .insert(systemd_unit.name.clone(), entry);
1088 }
1089 assert_eq!(expected_blocklist_stats, blocklist_stats);
1090 assert!(!did_fetch);
1092 Ok(())
1093 }
1094
1095 #[test]
1096 fn test_compute_time_in_state() {
1097 assert_eq!(
1099 compute_time_in_state(1_700_000_010_000_000, 1_700_000_000_000_000),
1100 Some(10_000_000)
1101 );
1102 assert_eq!(compute_time_in_state(1_700_000_010_000_000, 0), None);
1105 assert_eq!(compute_time_in_state(100, 200), Some(0));
1107 }
1108
1109 #[test]
1110 fn test_unit_parse() {
1111 let expected_stats = SystemdUnitStats {
1112 activating_units: 0,
1113 active_units: 0,
1114 automount_units: 0,
1115 device_units: 0,
1116 failed_units: 0,
1117 inactive_units: 1,
1118 jobs_queued: 0,
1119 loaded_units: 1,
1120 masked_units: 0,
1121 mount_units: 0,
1122 not_found_units: 0,
1123 path_units: 0,
1124 scope_units: 0,
1125 service_units: 0,
1126 slice_units: 0,
1127 socket_units: 0,
1128 target_units: 0,
1129 timer_units: 1,
1130 timer_persistent_units: 0,
1131 timer_remain_after_elapse: 0,
1132 total_units: 0,
1133 unit_files: UnitFilesStats::default(),
1134 service_stats: HashMap::new(),
1135 timer_stats: HashMap::new(),
1136 unit_states: HashMap::new(),
1137 collection_timings: UnitsCollectionTimings::default(),
1138 };
1139 let mut stats = SystemdUnitStats::default();
1140 let systemd_unit = get_unit_file();
1141 parse_unit(&mut stats, &systemd_unit);
1142 assert_eq!(expected_stats, stats);
1143 }
1144
1145 #[test]
1146 fn test_unit_parse_activating() {
1147 let mut activating_unit = get_unit_file();
1148 activating_unit.active_state = String::from("activating");
1149 let mut stats = SystemdUnitStats::default();
1150 parse_unit(&mut stats, &activating_unit);
1151 assert_eq!(stats.activating_units, 1);
1152 assert_eq!(stats.active_units, 0);
1153 assert_eq!(stats.inactive_units, 0);
1154 }
1155
1156 #[test]
1157 fn test_iterators() {
1158 assert!(SystemdUnitActiveState::iter().collect::<Vec<_>>().len() > 0);
1159 assert!(SystemdUnitLoadState::iter().collect::<Vec<_>>().len() > 0);
1160 }
1161
1162 #[tokio::test]
1163 async fn test_count_unit_files_by_type() {
1164 let dir = tempfile::tempdir().expect("Unable to create temp dir");
1165 let path = dir.path();
1166
1167 std::fs::write(path.join("sshd.service"), "").unwrap();
1168 std::fs::write(path.join("nginx.service"), "").unwrap();
1169 std::fs::write(path.join("boot.mount"), "").unwrap();
1170 std::fs::write(path.join("swap.swap"), "").unwrap();
1171 std::fs::create_dir(path.join("multi-user.target.wants")).unwrap();
1172
1173 let counts = count_unit_files_by_type(path.to_str().unwrap()).await;
1174 assert_eq!(counts.get("service"), Some(&2));
1175 assert_eq!(counts.get("mount"), Some(&1));
1176 assert_eq!(counts.get("swap"), Some(&1));
1177 assert_eq!(counts.get("wants"), None);
1178 assert_eq!(counts.len(), 3);
1179 }
1180
1181 #[tokio::test]
1182 async fn test_count_unit_files_by_type_nonexistent_dir() {
1183 let counts = count_unit_files_by_type("/nonexistent/path").await;
1184 assert!(counts.is_empty());
1185 }
1186
1187 #[tokio::test]
1188 async fn test_collect_unit_files_stats_with_fs_root() {
1189 let root = tempfile::tempdir().expect("Unable to create temp dir");
1190 let root_path = root.path();
1191
1192 let gen_dir = root_path.join("run/systemd/generator");
1193 std::fs::create_dir_all(&gen_dir).unwrap();
1194 std::fs::write(gen_dir.join("boot.mount"), "").unwrap();
1195 std::fs::write(gen_dir.join("swap.swap"), "").unwrap();
1196
1197 let transient_dir = root_path.join("run/systemd/transient");
1198 std::fs::create_dir_all(&transient_dir).unwrap();
1199 std::fs::write(transient_dir.join("run-thing.service"), "").unwrap();
1200
1201 let user_transient = root_path.join("run/user/1000/systemd/transient");
1202 std::fs::create_dir_all(&user_transient).unwrap();
1203 std::fs::write(user_transient.join("app-code.scope"), "").unwrap();
1204 std::fs::write(user_transient.join("app-term.scope"), "").unwrap();
1205
1206 let stats = collect_unit_files_stats(root_path.to_str().unwrap()).await;
1207 assert_eq!(stats.root.generated.get("mount"), Some(&1));
1208 assert_eq!(stats.root.generated.get("swap"), Some(&1));
1209 assert_eq!(stats.root.transient.get("service"), Some(&1));
1210 assert_eq!(stats.user.transient.get("scope"), Some(&2));
1211 assert!(stats.user.generated.is_empty());
1212 }
1213}