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