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;
44
45#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
50pub struct UnitsCollectionTimings {
51 pub list_units_ms: f64,
53 pub unit_files_ms: f64,
55 pub per_unit_loop_ms: f64,
58 pub timer_dbus_fetches: u64,
60 pub state_dbus_fetches: u64,
62 pub service_dbus_fetches: u64,
64 pub slowest_units: Vec<(String, f64)>,
67}
68
69#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
71pub struct UnitFilesScope {
72 pub generated: HashMap<String, u64>,
74 pub transient: HashMap<String, u64>,
76}
77
78#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialEq)]
80pub struct UnitFilesStats {
81 pub root: UnitFilesScope,
82 pub user: UnitFilesScope,
83}
84
85#[derive(
86 serde::Serialize, serde::Deserialize, Clone, Debug, Default, FieldNamesAsArray, PartialEq,
87)]
88
89pub struct SystemdUnitStats {
92 pub activating_units: u64,
94 pub active_units: u64,
96 pub automount_units: u64,
98 pub device_units: u64,
100 pub failed_units: u64,
102 pub inactive_units: u64,
104 pub jobs_queued: u64,
106 pub loaded_units: u64,
108 pub masked_units: u64,
110 pub mount_units: u64,
112 pub not_found_units: u64,
114 pub path_units: u64,
116 pub scope_units: u64,
118 pub service_units: u64,
120 pub slice_units: u64,
122 pub socket_units: u64,
124 pub target_units: u64,
126 pub timer_units: u64,
128 pub timer_persistent_units: u64,
130 pub timer_remain_after_elapse: u64,
132 pub total_units: u64,
134 pub unit_files: UnitFilesStats,
136 pub service_stats: HashMap<String, ServiceStats>,
138 pub timer_stats: HashMap<String, TimerStats>,
140 pub unit_states: HashMap<String, UnitStates>,
142 pub collection_timings: UnitsCollectionTimings,
145}
146
147#[derive(
150 serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, FieldNamesAsArray, PartialEq,
151)]
152pub struct ServiceStats {
153 pub active_enter_timestamp: u64,
155 pub active_exit_timestamp: u64,
157 pub cpuusage_nsec: u64,
159 pub inactive_exit_timestamp: u64,
161 pub ioread_bytes: u64,
163 pub ioread_operations: u64,
165 pub memory_available: u64,
167 pub memory_current: u64,
169 pub nrestarts: u32,
171 pub processes: u32,
173 pub restart_usec: u64,
175 pub state_change_timestamp: u64,
177 pub status_errno: i32,
179 pub tasks_current: u64,
181 pub timeout_clean_usec: u64,
183 pub watchdog_usec: u64,
185}
186
187#[derive(
190 serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, FieldNamesAsArray, PartialEq,
191)]
192pub struct UnitStates {
193 pub active_state: SystemdUnitActiveState,
195 pub load_state: SystemdUnitLoadState,
197 pub unhealthy: bool,
201 pub time_in_state_usecs: Option<u64>,
204}
205
206#[derive(Debug)]
211pub struct ListedUnit {
212 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, }
223impl
224 From<(
225 String,
226 String,
227 String,
228 String,
229 String,
230 String,
231 OwnedObjectPath,
232 u32,
233 String,
234 OwnedObjectPath,
235 )> for ListedUnit
236{
237 fn from(
238 tuple: (
239 String,
240 String,
241 String,
242 String,
243 String,
244 String,
245 OwnedObjectPath,
246 u32,
247 String,
248 OwnedObjectPath,
249 ),
250 ) -> Self {
251 ListedUnit {
252 name: tuple.0,
253 description: tuple.1,
254 load_state: tuple.2,
255 active_state: tuple.3,
256 sub_state: tuple.4,
257 follow_unit: tuple.5,
258 unit_object_path: tuple.6,
259 job_id: tuple.7,
260 job_type: tuple.8,
261 job_object_path: tuple.9,
262 }
263 }
264}
265
266pub const SERVICE_FIELD_NAMES: &[&str] = &ServiceStats::FIELD_NAMES_AS_ARRAY;
267pub const UNIT_FIELD_NAMES: &[&str] = &SystemdUnitStats::FIELD_NAMES_AS_ARRAY;
268pub const UNIT_STATES_FIELD_NAMES: &[&str] = &UnitStates::FIELD_NAMES_AS_ARRAY;
269
270#[tracing::instrument(level = "debug", skip(connection, object_path))]
272async fn parse_service(
273 connection: &zbus::Connection,
274 name: &str,
275 object_path: &OwnedObjectPath,
276) -> Result<ServiceStats, MonitordUnitsError> {
277 debug!("Parsing service {} stats", name);
278
279 let sp = crate::dbus::zbus_service::ServiceProxy::builder(connection)
280 .cache_properties(zbus::proxy::CacheProperties::No)
281 .path(object_path.clone())?
282 .build()
283 .await?;
284 let up = crate::dbus::zbus_unit::UnitProxy::builder(connection)
285 .cache_properties(zbus::proxy::CacheProperties::No)
286 .path(object_path.clone())?
287 .build()
288 .await?;
289
290 let (
293 active_enter_timestamp,
294 active_exit_timestamp,
295 cpuusage_nsec,
296 inactive_exit_timestamp,
297 ioread_bytes,
298 ioread_operations,
299 memory_current,
300 memory_available,
301 nrestarts,
302 processes,
303 restart_usec,
304 state_change_timestamp,
305 status_errno,
306 tasks_current,
307 timeout_clean_usec,
308 watchdog_usec,
309 ) = tokio::join!(
310 up.active_enter_timestamp(),
311 up.active_exit_timestamp(),
312 sp.cpuusage_nsec(),
313 up.inactive_exit_timestamp(),
314 sp.ioread_bytes(),
315 sp.ioread_operations(),
316 sp.memory_current(),
317 sp.memory_available(),
318 sp.nrestarts(),
319 sp.get_processes(),
320 sp.restart_usec(),
321 up.state_change_timestamp(),
322 sp.status_errno(),
323 sp.tasks_current(),
324 sp.timeout_clean_usec(),
325 sp.watchdog_usec(),
326 );
327
328 Ok(ServiceStats {
329 active_enter_timestamp: active_enter_timestamp?,
330 active_exit_timestamp: active_exit_timestamp?,
331 cpuusage_nsec: cpuusage_nsec?,
332 inactive_exit_timestamp: inactive_exit_timestamp?,
333 ioread_bytes: ioread_bytes?,
334 ioread_operations: ioread_operations?,
335 memory_current: memory_current?,
336 memory_available: memory_available?,
337 nrestarts: nrestarts?,
338 processes: processes?.len().try_into()?,
339 restart_usec: restart_usec?,
340 state_change_timestamp: state_change_timestamp?,
341 status_errno: status_errno?,
342 tasks_current: tasks_current?,
343 timeout_clean_usec: timeout_clean_usec?,
344 watchdog_usec: watchdog_usec?,
345 })
346}
347
348#[tracing::instrument(level = "debug", skip(connection))]
349async fn get_time_in_state(
350 connection: Option<&zbus::Connection>,
351 unit: &ListedUnit,
352) -> Result<Option<u64>, MonitordUnitsError> {
353 match connection {
354 Some(c) => {
355 let up = crate::dbus::zbus_unit::UnitProxy::builder(c)
356 .cache_properties(zbus::proxy::CacheProperties::No)
357 .path(ObjectPath::from(unit.unit_object_path.clone()))?
358 .build()
359 .await?;
360 let now: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() * 1_000_000;
361 let state_change_timestamp = match up.state_change_timestamp().await {
362 Ok(sct) => sct,
363 Err(err) => {
364 error!(
365 "Unable to get state_change_timestamp for {} - Setting to 0: {:?}",
366 &unit.name, err,
367 );
368 0
369 }
370 };
371 Ok(Some(now - state_change_timestamp))
372 }
373 None => {
374 error!("No zbus connection passed, but time_in_state_usecs enabled");
375 Ok(None)
376 }
377 }
378}
379
380#[tracing::instrument(level = "debug", skip(config, connection))]
387pub async fn parse_state(
388 unit: &ListedUnit,
389 config: &crate::config::UnitsConfig,
390 connection: Option<&zbus::Connection>,
391) -> Result<(bool, Option<UnitStates>), MonitordUnitsError> {
392 if config.state_stats_blocklist.contains(&unit.name) {
393 debug!("Skipping state stats for {} due to blocklist", &unit.name);
394 return Ok((false, None));
395 }
396 if !config.state_stats_allowlist.is_empty()
397 && !config.state_stats_allowlist.contains(&unit.name)
398 {
399 return Ok((false, None));
400 }
401 let active_state = SystemdUnitActiveState::from_str(&unit.active_state)
402 .unwrap_or(SystemdUnitActiveState::unknown);
403 let load_state = SystemdUnitLoadState::from_str(&unit.load_state.replace('-', "_"))
404 .unwrap_or(SystemdUnitLoadState::unknown);
405 let mut is_oneshot_service = false;
406 if config.ignore_inactive_oneshot_services
407 && unit.name.ends_with(SYSTEMD_SERVICE_SUFFIX)
408 && matches!(active_state, SystemdUnitActiveState::inactive)
409 && matches!(load_state, SystemdUnitLoadState::loaded)
410 {
411 if let Some(conn) = connection {
412 match is_oneshot_service_unit(conn, unit).await {
413 Ok(is_oneshot) => is_oneshot_service = is_oneshot,
414 Err(err) => warn!(
415 "Unable to get Service.Type for {} (assuming not oneshot): {:?}",
416 &unit.name, err
417 ),
418 }
419 }
420 }
421
422 let mut time_in_state_usecs: Option<u64> = None;
424 let mut did_dbus_fetch = false;
425 if config.state_stats_time_in_state {
426 time_in_state_usecs = get_time_in_state(connection, unit).await?;
427 did_dbus_fetch = connection.is_some();
430 }
431
432 let entry = UnitStates {
433 active_state,
434 load_state,
435 unhealthy: is_unit_unhealthy_for_service(
436 active_state,
437 load_state,
438 is_oneshot_service,
439 config.ignore_inactive_oneshot_services,
440 ),
441 time_in_state_usecs,
442 };
443 Ok((did_dbus_fetch, Some(entry)))
444}
445
446#[tracing::instrument(level = "debug", skip(connection))]
447async fn is_oneshot_service_unit(
448 connection: &zbus::Connection,
449 unit: &ListedUnit,
450) -> Result<bool, MonitordUnitsError> {
451 let sp = crate::dbus::zbus_service::ServiceProxy::builder(connection)
452 .cache_properties(zbus::proxy::CacheProperties::No)
453 .path(ObjectPath::from(unit.unit_object_path.clone()))?
454 .build()
455 .await?;
456 Ok(sp.type_().await? == "oneshot")
457}
458
459fn parse_unit(stats: &mut SystemdUnitStats, unit: &ListedUnit) {
461 match unit.name.rsplit('.').next() {
463 Some("automount") => stats.automount_units += 1,
464 Some("device") => stats.device_units += 1,
465 Some("mount") => stats.mount_units += 1,
466 Some("path") => stats.path_units += 1,
467 Some("scope") => stats.scope_units += 1,
468 Some("service") => stats.service_units += 1,
469 Some("slice") => stats.slice_units += 1,
470 Some("socket") => stats.socket_units += 1,
471 Some("target") => stats.target_units += 1,
472 Some("timer") => stats.timer_units += 1,
473 unknown => debug!("Found unhandled '{:?}' unit type", unknown),
474 };
475 match unit.load_state.as_str() {
477 "loaded" => stats.loaded_units += 1,
478 "masked" => stats.masked_units += 1,
479 "not-found" => stats.not_found_units += 1,
480 _ => debug!("{} is not loaded. It's {}", unit.name, unit.load_state),
481 };
482 match unit.active_state.as_str() {
484 "activating" => stats.activating_units += 1,
485 "active" => stats.active_units += 1,
486 "failed" => stats.failed_units += 1,
487 "inactive" => stats.inactive_units += 1,
488 unknown => debug!("Found unhandled '{}' unit state", unknown),
489 };
490 if unit.job_id != 0 {
492 stats.jobs_queued += 1;
493 }
494}
495
496const TRANSIENT_DIR: &str = "/run/systemd/transient";
497
498async fn count_unit_files_by_type(path: &str) -> HashMap<String, u64> {
499 let mut dir = match tokio::fs::read_dir(path).await {
500 Ok(d) => d,
501 Err(err) => {
502 debug!("Unable to read {}: {:?}", path, err);
503 return HashMap::new();
504 }
505 };
506 let mut counts = HashMap::new();
507 loop {
508 match dir.next_entry().await {
509 Ok(Some(entry)) => {
510 let file_type = match entry.file_type().await {
511 Ok(ft) => ft,
512 Err(_) => continue,
513 };
514 if !file_type.is_file() {
515 continue;
516 }
517 let name = entry.file_name();
518 let unit_type = name
519 .to_str()
520 .and_then(|n| n.rsplit('.').next())
521 .unwrap_or("unknown");
522 *counts.entry(unit_type.to_string()).or_insert(0) += 1;
523 }
524 Ok(None) => break,
525 Err(err) => {
526 warn!("Error reading entry in {}: {:?}", path, err);
527 continue;
528 }
529 }
530 }
531 counts
532}
533
534fn merge_counts(target: &mut HashMap<String, u64>, source: HashMap<String, u64>) {
535 for (unit_type, count) in source {
536 *target.entry(unit_type).or_insert(0) += count;
537 }
538}
539
540async fn enumerate_user_transient_dirs(fs_root: &str) -> Vec<String> {
542 let user_dir = format!("{fs_root}/run/user");
543 match tokio::fs::read_dir(&user_dir).await {
544 Ok(mut entries) => {
545 let mut dirs = Vec::new();
546 loop {
547 match entries.next_entry().await {
548 Ok(Some(entry)) => {
549 dirs.push(format!("{}/systemd/transient", entry.path().display()));
550 }
551 Ok(None) => break,
552 Err(err) => {
553 warn!("Error reading entry in {}: {:?}", user_dir, err);
554 continue;
555 }
556 }
557 }
558 dirs
559 }
560 Err(err) => {
561 debug!("Unable to read {}: {:?}", user_dir, err);
562 Vec::new()
563 }
564 }
565}
566
567pub async fn collect_unit_files_stats(fs_root: &str) -> UnitFilesStats {
575 let gen_path = format!("{fs_root}/run/systemd/generator");
577 let gen_early_path = format!("{fs_root}/run/systemd/generator.early");
578 let gen_late_path = format!("{fs_root}/run/systemd/generator.late");
579 let transient_path = format!("{fs_root}{TRANSIENT_DIR}");
580
581 let (gen, gen_early, gen_late, root_transient, user_dirs) = tokio::join!(
583 count_unit_files_by_type(&gen_path),
584 count_unit_files_by_type(&gen_early_path),
585 count_unit_files_by_type(&gen_late_path),
586 count_unit_files_by_type(&transient_path),
587 enumerate_user_transient_dirs(fs_root),
588 );
589
590 let mut root_generated = HashMap::new();
591 merge_counts(&mut root_generated, gen);
592 merge_counts(&mut root_generated, gen_early);
593 merge_counts(&mut root_generated, gen_late);
594
595 let user_transient_counts =
597 futures_util::future::join_all(user_dirs.iter().map(|d| count_unit_files_by_type(d))).await;
598
599 let mut user_transient = HashMap::new();
600 for counts in user_transient_counts {
601 merge_counts(&mut user_transient, counts);
602 }
603
604 UnitFilesStats {
605 root: UnitFilesScope {
606 generated: root_generated,
607 transient: root_transient,
608 },
609 user: UnitFilesScope {
610 generated: HashMap::new(),
611 transient: user_transient,
612 },
613 }
614}
615
616#[derive(Default)]
621struct PerUnitOutcome {
622 unit_name: String,
623 unit_states_entry: Option<UnitStates>,
624 state_dbus_fetch: bool,
625 service_stats_entry: Option<ServiceStats>,
626 timer_stats_entry: Option<TimerStats>,
627 duration_ms: f64,
628}
629
630#[tracing::instrument(level = "debug", skip(config, connection))]
632pub async fn parse_unit_state(
633 config: &Arc<crate::config::Config>,
634 connection: &zbus::Connection,
635 fs_root: &str,
636) -> Result<SystemdUnitStats, MonitordUnitsError> {
637 if !config.units.state_stats_allowlist.is_empty() {
638 debug!(
639 "Using unit state allowlist: {:?}",
640 config.units.state_stats_allowlist
641 );
642 }
643
644 if !config.units.state_stats_blocklist.is_empty() {
645 debug!(
646 "Using unit state blocklist: {:?}",
647 config.units.state_stats_blocklist,
648 );
649 }
650
651 let mut stats = SystemdUnitStats::default();
652
653 let p = crate::dbus::zbus_systemd::ManagerProxy::builder(connection)
654 .cache_properties(zbus::proxy::CacheProperties::No)
655 .build()
656 .await?;
657
658 let (unit_files_result, units_result) = tokio::join!(
660 async {
661 let start = Instant::now();
662 let files = if config.units.unit_files {
663 collect_unit_files_stats(fs_root).await
664 } else {
665 UnitFilesStats::default()
666 };
667 (files, start.elapsed().as_secs_f64() * 1000.0)
668 },
669 async {
670 let start = Instant::now();
671 let units = p.list_units().await;
672 (units, start.elapsed().as_secs_f64() * 1000.0)
673 },
674 );
675 let (unit_files, unit_files_ms) = unit_files_result;
676 let (units_result, list_units_ms) = units_result;
677 stats.collection_timings.unit_files_ms = unit_files_ms;
678 stats.collection_timings.list_units_ms = list_units_ms;
679 stats.unit_files = unit_files;
680
681 let units = units_result?;
682 stats.total_units = units.len() as u64;
683
684 let per_unit_loop_start = Instant::now();
685 let mut state_dbus_fetches: u64 = 0;
686 let mut service_dbus_fetches: u64 = 0;
687 let mut timer_dbus_fetches: u64 = 0;
688
689 let listed_units: Vec<ListedUnit> = units.into_iter().map(ListedUnit::from).collect();
693 for unit in &listed_units {
694 parse_unit(&mut stats, unit);
695 }
696
697 let semaphore = Arc::new(Semaphore::new(
703 config.units.per_unit_concurrency.max(1) as usize
704 ));
705 let parent_span = tracing::Span::current();
710 let mut join_set: JoinSet<PerUnitOutcome> = JoinSet::new();
711 for unit in listed_units {
712 let semaphore = Arc::clone(&semaphore);
713 let config = Arc::clone(config);
714 let connection = connection.clone();
715 let parent_span = parent_span.clone();
716 join_set.spawn(async move {
717 let _permit = semaphore
718 .acquire()
719 .await
720 .expect("semaphore closed unexpectedly");
721 let unit_collect_start = Instant::now();
722 let span =
723 tracing::debug_span!(parent: &parent_span, "unit_collect", unit = %unit.name);
724 let mut outcome = async {
725 let mut outcome = PerUnitOutcome {
726 unit_name: unit.name.clone(),
727 ..Default::default()
728 };
729
730 if config.units.state_stats {
735 match parse_state(&unit, &config.units, Some(&connection)).await {
736 Ok((did_fetch, entry)) => {
737 outcome.state_dbus_fetch = did_fetch;
738 outcome.unit_states_entry = entry;
739 }
740 Err(err) => {
741 error!("Unable to get state for {}: {:?}", unit.name, err);
742 }
743 }
744 }
745
746 if config.services.contains(&unit.name) {
748 debug!("Collecting service stats for {:?}", &unit);
749 match parse_service(&connection, &unit.name, &unit.unit_object_path).await {
750 Ok(service_stats) => outcome.service_stats_entry = Some(service_stats),
751 Err(err) => error!(
752 "Unable to get service stats for {} {}: {:#?}",
753 &unit.name, &unit.unit_object_path, err
754 ),
755 }
756 }
757
758 if config.timers.enabled
760 && unit.name.contains(".timer")
761 && !config.timers.blocklist.contains(&unit.name)
762 && (config.timers.allowlist.is_empty()
763 || config.timers.allowlist.contains(&unit.name))
764 {
765 match crate::timer::collect_timer_stats(&connection, &unit).await {
766 Ok(ts) => outcome.timer_stats_entry = Some(ts),
767 Err(err) => error!("Failed to get {} stats: {:#?}", &unit.name, err),
768 }
769 }
770
771 outcome
772 }
773 .instrument(span)
774 .await;
775
776 outcome.duration_ms = unit_collect_start.elapsed().as_secs_f64() * 1000.0;
777 outcome
778 });
779 }
780
781 let mut slowest_units: Vec<(String, f64)> = Vec::new();
782 while let Some(res) = join_set.join_next().await {
783 let outcome = match res {
784 Ok(outcome) => outcome,
785 Err(err) => {
786 error!("Per-unit collection task failed to join: {:?}", err);
787 continue;
788 }
789 };
790 if let Some(entry) = outcome.unit_states_entry {
791 stats.unit_states.insert(outcome.unit_name.clone(), entry);
792 }
793 if outcome.state_dbus_fetch {
794 state_dbus_fetches += 1;
795 }
796 if let Some(service_stats) = outcome.service_stats_entry {
797 stats
798 .service_stats
799 .insert(outcome.unit_name.clone(), service_stats);
800 service_dbus_fetches += 1;
801 }
802 if let Some(ts) = outcome.timer_stats_entry {
803 if ts.persistent {
804 stats.timer_persistent_units += 1;
805 }
806 if ts.remain_after_elapse {
807 stats.timer_remain_after_elapse += 1;
808 }
809 stats.timer_stats.insert(outcome.unit_name.clone(), ts);
810 timer_dbus_fetches += 1;
811 }
812 if config.units.slowest_units_count > 0 {
813 slowest_units.push((outcome.unit_name, outcome.duration_ms));
814 }
815 }
816
817 if config.units.slowest_units_count > 0 {
818 slowest_units.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
819 slowest_units.truncate(config.units.slowest_units_count as usize);
820 stats.collection_timings.slowest_units = slowest_units;
821 }
822
823 let per_unit_loop_elapsed = per_unit_loop_start.elapsed();
824 stats.collection_timings.per_unit_loop_ms = per_unit_loop_elapsed.as_secs_f64() * 1000.0;
825 stats.collection_timings.state_dbus_fetches = state_dbus_fetches;
826 stats.collection_timings.service_dbus_fetches = service_dbus_fetches;
827 stats.collection_timings.timer_dbus_fetches = timer_dbus_fetches;
828
829 debug!("unit stats: {:?}", stats);
830 Ok(stats)
831}
832
833pub async fn update_unit_stats(
843 config: Arc<crate::config::Config>,
844 connection: zbus::Connection,
845 locked_machine_stats: Arc<RwLock<MachineStats>>,
846 fs_root: String,
847) -> anyhow::Result<()> {
848 let units_stats = parse_unit_state(&config, &connection, &fs_root).await;
849 let mut machine_stats = locked_machine_stats.write().await;
850 match units_stats {
851 Ok(units_stats) => machine_stats.units = units_stats,
852 Err(err) => error!("units stats failed: {:?}", err),
853 }
854 Ok(())
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use std::collections::HashSet;
861 use strum::IntoEnumIterator;
862
863 fn get_unit_file() -> ListedUnit {
864 ListedUnit {
865 name: String::from("apport-autoreport.timer"),
866 description: String::from(
867 "Process error reports when automatic reporting is enabled (timer based)",
868 ),
869 load_state: String::from("loaded"),
870 active_state: String::from("inactive"),
871 sub_state: String::from("dead"),
872 follow_unit: String::from(""),
873 unit_object_path: ObjectPath::try_from(
874 "/org/freedesktop/systemd1/unit/apport_2dautoreport_2etimer",
875 )
876 .expect("Unable to make an object path")
877 .into(),
878 job_id: 0,
879 job_type: String::from(""),
880 job_object_path: ObjectPath::try_from("/").unwrap().into(),
881 }
882 }
883
884 #[tokio::test]
885 async fn test_state_parse() -> Result<(), MonitordUnitsError> {
886 let test_unit_name = String::from("apport-autoreport.timer");
887 let expected_stats = SystemdUnitStats {
888 activating_units: 0,
889 active_units: 0,
890 automount_units: 0,
891 device_units: 0,
892 failed_units: 0,
893 inactive_units: 0,
894 jobs_queued: 0,
895 loaded_units: 0,
896 masked_units: 0,
897 mount_units: 0,
898 not_found_units: 0,
899 path_units: 0,
900 scope_units: 0,
901 service_units: 0,
902 slice_units: 0,
903 socket_units: 0,
904 target_units: 0,
905 timer_units: 0,
906 timer_persistent_units: 0,
907 timer_remain_after_elapse: 0,
908 total_units: 0,
909 unit_files: UnitFilesStats::default(),
910 service_stats: HashMap::new(),
911 timer_stats: HashMap::new(),
912 unit_states: HashMap::from([(
913 test_unit_name.clone(),
914 UnitStates {
915 active_state: SystemdUnitActiveState::inactive,
916 load_state: SystemdUnitLoadState::loaded,
917 unhealthy: true,
918 time_in_state_usecs: None,
919 },
920 )]),
921 collection_timings: UnitsCollectionTimings::default(),
922 };
923 let mut stats = SystemdUnitStats::default();
924 let systemd_unit = get_unit_file();
925 let mut config = crate::config::UnitsConfig::default();
926
927 let (did_fetch, entry) = parse_state(&systemd_unit, &config, None).await?;
930 if let Some(entry) = entry {
931 stats.unit_states.insert(systemd_unit.name.clone(), entry);
932 }
933 assert_eq!(expected_stats, stats);
934 assert!(!did_fetch);
935
936 config.state_stats_allowlist = HashSet::from([test_unit_name.clone()]);
938
939 let mut allowlist_stats = SystemdUnitStats::default();
941 let (did_fetch, entry) = parse_state(&systemd_unit, &config, None).await?;
942 if let Some(entry) = entry {
943 allowlist_stats
944 .unit_states
945 .insert(systemd_unit.name.clone(), entry);
946 }
947 assert_eq!(expected_stats, allowlist_stats);
948 assert!(!did_fetch);
949
950 config.state_stats_blocklist = HashSet::from([test_unit_name]);
952
953 let mut blocklist_stats = SystemdUnitStats::default();
955 let expected_blocklist_stats = SystemdUnitStats::default();
956 let (did_fetch, entry) = parse_state(&systemd_unit, &config, None).await?;
957 if let Some(entry) = entry {
958 blocklist_stats
959 .unit_states
960 .insert(systemd_unit.name.clone(), entry);
961 }
962 assert_eq!(expected_blocklist_stats, blocklist_stats);
963 assert!(!did_fetch);
965 Ok(())
966 }
967
968 #[test]
969 fn test_unit_parse() {
970 let expected_stats = SystemdUnitStats {
971 activating_units: 0,
972 active_units: 0,
973 automount_units: 0,
974 device_units: 0,
975 failed_units: 0,
976 inactive_units: 1,
977 jobs_queued: 0,
978 loaded_units: 1,
979 masked_units: 0,
980 mount_units: 0,
981 not_found_units: 0,
982 path_units: 0,
983 scope_units: 0,
984 service_units: 0,
985 slice_units: 0,
986 socket_units: 0,
987 target_units: 0,
988 timer_units: 1,
989 timer_persistent_units: 0,
990 timer_remain_after_elapse: 0,
991 total_units: 0,
992 unit_files: UnitFilesStats::default(),
993 service_stats: HashMap::new(),
994 timer_stats: HashMap::new(),
995 unit_states: HashMap::new(),
996 collection_timings: UnitsCollectionTimings::default(),
997 };
998 let mut stats = SystemdUnitStats::default();
999 let systemd_unit = get_unit_file();
1000 parse_unit(&mut stats, &systemd_unit);
1001 assert_eq!(expected_stats, stats);
1002 }
1003
1004 #[test]
1005 fn test_unit_parse_activating() {
1006 let mut activating_unit = get_unit_file();
1007 activating_unit.active_state = String::from("activating");
1008 let mut stats = SystemdUnitStats::default();
1009 parse_unit(&mut stats, &activating_unit);
1010 assert_eq!(stats.activating_units, 1);
1011 assert_eq!(stats.active_units, 0);
1012 assert_eq!(stats.inactive_units, 0);
1013 }
1014
1015 #[test]
1016 fn test_iterators() {
1017 assert!(SystemdUnitActiveState::iter().collect::<Vec<_>>().len() > 0);
1018 assert!(SystemdUnitLoadState::iter().collect::<Vec<_>>().len() > 0);
1019 }
1020
1021 #[tokio::test]
1022 async fn test_count_unit_files_by_type() {
1023 let dir = tempfile::tempdir().expect("Unable to create temp dir");
1024 let path = dir.path();
1025
1026 std::fs::write(path.join("sshd.service"), "").unwrap();
1027 std::fs::write(path.join("nginx.service"), "").unwrap();
1028 std::fs::write(path.join("boot.mount"), "").unwrap();
1029 std::fs::write(path.join("swap.swap"), "").unwrap();
1030 std::fs::create_dir(path.join("multi-user.target.wants")).unwrap();
1031
1032 let counts = count_unit_files_by_type(path.to_str().unwrap()).await;
1033 assert_eq!(counts.get("service"), Some(&2));
1034 assert_eq!(counts.get("mount"), Some(&1));
1035 assert_eq!(counts.get("swap"), Some(&1));
1036 assert_eq!(counts.get("wants"), None);
1037 assert_eq!(counts.len(), 3);
1038 }
1039
1040 #[tokio::test]
1041 async fn test_count_unit_files_by_type_nonexistent_dir() {
1042 let counts = count_unit_files_by_type("/nonexistent/path").await;
1043 assert!(counts.is_empty());
1044 }
1045
1046 #[tokio::test]
1047 async fn test_collect_unit_files_stats_with_fs_root() {
1048 let root = tempfile::tempdir().expect("Unable to create temp dir");
1049 let root_path = root.path();
1050
1051 let gen_dir = root_path.join("run/systemd/generator");
1052 std::fs::create_dir_all(&gen_dir).unwrap();
1053 std::fs::write(gen_dir.join("boot.mount"), "").unwrap();
1054 std::fs::write(gen_dir.join("swap.swap"), "").unwrap();
1055
1056 let transient_dir = root_path.join("run/systemd/transient");
1057 std::fs::create_dir_all(&transient_dir).unwrap();
1058 std::fs::write(transient_dir.join("run-thing.service"), "").unwrap();
1059
1060 let user_transient = root_path.join("run/user/1000/systemd/transient");
1061 std::fs::create_dir_all(&user_transient).unwrap();
1062 std::fs::write(user_transient.join("app-code.scope"), "").unwrap();
1063 std::fs::write(user_transient.join("app-term.scope"), "").unwrap();
1064
1065 let stats = collect_unit_files_stats(root_path.to_str().unwrap()).await;
1066 assert_eq!(stats.root.generated.get("mount"), Some(&1));
1067 assert_eq!(stats.root.generated.get("swap"), Some(&1));
1068 assert_eq!(stats.root.transient.get("service"), Some(&1));
1069 assert_eq!(stats.user.transient.get("scope"), Some(&2));
1070 assert!(stats.user.generated.is_empty());
1071 }
1072}