1use std::collections::HashSet;
7use std::str::FromStr;
8use std::sync::Arc;
9use std::time::Instant;
10
11use tokio::sync::RwLock;
12use tracing::debug;
13
14use tracing::warn;
15
16use crate::unit_constants::{
17 is_unit_unhealthy, is_unit_unhealthy_for_service, SystemdUnitActiveState, SystemdUnitLoadState,
18 SYSTEMD_SERVICE_SUFFIX,
19};
20use crate::units::SystemdUnitStats;
21use crate::varlink::metrics::{ListOutput, Metrics};
22use crate::MachineStats;
23use futures_util::stream::TryStreamExt;
24use zlink::unix;
25
26pub const METRICS_SOCKET_PATH: &str = "/run/systemd/report/io.systemd.Manager";
27
28fn parse_metric_enum<T: FromStr>(metric: &ListOutput) -> Option<T> {
30 if !metric.value().is_string() {
31 warn!(
32 "Metric {} has non-string value: {:?}",
33 metric.name(),
34 metric.value()
35 );
36 return None;
37 }
38 let value_str = metric.value_as_string();
39 let normalized = value_str.replace('-', "_");
42 match T::from_str(&normalized) {
43 Ok(v) => Some(v),
44 Err(_) => {
45 warn!(
46 "Metric {} has unrecognized value: {:?}",
47 metric.name(),
48 value_str
49 );
50 None
51 }
52 }
53}
54
55fn should_skip_unit(object_name: &str, config: &crate::config::UnitsConfig) -> bool {
57 if config.state_stats_blocklist.contains(object_name) {
58 debug!("Skipping state stats for {} due to blocklist", object_name);
59 return true;
60 }
61 if !config.state_stats_allowlist.is_empty()
62 && !config.state_stats_allowlist.contains(object_name)
63 {
64 return true;
65 }
66 false
67}
68
69pub fn parse_one_metric(
71 stats: &mut SystemdUnitStats,
72 metric: &ListOutput,
73 config: &crate::config::UnitsConfig,
74) -> anyhow::Result<()> {
75 let metric_name_suffix = metric.name_suffix();
76 let object_name = metric.object_name();
77
78 match metric_name_suffix {
79 "UnitActiveState" => {
80 if !config.state_stats || should_skip_unit(&object_name, config) {
81 return Ok(());
82 }
83 let active_state: SystemdUnitActiveState = match parse_metric_enum(metric) {
84 Some(v) => v,
85 None => return Ok(()),
86 };
87 let unit_state = stats
88 .unit_states
89 .entry(object_name.to_string())
90 .or_default();
91 unit_state.active_state = active_state;
92 unit_state.unhealthy =
93 is_unit_unhealthy(unit_state.active_state, unit_state.load_state);
94 }
95 "UnitLoadState" => {
96 let load_state: SystemdUnitLoadState = match parse_metric_enum(metric) {
97 Some(v) => v,
98 None => return Ok(()),
99 };
100 match load_state {
103 SystemdUnitLoadState::loaded => stats.loaded_units += 1,
104 SystemdUnitLoadState::masked => stats.masked_units += 1,
105 SystemdUnitLoadState::not_found => stats.not_found_units += 1,
106 _ => {}
107 }
108 if !config.state_stats || should_skip_unit(&object_name, config) {
110 return Ok(());
111 }
112 let unit_state = stats
113 .unit_states
114 .entry(object_name.to_string())
115 .or_default();
116 unit_state.load_state = load_state;
117 unit_state.unhealthy =
118 is_unit_unhealthy(unit_state.active_state, unit_state.load_state);
119 }
120 "NRestarts" => {
121 if !config.state_stats || should_skip_unit(&object_name, config) {
122 return Ok(());
123 }
124 if !metric.value().is_i64() {
125 warn!(
126 "Metric {} has non-integer value: {:?}",
127 metric.name(),
128 metric.value()
129 );
130 return Ok(());
131 }
132 let value = metric.value_as_int();
133 let nrestarts: u32 = match value.try_into() {
134 Ok(v) => v,
135 Err(_) => {
136 warn!(
137 "Metric {} has out-of-range value for u32: {}",
138 metric.name(),
139 value
140 );
141 return Ok(());
142 }
143 };
144 stats
145 .service_stats
146 .entry(object_name.to_string())
147 .or_default()
148 .nrestarts = nrestarts;
149 }
150 "UnitsByTypeTotal" => {
151 if let Some(type_str) = metric.get_field_as_str("type") {
152 if !metric.value().is_i64() {
153 warn!(
154 "Metric {} has non-integer value: {:?}",
155 metric.name(),
156 metric.value()
157 );
158 return Ok(());
159 }
160 let value = metric.value_as_int();
161 let value: u64 = match value.try_into() {
162 Ok(v) => v,
163 Err(_) => {
164 warn!("Metric {} has negative value: {}", metric.name(), value);
165 return Ok(());
166 }
167 };
168 match type_str {
169 "automount" => stats.automount_units = value,
170 "device" => stats.device_units = value,
171 "mount" => stats.mount_units = value,
172 "path" => stats.path_units = value,
173 "scope" => stats.scope_units = value,
174 "service" => stats.service_units = value,
175 "slice" => stats.slice_units = value,
176 "socket" => stats.socket_units = value,
177 "target" => stats.target_units = value,
178 "timer" => stats.timer_units = value,
179 _ => debug!("Found unhandled unit type: {:?}", type_str),
180 }
181 }
182 }
183 "UnitsByStateTotal" => {
184 if let Some(state_str) = metric.get_field_as_str("state") {
185 if !metric.value().is_i64() {
186 warn!(
187 "Metric {} has non-integer value: {:?}",
188 metric.name(),
189 metric.value()
190 );
191 return Ok(());
192 }
193 let value = metric.value_as_int();
194 let value: u64 = match value.try_into() {
195 Ok(v) => v,
196 Err(_) => {
197 warn!("Metric {} has negative value: {}", metric.name(), value);
198 return Ok(());
199 }
200 };
201 match state_str {
202 "active" => stats.active_units = value,
203 "failed" => stats.failed_units = value,
204 "inactive" => stats.inactive_units = value,
205 _ => debug!("Found unhandled unit state: {:?}", state_str),
206 }
207 }
208 }
209 _ => debug!("Found unhandled metric: {:?}", metric.name()),
210 }
211
212 Ok(())
213}
214
215async fn collect_metrics(socket_path: String) -> anyhow::Result<Vec<ListOutput>> {
219 tokio::task::spawn_blocking(move || {
220 let rt = tokio::runtime::Builder::new_current_thread()
221 .enable_all()
222 .build()?;
223 rt.block_on(async move {
224 let mut conn = unix::connect(&socket_path).await?;
225 let stream = conn.list().await?;
226 futures_util::pin_mut!(stream);
227
228 let mut metrics = Vec::new();
229 let mut count = 0;
230 while let Some(result) = stream.try_next().await? {
231 let result: std::result::Result<ListOutput, _> = result;
232 match result {
233 Ok(metric) => {
234 debug!("Metrics {}: {:?}", count, metric);
235 count += 1;
236 metrics.push(metric);
237 }
238 Err(e) => {
239 debug!("Error deserializing metric {}: {:?}", count, e);
240 return Err(anyhow::anyhow!(e));
241 }
242 }
243 }
244 Ok(metrics)
245 })
246 })
247 .await?
248}
249
250pub async fn parse_metrics(
251 stats: &mut SystemdUnitStats,
252 socket_path: &str,
253 config: &crate::config::UnitsConfig,
254) -> anyhow::Result<()> {
255 let bulk_fetch_start = Instant::now();
260 let metrics = collect_metrics(socket_path.to_string()).await?;
261 let bulk_fetch_elapsed = bulk_fetch_start.elapsed();
262 stats.collection_timings.list_units_ms = bulk_fetch_elapsed.as_secs_f64() * 1000.0;
263
264 let parse_loop_start = Instant::now();
265 for metric in &metrics {
266 parse_one_metric(stats, metric, config)?;
267 }
268 apply_oneshot_service_health_override(stats, &metrics, config);
269 let parse_loop_elapsed = parse_loop_start.elapsed();
270 stats.collection_timings.per_unit_loop_ms = parse_loop_elapsed.as_secs_f64() * 1000.0;
271
272 Ok(())
273}
274
275fn apply_oneshot_service_health_override(
276 stats: &mut SystemdUnitStats,
277 metrics: &[ListOutput],
278 config: &crate::config::UnitsConfig,
279) {
280 if !config.ignore_inactive_oneshot_services {
281 return;
282 }
283 let mut oneshot_service_names = HashSet::new();
284 for metric in metrics {
285 if metric.name_suffix() != "Type" || !metric.value().is_string() {
286 continue;
287 }
288 let object_name = metric.object_name();
289 if !object_name.ends_with(SYSTEMD_SERVICE_SUFFIX) {
290 continue;
291 }
292 if metric.value_as_string() == "oneshot" {
293 oneshot_service_names.insert(object_name);
294 }
295 }
296 for (unit_name, unit_state) in stats.unit_states.iter_mut() {
297 unit_state.unhealthy = is_unit_unhealthy_for_service(
298 unit_state.active_state,
299 unit_state.load_state,
300 oneshot_service_names.contains(unit_name),
301 config.ignore_inactive_oneshot_services,
302 );
303 }
304}
305
306pub async fn get_unit_stats(
307 config: &crate::config::Config,
308 socket_path: &str,
309) -> anyhow::Result<SystemdUnitStats> {
310 if !config.units.state_stats_allowlist.is_empty() {
311 debug!(
312 "Using unit state allowlist: {:?}",
313 config.units.state_stats_allowlist
314 );
315 }
316
317 if !config.units.state_stats_blocklist.is_empty() {
318 debug!(
319 "Using unit state blocklist: {:?}",
320 config.units.state_stats_blocklist,
321 );
322 }
323
324 let mut stats = SystemdUnitStats::default();
325
326 parse_metrics(&mut stats, socket_path, &config.units).await?;
329
330 stats.total_units = stats.automount_units
333 + stats.device_units
334 + stats.mount_units
335 + stats.path_units
336 + stats.scope_units
337 + stats.service_units
338 + stats.slice_units
339 + stats.socket_units
340 + stats.target_units
341 + stats.timer_units;
342
343 debug!("unit stats: {:?}", stats);
344 Ok(stats)
345}
346
347pub async fn update_unit_stats(
349 config: Arc<crate::config::Config>,
350 locked_machine_stats: Arc<RwLock<MachineStats>>,
351 socket_path: String,
352) -> anyhow::Result<()> {
353 let units_stats = get_unit_stats(&config, &socket_path).await?;
354 let mut machine_stats = locked_machine_stats.write().await;
355 machine_stats.units = units_stats;
356 Ok(())
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use std::collections::HashSet;
363
364 fn string_value(s: &str) -> serde_json::Value {
365 serde_json::json!(s)
366 }
367
368 fn int_value(i: i64) -> serde_json::Value {
369 serde_json::json!(i)
370 }
371
372 fn empty_value() -> serde_json::Value {
373 serde_json::Value::Null
374 }
375
376 fn default_units_config() -> crate::config::UnitsConfig {
377 crate::config::UnitsConfig {
378 enabled: true,
379 state_stats: true,
380 state_stats_allowlist: HashSet::new(),
381 state_stats_blocklist: HashSet::new(),
382 state_stats_time_in_state: false,
383 ignore_inactive_oneshot_services: true,
384 unit_files: true,
385 }
386 }
387
388 #[tokio::test]
389 async fn test_parse_one_metric_unit_active_state() {
390 let mut stats = SystemdUnitStats::default();
391 let config = default_units_config();
392
393 let metric = ListOutput {
394 name: "io.systemd.Manager.UnitActiveState".to_string(),
395 value: string_value("active"),
396 object: Some("my-service.service".to_string()),
397 fields: None,
398 };
399
400 parse_one_metric(&mut stats, &metric, &config).unwrap();
401
402 assert_eq!(
403 stats
404 .unit_states
405 .get("my-service.service")
406 .unwrap()
407 .active_state,
408 SystemdUnitActiveState::active
409 );
410 }
411
412 #[tokio::test]
413 async fn test_parse_one_metric_unit_load_state() {
414 let mut stats = SystemdUnitStats::default();
415 let config = default_units_config();
416
417 let metric = ListOutput {
420 name: "io.systemd.Manager.UnitLoadState".to_string(),
421 value: string_value("not-found"),
422 object: Some("missing.service".to_string()),
423 fields: None,
424 };
425
426 parse_one_metric(&mut stats, &metric, &config).unwrap();
427
428 assert_eq!(
429 stats.unit_states.get("missing.service").unwrap().load_state,
430 SystemdUnitLoadState::not_found
431 );
432 }
433
434 #[tokio::test]
435 async fn test_parse_one_metric_nrestarts() {
436 let mut stats = SystemdUnitStats::default();
437 let config = default_units_config();
438
439 let metric = ListOutput {
440 name: "io.systemd.Manager.NRestarts".to_string(),
441 value: int_value(5),
442 object: Some("my-service.service".to_string()),
443 fields: None,
444 };
445
446 parse_one_metric(&mut stats, &metric, &config).unwrap();
447
448 assert_eq!(
449 stats
450 .service_stats
451 .get("my-service.service")
452 .unwrap()
453 .nrestarts,
454 5
455 );
456 }
457
458 #[tokio::test]
459 async fn test_parse_aggregated_metrics() {
460 let mut stats = SystemdUnitStats::default();
461 let config = default_units_config();
462
463 let type_metric = ListOutput {
465 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
466 value: int_value(42),
467 object: None,
468 fields: Some(std::collections::HashMap::from([(
469 "type".to_string(),
470 serde_json::json!("service"),
471 )])),
472 };
473 parse_one_metric(&mut stats, &type_metric, &config).unwrap();
474 assert_eq!(stats.service_units, 42);
475
476 let state_metric = ListOutput {
478 name: "io.systemd.Manager.UnitsByStateTotal".to_string(),
479 value: int_value(10),
480 object: None,
481 fields: Some(std::collections::HashMap::from([(
482 "state".to_string(),
483 serde_json::json!("active"),
484 )])),
485 };
486 parse_one_metric(&mut stats, &state_metric, &config).unwrap();
487 assert_eq!(stats.active_units, 10);
488 }
489
490 #[tokio::test]
491 async fn test_parse_multiple_units() {
492 let mut stats = SystemdUnitStats::default();
493 let config = default_units_config();
494
495 let metrics = vec![
496 ListOutput {
497 name: "io.systemd.Manager.UnitActiveState".to_string(),
498 value: string_value("active"),
499 object: Some("service1.service".to_string()),
500 fields: None,
501 },
502 ListOutput {
503 name: "io.systemd.Manager.UnitLoadState".to_string(),
504 value: string_value("loaded"),
505 object: Some("service1.service".to_string()),
506 fields: None,
507 },
508 ListOutput {
509 name: "io.systemd.Manager.UnitActiveState".to_string(),
510 value: string_value("failed"),
511 object: Some("service-2.service".to_string()),
512 fields: None,
513 },
514 ];
515
516 for metric in metrics {
517 parse_one_metric(&mut stats, &metric, &config).unwrap();
518 }
519
520 assert_eq!(stats.unit_states.len(), 2);
521 assert_eq!(
522 stats
523 .unit_states
524 .get("service1.service")
525 .unwrap()
526 .active_state,
527 SystemdUnitActiveState::active
528 );
529 assert_eq!(
530 stats
531 .unit_states
532 .get("service1.service")
533 .unwrap()
534 .load_state,
535 SystemdUnitLoadState::loaded
536 );
537 assert_eq!(
538 stats
539 .unit_states
540 .get("service-2.service")
541 .unwrap()
542 .active_state,
543 SystemdUnitActiveState::failed
544 );
545 }
546
547 #[tokio::test]
548 async fn test_parse_unknown_and_missing_values() {
549 let mut stats = SystemdUnitStats::default();
550 let config = default_units_config();
551
552 let metric1 = ListOutput {
554 name: "io.systemd.Manager.UnitActiveState".to_string(),
555 value: string_value("invalid_state"),
556 object: Some("test.service".to_string()),
557 fields: None,
558 };
559 parse_one_metric(&mut stats, &metric1, &config).unwrap();
560 assert!(
561 !stats.unit_states.contains_key("test.service"),
562 "invalid state should be skipped"
563 );
564
565 let metric2 = ListOutput {
567 name: "io.systemd.Manager.NRestarts".to_string(),
568 value: empty_value(),
569 object: Some("test2.service".to_string()),
570 fields: None,
571 };
572 parse_one_metric(&mut stats, &metric2, &config).unwrap();
573 assert!(
574 !stats.service_stats.contains_key("test2.service"),
575 "null value should be skipped"
576 );
577 }
578
579 #[tokio::test]
580 async fn test_parse_edge_cases() {
581 let mut stats = SystemdUnitStats::default();
582 let config = default_units_config();
583
584 let metric1 = ListOutput {
586 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
587 value: int_value(999),
588 object: None,
589 fields: Some(std::collections::HashMap::from([(
590 "type".to_string(),
591 serde_json::json!("unknown_type"),
592 )])),
593 };
594 parse_one_metric(&mut stats, &metric1, &config).unwrap();
595 assert_eq!(stats.service_units, 0);
596
597 let metric2 = ListOutput {
599 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
600 value: int_value(42),
601 object: None,
602 fields: None,
603 };
604 parse_one_metric(&mut stats, &metric2, &config).unwrap();
605
606 let metric3 = ListOutput {
608 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
609 value: int_value(42),
610 object: None,
611 fields: Some(std::collections::HashMap::from([(
612 "type".to_string(),
613 serde_json::json!(123),
614 )])),
615 };
616 parse_one_metric(&mut stats, &metric3, &config).unwrap();
617
618 let metric4 = ListOutput {
620 name: "io.systemd.Manager.UnknownMetric".to_string(),
621 value: int_value(999),
622 object: Some("test.service".to_string()),
623 fields: None,
624 };
625 parse_one_metric(&mut stats, &metric4, &config).unwrap();
626 }
627
628 #[test]
629 fn test_state_stats_disabled_skips_per_unit_data() {
630 let config = crate::config::UnitsConfig {
633 enabled: true,
634 state_stats: false,
635 state_stats_allowlist: HashSet::new(),
636 state_stats_blocklist: HashSet::new(),
637 state_stats_time_in_state: true,
638 ignore_inactive_oneshot_services: true,
639 unit_files: true,
640 };
641 let mut stats = SystemdUnitStats::default();
642
643 let active_state_metric = ListOutput {
644 name: "io.systemd.Manager.UnitActiveState".to_string(),
645 value: string_value("active"),
646 object: Some("test.service".to_string()),
647 fields: None,
648 };
649 parse_one_metric(&mut stats, &active_state_metric, &config).unwrap();
650
651 let load_state_metric = ListOutput {
652 name: "io.systemd.Manager.UnitLoadState".to_string(),
653 value: string_value("loaded"),
654 object: Some("test.service".to_string()),
655 fields: None,
656 };
657 parse_one_metric(&mut stats, &load_state_metric, &config).unwrap();
658
659 let nrestarts_metric = ListOutput {
660 name: "io.systemd.Manager.NRestarts".to_string(),
661 value: int_value(3),
662 object: Some("test.service".to_string()),
663 fields: None,
664 };
665 parse_one_metric(&mut stats, &nrestarts_metric, &config).unwrap();
666
667 assert_eq!(stats.unit_states.len(), 0);
669 assert_eq!(stats.service_stats.len(), 0);
670
671 let type_metric = ListOutput {
673 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
674 value: int_value(10),
675 object: None,
676 fields: Some(std::collections::HashMap::from([(
677 "type".to_string(),
678 serde_json::json!("service"),
679 )])),
680 };
681 parse_one_metric(&mut stats, &type_metric, &config).unwrap();
682 assert_eq!(stats.service_units, 10);
683 }
684
685 #[test]
686 fn test_parse_metric_enum() {
687 let metric_active = ListOutput {
688 name: "io.systemd.Manager.UnitActiveState".to_string(),
689 value: string_value("active"),
690 object: Some("test.service".to_string()),
691 fields: None,
692 };
693 assert_eq!(
694 parse_metric_enum::<SystemdUnitActiveState>(&metric_active),
695 Some(SystemdUnitActiveState::active)
696 );
697
698 let metric_loaded = ListOutput {
699 name: "io.systemd.Manager.UnitLoadState".to_string(),
700 value: string_value("loaded"),
701 object: Some("test.service".to_string()),
702 fields: None,
703 };
704 assert_eq!(
705 parse_metric_enum::<SystemdUnitLoadState>(&metric_loaded),
706 Some(SystemdUnitLoadState::loaded)
707 );
708
709 let metric_invalid = ListOutput {
711 name: "io.systemd.Manager.UnitActiveState".to_string(),
712 value: string_value("invalid"),
713 object: Some("test.service".to_string()),
714 fields: None,
715 };
716 assert_eq!(
717 parse_metric_enum::<SystemdUnitActiveState>(&metric_invalid),
718 None
719 );
720
721 let metric_empty = ListOutput {
723 name: "io.systemd.Manager.UnitActiveState".to_string(),
724 value: empty_value(),
725 object: Some("test.service".to_string()),
726 fields: None,
727 };
728 assert_eq!(
729 parse_metric_enum::<SystemdUnitActiveState>(&metric_empty),
730 None
731 );
732 }
733
734 #[test]
735 fn test_parse_metric_enum_all_states() {
736 let active_states = vec![
738 ("active", SystemdUnitActiveState::active),
739 ("reloading", SystemdUnitActiveState::reloading),
740 ("inactive", SystemdUnitActiveState::inactive),
741 ("failed", SystemdUnitActiveState::failed),
742 ("activating", SystemdUnitActiveState::activating),
743 ("deactivating", SystemdUnitActiveState::deactivating),
744 ];
745
746 for (state_str, expected) in active_states {
747 let metric = ListOutput {
748 name: "io.systemd.Manager.UnitActiveState".to_string(),
749 value: string_value(state_str),
750 object: Some("test.service".to_string()),
751 fields: None,
752 };
753 assert_eq!(
754 parse_metric_enum::<SystemdUnitActiveState>(&metric),
755 Some(expected)
756 );
757 }
758
759 let load_states = vec![
761 ("loaded", SystemdUnitLoadState::loaded),
762 ("error", SystemdUnitLoadState::error),
763 ("masked", SystemdUnitLoadState::masked),
764 ("not_found", SystemdUnitLoadState::not_found),
765 ];
766
767 for (state_str, expected) in load_states {
768 let metric = ListOutput {
769 name: "io.systemd.Manager.UnitLoadState".to_string(),
770 value: string_value(state_str),
771 object: Some("test.service".to_string()),
772 fields: None,
773 };
774 assert_eq!(
775 parse_metric_enum::<SystemdUnitLoadState>(&metric),
776 Some(expected)
777 );
778 }
779 }
780
781 #[tokio::test]
782 async fn test_parse_state_updates() {
783 let mut stats = SystemdUnitStats::default();
784 let config = default_units_config();
785
786 let metric1 = ListOutput {
788 name: "io.systemd.Manager.UnitActiveState".to_string(),
789 value: string_value("inactive"),
790 object: Some("test.service".to_string()),
791 fields: None,
792 };
793 parse_one_metric(&mut stats, &metric1, &config).unwrap();
794 assert_eq!(
795 stats.unit_states.get("test.service").unwrap().active_state,
796 SystemdUnitActiveState::inactive
797 );
798
799 let metric2 = ListOutput {
801 name: "io.systemd.Manager.UnitActiveState".to_string(),
802 value: string_value("active"),
803 object: Some("test.service".to_string()),
804 fields: None,
805 };
806 parse_one_metric(&mut stats, &metric2, &config).unwrap();
807 assert_eq!(
808 stats.unit_states.get("test.service").unwrap().active_state,
809 SystemdUnitActiveState::active
810 );
811 }
812
813 #[tokio::test]
814 async fn test_unhealthy_computed() {
815 let mut stats = SystemdUnitStats::default();
816 let config = default_units_config();
817
818 let metric1 = ListOutput {
820 name: "io.systemd.Manager.UnitActiveState".to_string(),
821 value: string_value("failed"),
822 object: Some("broken.service".to_string()),
823 fields: None,
824 };
825 parse_one_metric(&mut stats, &metric1, &config).unwrap();
826
827 let metric2 = ListOutput {
829 name: "io.systemd.Manager.UnitLoadState".to_string(),
830 value: string_value("loaded"),
831 object: Some("broken.service".to_string()),
832 fields: None,
833 };
834 parse_one_metric(&mut stats, &metric2, &config).unwrap();
835
836 assert!(stats.unit_states.get("broken.service").unwrap().unhealthy);
838
839 let metric3 = ListOutput {
841 name: "io.systemd.Manager.UnitActiveState".to_string(),
842 value: string_value("active"),
843 object: Some("healthy.service".to_string()),
844 fields: None,
845 };
846 parse_one_metric(&mut stats, &metric3, &config).unwrap();
847
848 let metric4 = ListOutput {
850 name: "io.systemd.Manager.UnitLoadState".to_string(),
851 value: string_value("loaded"),
852 object: Some("healthy.service".to_string()),
853 fields: None,
854 };
855 parse_one_metric(&mut stats, &metric4, &config).unwrap();
856
857 assert!(!stats.unit_states.get("healthy.service").unwrap().unhealthy);
859 }
860
861 #[tokio::test]
862 async fn test_parse_metrics_oneshot_inactive_not_unhealthy() {
863 let mut stats = SystemdUnitStats::default();
864 let config = default_units_config();
865 let metrics = vec![
866 ListOutput {
867 name: "io.systemd.Manager.UnitActiveState".to_string(),
868 value: string_value("inactive"),
869 object: Some("done.service".to_string()),
870 fields: None,
871 },
872 ListOutput {
873 name: "io.systemd.Manager.UnitLoadState".to_string(),
874 value: string_value("loaded"),
875 object: Some("done.service".to_string()),
876 fields: None,
877 },
878 ListOutput {
879 name: "io.systemd.Service.Type".to_string(),
880 value: string_value("oneshot"),
881 object: Some("done.service".to_string()),
882 fields: None,
883 },
884 ];
885
886 for metric in &metrics {
887 parse_one_metric(&mut stats, metric, &config).unwrap();
888 }
889 apply_oneshot_service_health_override(&mut stats, &metrics, &config);
890
891 assert!(!stats.unit_states.get("done.service").unwrap().unhealthy);
892 }
893
894 #[tokio::test]
895 async fn test_parse_metrics_oneshot_override_can_be_disabled() {
896 let mut stats = SystemdUnitStats::default();
897 let mut config = default_units_config();
898 config.ignore_inactive_oneshot_services = false;
899 let metrics = vec![
900 ListOutput {
901 name: "io.systemd.Manager.UnitActiveState".to_string(),
902 value: string_value("inactive"),
903 object: Some("done.service".to_string()),
904 fields: None,
905 },
906 ListOutput {
907 name: "io.systemd.Manager.UnitLoadState".to_string(),
908 value: string_value("loaded"),
909 object: Some("done.service".to_string()),
910 fields: None,
911 },
912 ListOutput {
913 name: "io.systemd.Service.Type".to_string(),
914 value: string_value("oneshot"),
915 object: Some("done.service".to_string()),
916 fields: None,
917 },
918 ];
919
920 for metric in &metrics {
921 parse_one_metric(&mut stats, metric, &config).unwrap();
922 }
923 apply_oneshot_service_health_override(&mut stats, &metrics, &config);
924
925 assert!(stats.unit_states.get("done.service").unwrap().unhealthy);
926 }
927
928 #[tokio::test]
929 async fn test_allowlist_filtering() {
930 let mut stats = SystemdUnitStats::default();
931 let config = crate::config::UnitsConfig {
932 enabled: true,
933 state_stats: true,
934 state_stats_allowlist: HashSet::from(["allowed.service".to_string()]),
935 state_stats_blocklist: HashSet::new(),
936 state_stats_time_in_state: false,
937 ignore_inactive_oneshot_services: true,
938 unit_files: true,
939 };
940
941 let metric1 = ListOutput {
943 name: "io.systemd.Manager.UnitActiveState".to_string(),
944 value: string_value("active"),
945 object: Some("allowed.service".to_string()),
946 fields: None,
947 };
948 parse_one_metric(&mut stats, &metric1, &config).unwrap();
949 assert!(stats.unit_states.contains_key("allowed.service"));
950
951 let metric2 = ListOutput {
953 name: "io.systemd.Manager.UnitActiveState".to_string(),
954 value: string_value("active"),
955 object: Some("not-allowed.service".to_string()),
956 fields: None,
957 };
958 parse_one_metric(&mut stats, &metric2, &config).unwrap();
959 assert!(!stats.unit_states.contains_key("not-allowed.service"));
960 }
961
962 #[tokio::test]
963 async fn test_blocklist_filtering() {
964 let mut stats = SystemdUnitStats::default();
965 let config = crate::config::UnitsConfig {
966 enabled: true,
967 state_stats: true,
968 state_stats_allowlist: HashSet::new(),
969 state_stats_blocklist: HashSet::from(["blocked.service".to_string()]),
970 state_stats_time_in_state: false,
971 ignore_inactive_oneshot_services: true,
972 unit_files: true,
973 };
974
975 let metric1 = ListOutput {
977 name: "io.systemd.Manager.UnitActiveState".to_string(),
978 value: string_value("active"),
979 object: Some("blocked.service".to_string()),
980 fields: None,
981 };
982 parse_one_metric(&mut stats, &metric1, &config).unwrap();
983 assert!(!stats.unit_states.contains_key("blocked.service"));
984
985 let metric2 = ListOutput {
987 name: "io.systemd.Manager.UnitActiveState".to_string(),
988 value: string_value("active"),
989 object: Some("ok.service".to_string()),
990 fields: None,
991 };
992 parse_one_metric(&mut stats, &metric2, &config).unwrap();
993 assert!(stats.unit_states.contains_key("ok.service"));
994 }
995
996 #[tokio::test]
997 async fn test_blocklist_overrides_allowlist() {
998 let mut stats = SystemdUnitStats::default();
999 let config = crate::config::UnitsConfig {
1000 enabled: true,
1001 state_stats: true,
1002 state_stats_allowlist: HashSet::from(["both.service".to_string()]),
1003 state_stats_blocklist: HashSet::from(["both.service".to_string()]),
1004 state_stats_time_in_state: false,
1005 ignore_inactive_oneshot_services: true,
1006 unit_files: true,
1007 };
1008
1009 let metric = ListOutput {
1011 name: "io.systemd.Manager.UnitActiveState".to_string(),
1012 value: string_value("active"),
1013 object: Some("both.service".to_string()),
1014 fields: None,
1015 };
1016 parse_one_metric(&mut stats, &metric, &config).unwrap();
1017 assert!(!stats.unit_states.contains_key("both.service"));
1018 }
1019
1020 #[test]
1021 fn test_load_state_counts_bypass_allowlist() {
1022 let config = crate::config::UnitsConfig {
1025 enabled: true,
1026 state_stats: true,
1027 state_stats_allowlist: HashSet::from(["allowed.service".to_string()]),
1029 state_stats_blocklist: HashSet::new(),
1030 state_stats_time_in_state: false,
1031 ignore_inactive_oneshot_services: true,
1032 unit_files: true,
1033 };
1034 let mut stats = SystemdUnitStats::default();
1035
1036 let metrics = vec![
1037 ListOutput {
1039 name: "io.systemd.Manager.UnitLoadState".to_string(),
1040 value: string_value("loaded"),
1041 object: Some("allowed.service".to_string()),
1042 fields: None,
1043 },
1044 ListOutput {
1046 name: "io.systemd.Manager.UnitLoadState".to_string(),
1047 value: string_value("loaded"),
1048 object: Some("other.service".to_string()),
1049 fields: None,
1050 },
1051 ListOutput {
1052 name: "io.systemd.Manager.UnitLoadState".to_string(),
1053 value: string_value("not-found"), object: Some("missing.service".to_string()),
1055 fields: None,
1056 },
1057 ListOutput {
1058 name: "io.systemd.Manager.UnitLoadState".to_string(),
1059 value: string_value("masked"),
1060 object: Some("masked.service".to_string()),
1061 fields: None,
1062 },
1063 ];
1064 for m in metrics {
1065 parse_one_metric(&mut stats, &m, &config).unwrap();
1066 }
1067
1068 assert_eq!(stats.loaded_units, 2);
1070 assert_eq!(stats.not_found_units, 1);
1071 assert_eq!(stats.masked_units, 1);
1072 assert_eq!(stats.unit_states.len(), 1);
1074 assert!(stats.unit_states.contains_key("allowed.service"));
1075 }
1076
1077 #[test]
1078 fn test_load_state_counts_when_state_stats_disabled() {
1079 let config = crate::config::UnitsConfig {
1081 enabled: true,
1082 state_stats: false,
1083 state_stats_allowlist: HashSet::new(),
1084 state_stats_blocklist: HashSet::new(),
1085 state_stats_time_in_state: false,
1086 ignore_inactive_oneshot_services: true,
1087 unit_files: true,
1088 };
1089 let mut stats = SystemdUnitStats::default();
1090
1091 let metrics = vec![
1092 ListOutput {
1093 name: "io.systemd.Manager.UnitLoadState".to_string(),
1094 value: string_value("loaded"),
1095 object: Some("svc1.service".to_string()),
1096 fields: None,
1097 },
1098 ListOutput {
1099 name: "io.systemd.Manager.UnitLoadState".to_string(),
1100 value: string_value("not-found"), object: Some("svc2.service".to_string()),
1102 fields: None,
1103 },
1104 ];
1105 for m in metrics {
1106 parse_one_metric(&mut stats, &m, &config).unwrap();
1107 }
1108
1109 assert_eq!(stats.loaded_units, 1);
1110 assert_eq!(stats.not_found_units, 1);
1111 assert_eq!(stats.unit_states.len(), 0);
1113 }
1114}