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 ..Default::default()
386 }
387 }
388
389 #[tokio::test]
390 async fn test_parse_one_metric_unit_active_state() {
391 let mut stats = SystemdUnitStats::default();
392 let config = default_units_config();
393
394 let metric = ListOutput {
395 name: "io.systemd.Manager.UnitActiveState".to_string(),
396 value: string_value("active"),
397 object: Some("my-service.service".to_string()),
398 fields: None,
399 };
400
401 parse_one_metric(&mut stats, &metric, &config).unwrap();
402
403 assert_eq!(
404 stats
405 .unit_states
406 .get("my-service.service")
407 .unwrap()
408 .active_state,
409 SystemdUnitActiveState::active
410 );
411 }
412
413 #[tokio::test]
414 async fn test_parse_one_metric_unit_load_state() {
415 let mut stats = SystemdUnitStats::default();
416 let config = default_units_config();
417
418 let metric = ListOutput {
421 name: "io.systemd.Manager.UnitLoadState".to_string(),
422 value: string_value("not-found"),
423 object: Some("missing.service".to_string()),
424 fields: None,
425 };
426
427 parse_one_metric(&mut stats, &metric, &config).unwrap();
428
429 assert_eq!(
430 stats.unit_states.get("missing.service").unwrap().load_state,
431 SystemdUnitLoadState::not_found
432 );
433 }
434
435 #[tokio::test]
436 async fn test_parse_one_metric_nrestarts() {
437 let mut stats = SystemdUnitStats::default();
438 let config = default_units_config();
439
440 let metric = ListOutput {
441 name: "io.systemd.Manager.NRestarts".to_string(),
442 value: int_value(5),
443 object: Some("my-service.service".to_string()),
444 fields: None,
445 };
446
447 parse_one_metric(&mut stats, &metric, &config).unwrap();
448
449 assert_eq!(
450 stats
451 .service_stats
452 .get("my-service.service")
453 .unwrap()
454 .nrestarts,
455 5
456 );
457 }
458
459 #[tokio::test]
460 async fn test_parse_aggregated_metrics() {
461 let mut stats = SystemdUnitStats::default();
462 let config = default_units_config();
463
464 let type_metric = ListOutput {
466 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
467 value: int_value(42),
468 object: None,
469 fields: Some(std::collections::HashMap::from([(
470 "type".to_string(),
471 serde_json::json!("service"),
472 )])),
473 };
474 parse_one_metric(&mut stats, &type_metric, &config).unwrap();
475 assert_eq!(stats.service_units, 42);
476
477 let state_metric = ListOutput {
479 name: "io.systemd.Manager.UnitsByStateTotal".to_string(),
480 value: int_value(10),
481 object: None,
482 fields: Some(std::collections::HashMap::from([(
483 "state".to_string(),
484 serde_json::json!("active"),
485 )])),
486 };
487 parse_one_metric(&mut stats, &state_metric, &config).unwrap();
488 assert_eq!(stats.active_units, 10);
489 }
490
491 #[tokio::test]
492 async fn test_parse_multiple_units() {
493 let mut stats = SystemdUnitStats::default();
494 let config = default_units_config();
495
496 let metrics = vec![
497 ListOutput {
498 name: "io.systemd.Manager.UnitActiveState".to_string(),
499 value: string_value("active"),
500 object: Some("service1.service".to_string()),
501 fields: None,
502 },
503 ListOutput {
504 name: "io.systemd.Manager.UnitLoadState".to_string(),
505 value: string_value("loaded"),
506 object: Some("service1.service".to_string()),
507 fields: None,
508 },
509 ListOutput {
510 name: "io.systemd.Manager.UnitActiveState".to_string(),
511 value: string_value("failed"),
512 object: Some("service-2.service".to_string()),
513 fields: None,
514 },
515 ];
516
517 for metric in metrics {
518 parse_one_metric(&mut stats, &metric, &config).unwrap();
519 }
520
521 assert_eq!(stats.unit_states.len(), 2);
522 assert_eq!(
523 stats
524 .unit_states
525 .get("service1.service")
526 .unwrap()
527 .active_state,
528 SystemdUnitActiveState::active
529 );
530 assert_eq!(
531 stats
532 .unit_states
533 .get("service1.service")
534 .unwrap()
535 .load_state,
536 SystemdUnitLoadState::loaded
537 );
538 assert_eq!(
539 stats
540 .unit_states
541 .get("service-2.service")
542 .unwrap()
543 .active_state,
544 SystemdUnitActiveState::failed
545 );
546 }
547
548 #[tokio::test]
549 async fn test_parse_unknown_and_missing_values() {
550 let mut stats = SystemdUnitStats::default();
551 let config = default_units_config();
552
553 let metric1 = ListOutput {
555 name: "io.systemd.Manager.UnitActiveState".to_string(),
556 value: string_value("invalid_state"),
557 object: Some("test.service".to_string()),
558 fields: None,
559 };
560 parse_one_metric(&mut stats, &metric1, &config).unwrap();
561 assert!(
562 !stats.unit_states.contains_key("test.service"),
563 "invalid state should be skipped"
564 );
565
566 let metric2 = ListOutput {
568 name: "io.systemd.Manager.NRestarts".to_string(),
569 value: empty_value(),
570 object: Some("test2.service".to_string()),
571 fields: None,
572 };
573 parse_one_metric(&mut stats, &metric2, &config).unwrap();
574 assert!(
575 !stats.service_stats.contains_key("test2.service"),
576 "null value should be skipped"
577 );
578 }
579
580 #[tokio::test]
581 async fn test_parse_edge_cases() {
582 let mut stats = SystemdUnitStats::default();
583 let config = default_units_config();
584
585 let metric1 = ListOutput {
587 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
588 value: int_value(999),
589 object: None,
590 fields: Some(std::collections::HashMap::from([(
591 "type".to_string(),
592 serde_json::json!("unknown_type"),
593 )])),
594 };
595 parse_one_metric(&mut stats, &metric1, &config).unwrap();
596 assert_eq!(stats.service_units, 0);
597
598 let metric2 = ListOutput {
600 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
601 value: int_value(42),
602 object: None,
603 fields: None,
604 };
605 parse_one_metric(&mut stats, &metric2, &config).unwrap();
606
607 let metric3 = ListOutput {
609 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
610 value: int_value(42),
611 object: None,
612 fields: Some(std::collections::HashMap::from([(
613 "type".to_string(),
614 serde_json::json!(123),
615 )])),
616 };
617 parse_one_metric(&mut stats, &metric3, &config).unwrap();
618
619 let metric4 = ListOutput {
621 name: "io.systemd.Manager.UnknownMetric".to_string(),
622 value: int_value(999),
623 object: Some("test.service".to_string()),
624 fields: None,
625 };
626 parse_one_metric(&mut stats, &metric4, &config).unwrap();
627 }
628
629 #[test]
630 fn test_state_stats_disabled_skips_per_unit_data() {
631 let config = crate::config::UnitsConfig {
634 enabled: true,
635 state_stats: false,
636 state_stats_allowlist: HashSet::new(),
637 state_stats_blocklist: HashSet::new(),
638 state_stats_time_in_state: true,
639 ignore_inactive_oneshot_services: true,
640 unit_files: true,
641 ..Default::default()
642 };
643 let mut stats = SystemdUnitStats::default();
644
645 let active_state_metric = ListOutput {
646 name: "io.systemd.Manager.UnitActiveState".to_string(),
647 value: string_value("active"),
648 object: Some("test.service".to_string()),
649 fields: None,
650 };
651 parse_one_metric(&mut stats, &active_state_metric, &config).unwrap();
652
653 let load_state_metric = ListOutput {
654 name: "io.systemd.Manager.UnitLoadState".to_string(),
655 value: string_value("loaded"),
656 object: Some("test.service".to_string()),
657 fields: None,
658 };
659 parse_one_metric(&mut stats, &load_state_metric, &config).unwrap();
660
661 let nrestarts_metric = ListOutput {
662 name: "io.systemd.Manager.NRestarts".to_string(),
663 value: int_value(3),
664 object: Some("test.service".to_string()),
665 fields: None,
666 };
667 parse_one_metric(&mut stats, &nrestarts_metric, &config).unwrap();
668
669 assert_eq!(stats.unit_states.len(), 0);
671 assert_eq!(stats.service_stats.len(), 0);
672
673 let type_metric = ListOutput {
675 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
676 value: int_value(10),
677 object: None,
678 fields: Some(std::collections::HashMap::from([(
679 "type".to_string(),
680 serde_json::json!("service"),
681 )])),
682 };
683 parse_one_metric(&mut stats, &type_metric, &config).unwrap();
684 assert_eq!(stats.service_units, 10);
685 }
686
687 #[test]
688 fn test_parse_metric_enum() {
689 let metric_active = ListOutput {
690 name: "io.systemd.Manager.UnitActiveState".to_string(),
691 value: string_value("active"),
692 object: Some("test.service".to_string()),
693 fields: None,
694 };
695 assert_eq!(
696 parse_metric_enum::<SystemdUnitActiveState>(&metric_active),
697 Some(SystemdUnitActiveState::active)
698 );
699
700 let metric_loaded = ListOutput {
701 name: "io.systemd.Manager.UnitLoadState".to_string(),
702 value: string_value("loaded"),
703 object: Some("test.service".to_string()),
704 fields: None,
705 };
706 assert_eq!(
707 parse_metric_enum::<SystemdUnitLoadState>(&metric_loaded),
708 Some(SystemdUnitLoadState::loaded)
709 );
710
711 let metric_invalid = ListOutput {
713 name: "io.systemd.Manager.UnitActiveState".to_string(),
714 value: string_value("invalid"),
715 object: Some("test.service".to_string()),
716 fields: None,
717 };
718 assert_eq!(
719 parse_metric_enum::<SystemdUnitActiveState>(&metric_invalid),
720 None
721 );
722
723 let metric_empty = ListOutput {
725 name: "io.systemd.Manager.UnitActiveState".to_string(),
726 value: empty_value(),
727 object: Some("test.service".to_string()),
728 fields: None,
729 };
730 assert_eq!(
731 parse_metric_enum::<SystemdUnitActiveState>(&metric_empty),
732 None
733 );
734 }
735
736 #[test]
737 fn test_parse_metric_enum_all_states() {
738 let active_states = vec![
740 ("active", SystemdUnitActiveState::active),
741 ("reloading", SystemdUnitActiveState::reloading),
742 ("inactive", SystemdUnitActiveState::inactive),
743 ("failed", SystemdUnitActiveState::failed),
744 ("activating", SystemdUnitActiveState::activating),
745 ("deactivating", SystemdUnitActiveState::deactivating),
746 ];
747
748 for (state_str, expected) in active_states {
749 let metric = ListOutput {
750 name: "io.systemd.Manager.UnitActiveState".to_string(),
751 value: string_value(state_str),
752 object: Some("test.service".to_string()),
753 fields: None,
754 };
755 assert_eq!(
756 parse_metric_enum::<SystemdUnitActiveState>(&metric),
757 Some(expected)
758 );
759 }
760
761 let load_states = vec![
763 ("loaded", SystemdUnitLoadState::loaded),
764 ("error", SystemdUnitLoadState::error),
765 ("masked", SystemdUnitLoadState::masked),
766 ("not_found", SystemdUnitLoadState::not_found),
767 ];
768
769 for (state_str, expected) in load_states {
770 let metric = ListOutput {
771 name: "io.systemd.Manager.UnitLoadState".to_string(),
772 value: string_value(state_str),
773 object: Some("test.service".to_string()),
774 fields: None,
775 };
776 assert_eq!(
777 parse_metric_enum::<SystemdUnitLoadState>(&metric),
778 Some(expected)
779 );
780 }
781 }
782
783 #[tokio::test]
784 async fn test_parse_state_updates() {
785 let mut stats = SystemdUnitStats::default();
786 let config = default_units_config();
787
788 let metric1 = ListOutput {
790 name: "io.systemd.Manager.UnitActiveState".to_string(),
791 value: string_value("inactive"),
792 object: Some("test.service".to_string()),
793 fields: None,
794 };
795 parse_one_metric(&mut stats, &metric1, &config).unwrap();
796 assert_eq!(
797 stats.unit_states.get("test.service").unwrap().active_state,
798 SystemdUnitActiveState::inactive
799 );
800
801 let metric2 = ListOutput {
803 name: "io.systemd.Manager.UnitActiveState".to_string(),
804 value: string_value("active"),
805 object: Some("test.service".to_string()),
806 fields: None,
807 };
808 parse_one_metric(&mut stats, &metric2, &config).unwrap();
809 assert_eq!(
810 stats.unit_states.get("test.service").unwrap().active_state,
811 SystemdUnitActiveState::active
812 );
813 }
814
815 #[tokio::test]
816 async fn test_unhealthy_computed() {
817 let mut stats = SystemdUnitStats::default();
818 let config = default_units_config();
819
820 let metric1 = ListOutput {
822 name: "io.systemd.Manager.UnitActiveState".to_string(),
823 value: string_value("failed"),
824 object: Some("broken.service".to_string()),
825 fields: None,
826 };
827 parse_one_metric(&mut stats, &metric1, &config).unwrap();
828
829 let metric2 = ListOutput {
831 name: "io.systemd.Manager.UnitLoadState".to_string(),
832 value: string_value("loaded"),
833 object: Some("broken.service".to_string()),
834 fields: None,
835 };
836 parse_one_metric(&mut stats, &metric2, &config).unwrap();
837
838 assert!(stats.unit_states.get("broken.service").unwrap().unhealthy);
840
841 let metric3 = ListOutput {
843 name: "io.systemd.Manager.UnitActiveState".to_string(),
844 value: string_value("active"),
845 object: Some("healthy.service".to_string()),
846 fields: None,
847 };
848 parse_one_metric(&mut stats, &metric3, &config).unwrap();
849
850 let metric4 = ListOutput {
852 name: "io.systemd.Manager.UnitLoadState".to_string(),
853 value: string_value("loaded"),
854 object: Some("healthy.service".to_string()),
855 fields: None,
856 };
857 parse_one_metric(&mut stats, &metric4, &config).unwrap();
858
859 assert!(!stats.unit_states.get("healthy.service").unwrap().unhealthy);
861 }
862
863 #[tokio::test]
864 async fn test_parse_metrics_oneshot_inactive_not_unhealthy() {
865 let mut stats = SystemdUnitStats::default();
866 let config = default_units_config();
867 let metrics = vec![
868 ListOutput {
869 name: "io.systemd.Manager.UnitActiveState".to_string(),
870 value: string_value("inactive"),
871 object: Some("done.service".to_string()),
872 fields: None,
873 },
874 ListOutput {
875 name: "io.systemd.Manager.UnitLoadState".to_string(),
876 value: string_value("loaded"),
877 object: Some("done.service".to_string()),
878 fields: None,
879 },
880 ListOutput {
881 name: "io.systemd.Service.Type".to_string(),
882 value: string_value("oneshot"),
883 object: Some("done.service".to_string()),
884 fields: None,
885 },
886 ];
887
888 for metric in &metrics {
889 parse_one_metric(&mut stats, metric, &config).unwrap();
890 }
891 apply_oneshot_service_health_override(&mut stats, &metrics, &config);
892
893 assert!(!stats.unit_states.get("done.service").unwrap().unhealthy);
894 }
895
896 #[tokio::test]
897 async fn test_parse_metrics_oneshot_override_can_be_disabled() {
898 let mut stats = SystemdUnitStats::default();
899 let mut config = default_units_config();
900 config.ignore_inactive_oneshot_services = false;
901 let metrics = vec![
902 ListOutput {
903 name: "io.systemd.Manager.UnitActiveState".to_string(),
904 value: string_value("inactive"),
905 object: Some("done.service".to_string()),
906 fields: None,
907 },
908 ListOutput {
909 name: "io.systemd.Manager.UnitLoadState".to_string(),
910 value: string_value("loaded"),
911 object: Some("done.service".to_string()),
912 fields: None,
913 },
914 ListOutput {
915 name: "io.systemd.Service.Type".to_string(),
916 value: string_value("oneshot"),
917 object: Some("done.service".to_string()),
918 fields: None,
919 },
920 ];
921
922 for metric in &metrics {
923 parse_one_metric(&mut stats, metric, &config).unwrap();
924 }
925 apply_oneshot_service_health_override(&mut stats, &metrics, &config);
926
927 assert!(stats.unit_states.get("done.service").unwrap().unhealthy);
928 }
929
930 #[tokio::test]
931 async fn test_allowlist_filtering() {
932 let mut stats = SystemdUnitStats::default();
933 let config = crate::config::UnitsConfig {
934 enabled: true,
935 state_stats: true,
936 state_stats_allowlist: HashSet::from(["allowed.service".to_string()]),
937 state_stats_blocklist: HashSet::new(),
938 state_stats_time_in_state: false,
939 ignore_inactive_oneshot_services: true,
940 unit_files: true,
941 ..Default::default()
942 };
943
944 let metric1 = ListOutput {
946 name: "io.systemd.Manager.UnitActiveState".to_string(),
947 value: string_value("active"),
948 object: Some("allowed.service".to_string()),
949 fields: None,
950 };
951 parse_one_metric(&mut stats, &metric1, &config).unwrap();
952 assert!(stats.unit_states.contains_key("allowed.service"));
953
954 let metric2 = ListOutput {
956 name: "io.systemd.Manager.UnitActiveState".to_string(),
957 value: string_value("active"),
958 object: Some("not-allowed.service".to_string()),
959 fields: None,
960 };
961 parse_one_metric(&mut stats, &metric2, &config).unwrap();
962 assert!(!stats.unit_states.contains_key("not-allowed.service"));
963 }
964
965 #[tokio::test]
966 async fn test_blocklist_filtering() {
967 let mut stats = SystemdUnitStats::default();
968 let config = crate::config::UnitsConfig {
969 enabled: true,
970 state_stats: true,
971 state_stats_allowlist: HashSet::new(),
972 state_stats_blocklist: HashSet::from(["blocked.service".to_string()]),
973 state_stats_time_in_state: false,
974 ignore_inactive_oneshot_services: true,
975 unit_files: true,
976 ..Default::default()
977 };
978
979 let metric1 = ListOutput {
981 name: "io.systemd.Manager.UnitActiveState".to_string(),
982 value: string_value("active"),
983 object: Some("blocked.service".to_string()),
984 fields: None,
985 };
986 parse_one_metric(&mut stats, &metric1, &config).unwrap();
987 assert!(!stats.unit_states.contains_key("blocked.service"));
988
989 let metric2 = ListOutput {
991 name: "io.systemd.Manager.UnitActiveState".to_string(),
992 value: string_value("active"),
993 object: Some("ok.service".to_string()),
994 fields: None,
995 };
996 parse_one_metric(&mut stats, &metric2, &config).unwrap();
997 assert!(stats.unit_states.contains_key("ok.service"));
998 }
999
1000 #[tokio::test]
1001 async fn test_blocklist_overrides_allowlist() {
1002 let mut stats = SystemdUnitStats::default();
1003 let config = crate::config::UnitsConfig {
1004 enabled: true,
1005 state_stats: true,
1006 state_stats_allowlist: HashSet::from(["both.service".to_string()]),
1007 state_stats_blocklist: HashSet::from(["both.service".to_string()]),
1008 state_stats_time_in_state: false,
1009 ignore_inactive_oneshot_services: true,
1010 unit_files: true,
1011 ..Default::default()
1012 };
1013
1014 let metric = ListOutput {
1016 name: "io.systemd.Manager.UnitActiveState".to_string(),
1017 value: string_value("active"),
1018 object: Some("both.service".to_string()),
1019 fields: None,
1020 };
1021 parse_one_metric(&mut stats, &metric, &config).unwrap();
1022 assert!(!stats.unit_states.contains_key("both.service"));
1023 }
1024
1025 #[test]
1026 fn test_load_state_counts_bypass_allowlist() {
1027 let config = crate::config::UnitsConfig {
1030 enabled: true,
1031 state_stats: true,
1032 state_stats_allowlist: HashSet::from(["allowed.service".to_string()]),
1034 state_stats_blocklist: HashSet::new(),
1035 state_stats_time_in_state: false,
1036 ignore_inactive_oneshot_services: true,
1037 unit_files: true,
1038 ..Default::default()
1039 };
1040 let mut stats = SystemdUnitStats::default();
1041
1042 let metrics = vec![
1043 ListOutput {
1045 name: "io.systemd.Manager.UnitLoadState".to_string(),
1046 value: string_value("loaded"),
1047 object: Some("allowed.service".to_string()),
1048 fields: None,
1049 },
1050 ListOutput {
1052 name: "io.systemd.Manager.UnitLoadState".to_string(),
1053 value: string_value("loaded"),
1054 object: Some("other.service".to_string()),
1055 fields: None,
1056 },
1057 ListOutput {
1058 name: "io.systemd.Manager.UnitLoadState".to_string(),
1059 value: string_value("not-found"), object: Some("missing.service".to_string()),
1061 fields: None,
1062 },
1063 ListOutput {
1064 name: "io.systemd.Manager.UnitLoadState".to_string(),
1065 value: string_value("masked"),
1066 object: Some("masked.service".to_string()),
1067 fields: None,
1068 },
1069 ];
1070 for m in metrics {
1071 parse_one_metric(&mut stats, &m, &config).unwrap();
1072 }
1073
1074 assert_eq!(stats.loaded_units, 2);
1076 assert_eq!(stats.not_found_units, 1);
1077 assert_eq!(stats.masked_units, 1);
1078 assert_eq!(stats.unit_states.len(), 1);
1080 assert!(stats.unit_states.contains_key("allowed.service"));
1081 }
1082
1083 #[test]
1084 fn test_load_state_counts_when_state_stats_disabled() {
1085 let config = crate::config::UnitsConfig {
1087 enabled: true,
1088 state_stats: false,
1089 state_stats_allowlist: HashSet::new(),
1090 state_stats_blocklist: HashSet::new(),
1091 state_stats_time_in_state: false,
1092 ignore_inactive_oneshot_services: true,
1093 unit_files: true,
1094 ..Default::default()
1095 };
1096 let mut stats = SystemdUnitStats::default();
1097
1098 let metrics = vec![
1099 ListOutput {
1100 name: "io.systemd.Manager.UnitLoadState".to_string(),
1101 value: string_value("loaded"),
1102 object: Some("svc1.service".to_string()),
1103 fields: None,
1104 },
1105 ListOutput {
1106 name: "io.systemd.Manager.UnitLoadState".to_string(),
1107 value: string_value("not-found"), object: Some("svc2.service".to_string()),
1109 fields: None,
1110 },
1111 ];
1112 for m in metrics {
1113 parse_one_metric(&mut stats, &m, &config).unwrap();
1114 }
1115
1116 assert_eq!(stats.loaded_units, 1);
1117 assert_eq!(stats.not_found_units, 1);
1118 assert_eq!(stats.unit_states.len(), 0);
1120 }
1121}