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::zvariant::ObjectPath;
23use zbus::zvariant::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
573pub(crate) async fn is_oneshot_service_by_name(
579 connection: &zbus::Connection,
580 unit_name: &str,
581) -> Result<bool, MonitordUnitsError> {
582 let mp = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
583 .cache_properties(zbus::proxy::CacheProperties::No)
584 .build()
585 .await?;
586 let object_path = mp.get_unit(unit_name).await?;
587 is_oneshot_service_at_path(connection, &object_path).await
588}
589
590fn parse_unit(stats: &mut SystemdUnitStats, unit: &ListedUnit) {
592 match unit.name.rsplit('.').next() {
594 Some("automount") => stats.automount_units += 1,
595 Some("device") => stats.device_units += 1,
596 Some("mount") => stats.mount_units += 1,
597 Some("path") => stats.path_units += 1,
598 Some("scope") => stats.scope_units += 1,
599 Some("service") => stats.service_units += 1,
600 Some("slice") => stats.slice_units += 1,
601 Some("socket") => stats.socket_units += 1,
602 Some("target") => stats.target_units += 1,
603 Some("timer") => stats.timer_units += 1,
604 unknown => debug!("Found unhandled '{:?}' unit type", unknown),
605 };
606 match unit.load_state.as_str() {
608 "loaded" => stats.loaded_units += 1,
609 "masked" => stats.masked_units += 1,
610 "not-found" => stats.not_found_units += 1,
611 _ => debug!("{} is not loaded. It's {}", unit.name, unit.load_state),
612 };
613 match unit.active_state.as_str() {
615 "activating" => stats.activating_units += 1,
616 "active" => stats.active_units += 1,
617 "failed" => stats.failed_units += 1,
618 "inactive" => stats.inactive_units += 1,
619 unknown => debug!("Found unhandled '{}' unit state", unknown),
620 };
621 if unit.job_id != 0 {
623 stats.jobs_queued += 1;
624 }
625}
626
627const TRANSIENT_DIR: &str = "/run/systemd/transient";
628
629async fn count_unit_files_by_type(path: &str) -> HashMap<String, u64> {
630 let mut dir = match tokio::fs::read_dir(path).await {
631 Ok(d) => d,
632 Err(err) => {
633 debug!("Unable to read {}: {:?}", path, err);
634 return HashMap::new();
635 }
636 };
637 let mut counts = HashMap::new();
638 loop {
639 match dir.next_entry().await {
640 Ok(Some(entry)) => {
641 let file_type = match entry.file_type().await {
642 Ok(ft) => ft,
643 Err(_) => continue,
644 };
645 if !file_type.is_file() {
646 continue;
647 }
648 let name = entry.file_name();
649 let unit_type = name
650 .to_str()
651 .and_then(|n| n.rsplit('.').next())
652 .unwrap_or("unknown");
653 *counts.entry(unit_type.to_string()).or_insert(0) += 1;
654 }
655 Ok(None) => break,
656 Err(err) => {
657 warn!("Error reading entry in {}: {:?}", path, err);
658 continue;
659 }
660 }
661 }
662 counts
663}
664
665fn merge_counts(target: &mut HashMap<String, u64>, source: HashMap<String, u64>) {
666 for (unit_type, count) in source {
667 *target.entry(unit_type).or_insert(0) += count;
668 }
669}
670
671async fn enumerate_user_transient_dirs(fs_root: &str) -> Vec<String> {
673 let user_dir = format!("{fs_root}/run/user");
674 match tokio::fs::read_dir(&user_dir).await {
675 Ok(mut entries) => {
676 let mut dirs = Vec::new();
677 loop {
678 match entries.next_entry().await {
679 Ok(Some(entry)) => {
680 dirs.push(format!("{}/systemd/transient", entry.path().display()));
681 }
682 Ok(None) => break,
683 Err(err) => {
684 warn!("Error reading entry in {}: {:?}", user_dir, err);
685 continue;
686 }
687 }
688 }
689 dirs
690 }
691 Err(err) => {
692 debug!("Unable to read {}: {:?}", user_dir, err);
693 Vec::new()
694 }
695 }
696}
697
698pub async fn collect_unit_files_stats(fs_root: &str) -> UnitFilesStats {
706 let gen_path = format!("{fs_root}/run/systemd/generator");
708 let gen_early_path = format!("{fs_root}/run/systemd/generator.early");
709 let gen_late_path = format!("{fs_root}/run/systemd/generator.late");
710 let transient_path = format!("{fs_root}{TRANSIENT_DIR}");
711
712 let (gen, gen_early, gen_late, root_transient, user_dirs) = tokio::join!(
714 count_unit_files_by_type(&gen_path),
715 count_unit_files_by_type(&gen_early_path),
716 count_unit_files_by_type(&gen_late_path),
717 count_unit_files_by_type(&transient_path),
718 enumerate_user_transient_dirs(fs_root),
719 );
720
721 let mut root_generated = HashMap::new();
722 merge_counts(&mut root_generated, gen);
723 merge_counts(&mut root_generated, gen_early);
724 merge_counts(&mut root_generated, gen_late);
725
726 let user_transient_counts =
728 futures_util::future::join_all(user_dirs.iter().map(|d| count_unit_files_by_type(d))).await;
729
730 let mut user_transient = HashMap::new();
731 for counts in user_transient_counts {
732 merge_counts(&mut user_transient, counts);
733 }
734
735 UnitFilesStats {
736 root: UnitFilesScope {
737 generated: root_generated,
738 transient: root_transient,
739 },
740 user: UnitFilesScope {
741 generated: HashMap::new(),
742 transient: user_transient,
743 },
744 }
745}
746
747#[derive(Default)]
752struct PerUnitOutcome {
753 unit_name: String,
754 unit_states_entry: Option<UnitStates>,
755 state_dbus_fetch: bool,
756 service_stats_entry: Option<ServiceStats>,
757 timer_stats_entry: Option<TimerStats>,
758 duration_ms: f64,
759}
760
761#[tracing::instrument(level = "debug", skip(config, connection))]
763pub async fn parse_unit_state(
764 config: &Arc<crate::config::Config>,
765 connection: &zbus::Connection,
766 fs_root: &str,
767) -> Result<SystemdUnitStats, MonitordUnitsError> {
768 if !config.units.state_stats_allowlist.is_empty() {
769 debug!(
770 "Using unit state allowlist: {:?}",
771 config.units.state_stats_allowlist
772 );
773 }
774
775 if !config.units.state_stats_blocklist.is_empty() {
776 debug!(
777 "Using unit state blocklist: {:?}",
778 config.units.state_stats_blocklist,
779 );
780 }
781
782 let mut stats = SystemdUnitStats::default();
783
784 let p = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
785 .cache_properties(zbus::proxy::CacheProperties::No)
786 .build()
787 .await?;
788
789 let (unit_files_result, units_result) = tokio::join!(
791 async {
792 let start = Instant::now();
793 let files = if config.units.unit_files {
794 collect_unit_files_stats(fs_root).await
795 } else {
796 UnitFilesStats::default()
797 };
798 (files, start.elapsed().as_secs_f64() * 1000.0)
799 },
800 async {
801 let start = Instant::now();
802 let units = p.list_units().await;
803 (units, start.elapsed().as_secs_f64() * 1000.0)
804 },
805 );
806 let (unit_files, unit_files_ms) = unit_files_result;
807 let (units_result, list_units_ms) = units_result;
808 stats.collection_timings.unit_files_ms = unit_files_ms;
809 stats.collection_timings.list_units_ms = list_units_ms;
810 stats.unit_files = unit_files;
811
812 let units = units_result?;
813 stats.total_units = units.len() as u64;
814
815 let per_unit_loop_start = Instant::now();
816 let mut state_dbus_fetches: u64 = 0;
817 let mut service_dbus_fetches: u64 = 0;
818 let mut timer_dbus_fetches: u64 = 0;
819
820 let listed_units: Vec<ListedUnit> = units.into_iter().map(ListedUnit::from).collect();
824 for unit in &listed_units {
825 parse_unit(&mut stats, unit);
826 }
827
828 let semaphore = Arc::new(Semaphore::new(
834 config.units.per_unit_concurrency.max(1) as usize
835 ));
836 let parent_span = tracing::Span::current();
841 let host_memory = crate::cgroup::read_host_memory().await;
845 let mut join_set: JoinSet<PerUnitOutcome> = JoinSet::new();
846 for unit in listed_units {
847 let semaphore = Arc::clone(&semaphore);
848 let config = Arc::clone(config);
849 let connection = connection.clone();
850 let parent_span = parent_span.clone();
851 let fs_root = fs_root.to_string();
852 join_set.spawn(async move {
853 let _permit = semaphore
854 .acquire()
855 .await
856 .expect("semaphore closed unexpectedly");
857 let unit_collect_start = Instant::now();
858 let span =
859 tracing::debug_span!(parent: &parent_span, "unit_collect", unit = %unit.name);
860 let mut outcome = async {
861 let mut outcome = PerUnitOutcome {
862 unit_name: unit.name.clone(),
863 ..Default::default()
864 };
865
866 if config.units.state_stats {
871 match parse_state(&unit, &config.units, Some(&connection)).await {
872 Ok((did_fetch, entry)) => {
873 outcome.state_dbus_fetch = did_fetch;
874 outcome.unit_states_entry = entry;
875 }
876 Err(err) => {
877 error!("Unable to get state for {}: {:?}", unit.name, err);
878 }
879 }
880 }
881
882 if config.services.contains(&unit.name) {
884 debug!("Collecting service stats for {:?}", &unit);
885 match parse_service(
886 &connection,
887 &unit.name,
888 &unit.unit_object_path,
889 &fs_root,
890 host_memory,
891 )
892 .await
893 {
894 Ok(service_stats) => outcome.service_stats_entry = Some(service_stats),
895 Err(err) => error!(
896 "Unable to get service stats for {} {}: {:#?}",
897 &unit.name, &unit.unit_object_path, err
898 ),
899 }
900 }
901
902 if config.timers.enabled
904 && unit.name.ends_with(SYSTEMD_TIMER_SUFFIX)
905 && !config.timers.blocklist.contains(&unit.name)
906 && (config.timers.allowlist.is_empty()
907 || config.timers.allowlist.contains(&unit.name))
908 {
909 match crate::timer::collect_timer_stats(&connection, &unit).await {
910 Ok(ts) => outcome.timer_stats_entry = Some(ts),
911 Err(err) => error!("Failed to get {} stats: {:#?}", &unit.name, err),
912 }
913 }
914
915 outcome
916 }
917 .instrument(span)
918 .await;
919
920 outcome.duration_ms = unit_collect_start.elapsed().as_secs_f64() * 1000.0;
921 outcome
922 });
923 }
924
925 let mut slowest_units: Vec<(String, f64)> = Vec::new();
926 while let Some(res) = join_set.join_next().await {
927 let outcome = match res {
928 Ok(outcome) => outcome,
929 Err(err) => {
930 error!("Per-unit collection task failed to join: {:?}", err);
931 continue;
932 }
933 };
934 if let Some(entry) = outcome.unit_states_entry {
935 stats.unit_states.insert(outcome.unit_name.clone(), entry);
936 }
937 if outcome.state_dbus_fetch {
938 state_dbus_fetches += 1;
939 }
940 if let Some(service_stats) = outcome.service_stats_entry {
941 stats
942 .service_stats
943 .insert(outcome.unit_name.clone(), service_stats);
944 service_dbus_fetches += 1;
945 }
946 if let Some(ts) = outcome.timer_stats_entry {
947 if ts.persistent {
948 stats.timer_persistent_units += 1;
949 }
950 if ts.remain_after_elapse {
951 stats.timer_remain_after_elapse += 1;
952 }
953 stats.timer_stats.insert(outcome.unit_name.clone(), ts);
954 timer_dbus_fetches += 1;
955 }
956 if config.units.slowest_units_count > 0 {
957 slowest_units.push((outcome.unit_name, outcome.duration_ms));
958 }
959 }
960
961 if config.units.slowest_units_count > 0 {
962 slowest_units.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
963 slowest_units.truncate(config.units.slowest_units_count as usize);
964 stats.collection_timings.slowest_units = slowest_units;
965 }
966
967 let per_unit_loop_elapsed = per_unit_loop_start.elapsed();
968 stats.collection_timings.per_unit_loop_ms = per_unit_loop_elapsed.as_secs_f64() * 1000.0;
969 stats.collection_timings.state_dbus_fetches = state_dbus_fetches;
970 stats.collection_timings.service_dbus_fetches = service_dbus_fetches;
971 stats.collection_timings.timer_dbus_fetches = timer_dbus_fetches;
972
973 debug!("unit stats: {:?}", stats);
974 Ok(stats)
975}
976
977pub async fn update_unit_stats(
987 config: Arc<crate::config::Config>,
988 connection: zbus::Connection,
989 locked_machine_stats: Arc<RwLock<MachineStats>>,
990 fs_root: String,
991) -> anyhow::Result<()> {
992 let units_stats = parse_unit_state(&config, &connection, &fs_root).await;
993 let mut machine_stats = locked_machine_stats.write().await;
994 match units_stats {
995 Ok(units_stats) => machine_stats.units = units_stats,
996 Err(err) => error!("units stats failed: {:?}", err),
997 }
998 Ok(())
999}
1000
1001#[cfg(test)]
1002mod tests {
1003 use super::*;
1004 use std::collections::HashSet;
1005 use strum::IntoEnumIterator;
1006
1007 fn get_unit_file() -> ListedUnit {
1008 ListedUnit {
1009 name: String::from("apport-autoreport.timer"),
1010 description: String::from(
1011 "Process error reports when automatic reporting is enabled (timer based)",
1012 ),
1013 load_state: String::from("loaded"),
1014 active_state: String::from("inactive"),
1015 sub_state: String::from("dead"),
1016 follow_unit: String::from(""),
1017 unit_object_path: ObjectPath::try_from(
1018 "/org/freedesktop/systemd1/unit/apport_2dautoreport_2etimer",
1019 )
1020 .expect("Unable to make an object path")
1021 .into(),
1022 job_id: 0,
1023 job_type: String::from(""),
1024 job_object_path: ObjectPath::try_from("/").unwrap().into(),
1025 }
1026 }
1027
1028 #[tokio::test]
1029 async fn test_state_parse() -> Result<(), MonitordUnitsError> {
1030 let test_unit_name = String::from("apport-autoreport.timer");
1031 let expected_stats = SystemdUnitStats {
1032 activating_units: 0,
1033 active_units: 0,
1034 automount_units: 0,
1035 device_units: 0,
1036 failed_units: 0,
1037 inactive_units: 0,
1038 jobs_queued: 0,
1039 loaded_units: 0,
1040 masked_units: 0,
1041 mount_units: 0,
1042 not_found_units: 0,
1043 path_units: 0,
1044 scope_units: 0,
1045 service_units: 0,
1046 slice_units: 0,
1047 socket_units: 0,
1048 target_units: 0,
1049 timer_units: 0,
1050 timer_persistent_units: 0,
1051 timer_remain_after_elapse: 0,
1052 total_units: 0,
1053 unit_files: UnitFilesStats::default(),
1054 service_stats: HashMap::new(),
1055 timer_stats: HashMap::new(),
1056 unit_states: HashMap::from([(
1057 test_unit_name.clone(),
1058 UnitStates {
1059 active_state: SystemdUnitActiveState::inactive,
1060 load_state: SystemdUnitLoadState::loaded,
1061 unhealthy: true,
1062 time_in_state_usecs: None,
1063 },
1064 )]),
1065 collection_timings: UnitsCollectionTimings::default(),
1066 };
1067 let mut stats = SystemdUnitStats::default();
1068 let systemd_unit = get_unit_file();
1069 let mut config = crate::config::UnitsConfig::default();
1070
1071 let (did_fetch, entry) = parse_state(&systemd_unit, &config, None).await?;
1074 if let Some(entry) = entry {
1075 stats.unit_states.insert(systemd_unit.name.clone(), entry);
1076 }
1077 assert_eq!(expected_stats, stats);
1078 assert!(!did_fetch);
1079
1080 config.state_stats_allowlist = HashSet::from([test_unit_name.clone()]);
1082
1083 let mut allowlist_stats = SystemdUnitStats::default();
1085 let (did_fetch, entry) = parse_state(&systemd_unit, &config, None).await?;
1086 if let Some(entry) = entry {
1087 allowlist_stats
1088 .unit_states
1089 .insert(systemd_unit.name.clone(), entry);
1090 }
1091 assert_eq!(expected_stats, allowlist_stats);
1092 assert!(!did_fetch);
1093
1094 config.state_stats_blocklist = HashSet::from([test_unit_name]);
1096
1097 let mut blocklist_stats = SystemdUnitStats::default();
1099 let expected_blocklist_stats = SystemdUnitStats::default();
1100 let (did_fetch, entry) = parse_state(&systemd_unit, &config, None).await?;
1101 if let Some(entry) = entry {
1102 blocklist_stats
1103 .unit_states
1104 .insert(systemd_unit.name.clone(), entry);
1105 }
1106 assert_eq!(expected_blocklist_stats, blocklist_stats);
1107 assert!(!did_fetch);
1109 Ok(())
1110 }
1111
1112 #[test]
1113 fn test_compute_time_in_state() {
1114 assert_eq!(
1116 compute_time_in_state(1_700_000_010_000_000, 1_700_000_000_000_000),
1117 Some(10_000_000)
1118 );
1119 assert_eq!(compute_time_in_state(1_700_000_010_000_000, 0), None);
1122 assert_eq!(compute_time_in_state(100, 200), Some(0));
1124 }
1125
1126 #[test]
1127 fn test_unit_parse() {
1128 let expected_stats = SystemdUnitStats {
1129 activating_units: 0,
1130 active_units: 0,
1131 automount_units: 0,
1132 device_units: 0,
1133 failed_units: 0,
1134 inactive_units: 1,
1135 jobs_queued: 0,
1136 loaded_units: 1,
1137 masked_units: 0,
1138 mount_units: 0,
1139 not_found_units: 0,
1140 path_units: 0,
1141 scope_units: 0,
1142 service_units: 0,
1143 slice_units: 0,
1144 socket_units: 0,
1145 target_units: 0,
1146 timer_units: 1,
1147 timer_persistent_units: 0,
1148 timer_remain_after_elapse: 0,
1149 total_units: 0,
1150 unit_files: UnitFilesStats::default(),
1151 service_stats: HashMap::new(),
1152 timer_stats: HashMap::new(),
1153 unit_states: HashMap::new(),
1154 collection_timings: UnitsCollectionTimings::default(),
1155 };
1156 let mut stats = SystemdUnitStats::default();
1157 let systemd_unit = get_unit_file();
1158 parse_unit(&mut stats, &systemd_unit);
1159 assert_eq!(expected_stats, stats);
1160 }
1161
1162 #[test]
1163 fn test_unit_parse_activating() {
1164 let mut activating_unit = get_unit_file();
1165 activating_unit.active_state = String::from("activating");
1166 let mut stats = SystemdUnitStats::default();
1167 parse_unit(&mut stats, &activating_unit);
1168 assert_eq!(stats.activating_units, 1);
1169 assert_eq!(stats.active_units, 0);
1170 assert_eq!(stats.inactive_units, 0);
1171 }
1172
1173 #[test]
1174 fn test_iterators() {
1175 assert!(SystemdUnitActiveState::iter().collect::<Vec<_>>().len() > 0);
1176 assert!(SystemdUnitLoadState::iter().collect::<Vec<_>>().len() > 0);
1177 }
1178
1179 #[tokio::test]
1180 async fn test_count_unit_files_by_type() {
1181 let dir = tempfile::tempdir().expect("Unable to create temp dir");
1182 let path = dir.path();
1183
1184 std::fs::write(path.join("sshd.service"), "").unwrap();
1185 std::fs::write(path.join("nginx.service"), "").unwrap();
1186 std::fs::write(path.join("boot.mount"), "").unwrap();
1187 std::fs::write(path.join("swap.swap"), "").unwrap();
1188 std::fs::create_dir(path.join("multi-user.target.wants")).unwrap();
1189
1190 let counts = count_unit_files_by_type(path.to_str().unwrap()).await;
1191 assert_eq!(counts.get("service"), Some(&2));
1192 assert_eq!(counts.get("mount"), Some(&1));
1193 assert_eq!(counts.get("swap"), Some(&1));
1194 assert_eq!(counts.get("wants"), None);
1195 assert_eq!(counts.len(), 3);
1196 }
1197
1198 #[tokio::test]
1199 async fn test_count_unit_files_by_type_nonexistent_dir() {
1200 let counts = count_unit_files_by_type("/nonexistent/path").await;
1201 assert!(counts.is_empty());
1202 }
1203
1204 #[tokio::test]
1205 async fn test_collect_unit_files_stats_with_fs_root() {
1206 let root = tempfile::tempdir().expect("Unable to create temp dir");
1207 let root_path = root.path();
1208
1209 let gen_dir = root_path.join("run/systemd/generator");
1210 std::fs::create_dir_all(&gen_dir).unwrap();
1211 std::fs::write(gen_dir.join("boot.mount"), "").unwrap();
1212 std::fs::write(gen_dir.join("swap.swap"), "").unwrap();
1213
1214 let transient_dir = root_path.join("run/systemd/transient");
1215 std::fs::create_dir_all(&transient_dir).unwrap();
1216 std::fs::write(transient_dir.join("run-thing.service"), "").unwrap();
1217
1218 let user_transient = root_path.join("run/user/1000/systemd/transient");
1219 std::fs::create_dir_all(&user_transient).unwrap();
1220 std::fs::write(user_transient.join("app-code.scope"), "").unwrap();
1221 std::fs::write(user_transient.join("app-term.scope"), "").unwrap();
1222
1223 let stats = collect_unit_files_stats(root_path.to_str().unwrap()).await;
1224 assert_eq!(stats.root.generated.get("mount"), Some(&1));
1225 assert_eq!(stats.root.generated.get("swap"), Some(&1));
1226 assert_eq!(stats.root.transient.get("service"), Some(&1));
1227 assert_eq!(stats.user.transient.get("scope"), Some(&2));
1228 assert!(stats.user.generated.is_empty());
1229 }
1230}