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 }
348 }
349
350 #[tokio::test]
351 async fn test_parse_one_metric_unit_active_state() {
352 let mut stats = SystemdUnitStats::default();
353 let config = default_units_config();
354
355 let metric = ListOutput {
356 name: "io.systemd.Manager.UnitActiveState".to_string(),
357 value: string_value("active"),
358 object: Some("my-service.service".to_string()),
359 fields: None,
360 };
361
362 parse_one_metric(&mut stats, &metric, &config).unwrap();
363
364 assert_eq!(
365 stats
366 .unit_states
367 .get("my-service.service")
368 .unwrap()
369 .active_state,
370 SystemdUnitActiveState::active
371 );
372 }
373
374 #[tokio::test]
375 async fn test_parse_one_metric_unit_load_state() {
376 let mut stats = SystemdUnitStats::default();
377 let config = default_units_config();
378
379 let metric = ListOutput {
382 name: "io.systemd.Manager.UnitLoadState".to_string(),
383 value: string_value("not-found"),
384 object: Some("missing.service".to_string()),
385 fields: None,
386 };
387
388 parse_one_metric(&mut stats, &metric, &config).unwrap();
389
390 assert_eq!(
391 stats.unit_states.get("missing.service").unwrap().load_state,
392 SystemdUnitLoadState::not_found
393 );
394 }
395
396 #[tokio::test]
397 async fn test_parse_one_metric_nrestarts() {
398 let mut stats = SystemdUnitStats::default();
399 let config = default_units_config();
400
401 let metric = ListOutput {
402 name: "io.systemd.Manager.NRestarts".to_string(),
403 value: int_value(5),
404 object: Some("my-service.service".to_string()),
405 fields: None,
406 };
407
408 parse_one_metric(&mut stats, &metric, &config).unwrap();
409
410 assert_eq!(
411 stats
412 .service_stats
413 .get("my-service.service")
414 .unwrap()
415 .nrestarts,
416 5
417 );
418 }
419
420 #[tokio::test]
421 async fn test_parse_aggregated_metrics() {
422 let mut stats = SystemdUnitStats::default();
423 let config = default_units_config();
424
425 let type_metric = ListOutput {
427 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
428 value: int_value(42),
429 object: None,
430 fields: Some(std::collections::HashMap::from([(
431 "type".to_string(),
432 serde_json::json!("service"),
433 )])),
434 };
435 parse_one_metric(&mut stats, &type_metric, &config).unwrap();
436 assert_eq!(stats.service_units, 42);
437
438 let state_metric = ListOutput {
440 name: "io.systemd.Manager.UnitsByStateTotal".to_string(),
441 value: int_value(10),
442 object: None,
443 fields: Some(std::collections::HashMap::from([(
444 "state".to_string(),
445 serde_json::json!("active"),
446 )])),
447 };
448 parse_one_metric(&mut stats, &state_metric, &config).unwrap();
449 assert_eq!(stats.active_units, 10);
450 }
451
452 #[tokio::test]
453 async fn test_parse_multiple_units() {
454 let mut stats = SystemdUnitStats::default();
455 let config = default_units_config();
456
457 let metrics = vec![
458 ListOutput {
459 name: "io.systemd.Manager.UnitActiveState".to_string(),
460 value: string_value("active"),
461 object: Some("service1.service".to_string()),
462 fields: None,
463 },
464 ListOutput {
465 name: "io.systemd.Manager.UnitLoadState".to_string(),
466 value: string_value("loaded"),
467 object: Some("service1.service".to_string()),
468 fields: None,
469 },
470 ListOutput {
471 name: "io.systemd.Manager.UnitActiveState".to_string(),
472 value: string_value("failed"),
473 object: Some("service-2.service".to_string()),
474 fields: None,
475 },
476 ];
477
478 for metric in metrics {
479 parse_one_metric(&mut stats, &metric, &config).unwrap();
480 }
481
482 assert_eq!(stats.unit_states.len(), 2);
483 assert_eq!(
484 stats
485 .unit_states
486 .get("service1.service")
487 .unwrap()
488 .active_state,
489 SystemdUnitActiveState::active
490 );
491 assert_eq!(
492 stats
493 .unit_states
494 .get("service1.service")
495 .unwrap()
496 .load_state,
497 SystemdUnitLoadState::loaded
498 );
499 assert_eq!(
500 stats
501 .unit_states
502 .get("service-2.service")
503 .unwrap()
504 .active_state,
505 SystemdUnitActiveState::failed
506 );
507 }
508
509 #[tokio::test]
510 async fn test_parse_unknown_and_missing_values() {
511 let mut stats = SystemdUnitStats::default();
512 let config = default_units_config();
513
514 let metric1 = ListOutput {
516 name: "io.systemd.Manager.UnitActiveState".to_string(),
517 value: string_value("invalid_state"),
518 object: Some("test.service".to_string()),
519 fields: None,
520 };
521 parse_one_metric(&mut stats, &metric1, &config).unwrap();
522 assert!(
523 !stats.unit_states.contains_key("test.service"),
524 "invalid state should be skipped"
525 );
526
527 let metric2 = ListOutput {
529 name: "io.systemd.Manager.NRestarts".to_string(),
530 value: empty_value(),
531 object: Some("test2.service".to_string()),
532 fields: None,
533 };
534 parse_one_metric(&mut stats, &metric2, &config).unwrap();
535 assert!(
536 !stats.service_stats.contains_key("test2.service"),
537 "null value should be skipped"
538 );
539 }
540
541 #[tokio::test]
542 async fn test_parse_edge_cases() {
543 let mut stats = SystemdUnitStats::default();
544 let config = default_units_config();
545
546 let metric1 = ListOutput {
548 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
549 value: int_value(999),
550 object: None,
551 fields: Some(std::collections::HashMap::from([(
552 "type".to_string(),
553 serde_json::json!("unknown_type"),
554 )])),
555 };
556 parse_one_metric(&mut stats, &metric1, &config).unwrap();
557 assert_eq!(stats.service_units, 0);
558
559 let metric2 = ListOutput {
561 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
562 value: int_value(42),
563 object: None,
564 fields: None,
565 };
566 parse_one_metric(&mut stats, &metric2, &config).unwrap();
567
568 let metric3 = ListOutput {
570 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
571 value: int_value(42),
572 object: None,
573 fields: Some(std::collections::HashMap::from([(
574 "type".to_string(),
575 serde_json::json!(123),
576 )])),
577 };
578 parse_one_metric(&mut stats, &metric3, &config).unwrap();
579
580 let metric4 = ListOutput {
582 name: "io.systemd.Manager.UnknownMetric".to_string(),
583 value: int_value(999),
584 object: Some("test.service".to_string()),
585 fields: None,
586 };
587 parse_one_metric(&mut stats, &metric4, &config).unwrap();
588 }
589
590 #[test]
591 fn test_state_stats_disabled_skips_per_unit_data() {
592 let config = crate::config::UnitsConfig {
595 enabled: true,
596 state_stats: false,
597 state_stats_allowlist: HashSet::new(),
598 state_stats_blocklist: HashSet::new(),
599 state_stats_time_in_state: true,
600 };
601 let mut stats = SystemdUnitStats::default();
602
603 let active_state_metric = ListOutput {
604 name: "io.systemd.Manager.UnitActiveState".to_string(),
605 value: string_value("active"),
606 object: Some("test.service".to_string()),
607 fields: None,
608 };
609 parse_one_metric(&mut stats, &active_state_metric, &config).unwrap();
610
611 let load_state_metric = ListOutput {
612 name: "io.systemd.Manager.UnitLoadState".to_string(),
613 value: string_value("loaded"),
614 object: Some("test.service".to_string()),
615 fields: None,
616 };
617 parse_one_metric(&mut stats, &load_state_metric, &config).unwrap();
618
619 let nrestarts_metric = ListOutput {
620 name: "io.systemd.Manager.NRestarts".to_string(),
621 value: int_value(3),
622 object: Some("test.service".to_string()),
623 fields: None,
624 };
625 parse_one_metric(&mut stats, &nrestarts_metric, &config).unwrap();
626
627 assert_eq!(stats.unit_states.len(), 0);
629 assert_eq!(stats.service_stats.len(), 0);
630
631 let type_metric = ListOutput {
633 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
634 value: int_value(10),
635 object: None,
636 fields: Some(std::collections::HashMap::from([(
637 "type".to_string(),
638 serde_json::json!("service"),
639 )])),
640 };
641 parse_one_metric(&mut stats, &type_metric, &config).unwrap();
642 assert_eq!(stats.service_units, 10);
643 }
644
645 #[test]
646 fn test_parse_metric_enum() {
647 let metric_active = ListOutput {
648 name: "io.systemd.Manager.UnitActiveState".to_string(),
649 value: string_value("active"),
650 object: Some("test.service".to_string()),
651 fields: None,
652 };
653 assert_eq!(
654 parse_metric_enum::<SystemdUnitActiveState>(&metric_active),
655 Some(SystemdUnitActiveState::active)
656 );
657
658 let metric_loaded = ListOutput {
659 name: "io.systemd.Manager.UnitLoadState".to_string(),
660 value: string_value("loaded"),
661 object: Some("test.service".to_string()),
662 fields: None,
663 };
664 assert_eq!(
665 parse_metric_enum::<SystemdUnitLoadState>(&metric_loaded),
666 Some(SystemdUnitLoadState::loaded)
667 );
668
669 let metric_invalid = ListOutput {
671 name: "io.systemd.Manager.UnitActiveState".to_string(),
672 value: string_value("invalid"),
673 object: Some("test.service".to_string()),
674 fields: None,
675 };
676 assert_eq!(
677 parse_metric_enum::<SystemdUnitActiveState>(&metric_invalid),
678 None
679 );
680
681 let metric_empty = ListOutput {
683 name: "io.systemd.Manager.UnitActiveState".to_string(),
684 value: empty_value(),
685 object: Some("test.service".to_string()),
686 fields: None,
687 };
688 assert_eq!(
689 parse_metric_enum::<SystemdUnitActiveState>(&metric_empty),
690 None
691 );
692 }
693
694 #[test]
695 fn test_parse_metric_enum_all_states() {
696 let active_states = vec![
698 ("active", SystemdUnitActiveState::active),
699 ("reloading", SystemdUnitActiveState::reloading),
700 ("inactive", SystemdUnitActiveState::inactive),
701 ("failed", SystemdUnitActiveState::failed),
702 ("activating", SystemdUnitActiveState::activating),
703 ("deactivating", SystemdUnitActiveState::deactivating),
704 ];
705
706 for (state_str, expected) in active_states {
707 let metric = ListOutput {
708 name: "io.systemd.Manager.UnitActiveState".to_string(),
709 value: string_value(state_str),
710 object: Some("test.service".to_string()),
711 fields: None,
712 };
713 assert_eq!(
714 parse_metric_enum::<SystemdUnitActiveState>(&metric),
715 Some(expected)
716 );
717 }
718
719 let load_states = vec![
721 ("loaded", SystemdUnitLoadState::loaded),
722 ("error", SystemdUnitLoadState::error),
723 ("masked", SystemdUnitLoadState::masked),
724 ("not_found", SystemdUnitLoadState::not_found),
725 ];
726
727 for (state_str, expected) in load_states {
728 let metric = ListOutput {
729 name: "io.systemd.Manager.UnitLoadState".to_string(),
730 value: string_value(state_str),
731 object: Some("test.service".to_string()),
732 fields: None,
733 };
734 assert_eq!(
735 parse_metric_enum::<SystemdUnitLoadState>(&metric),
736 Some(expected)
737 );
738 }
739 }
740
741 #[tokio::test]
742 async fn test_parse_state_updates() {
743 let mut stats = SystemdUnitStats::default();
744 let config = default_units_config();
745
746 let metric1 = ListOutput {
748 name: "io.systemd.Manager.UnitActiveState".to_string(),
749 value: string_value("inactive"),
750 object: Some("test.service".to_string()),
751 fields: None,
752 };
753 parse_one_metric(&mut stats, &metric1, &config).unwrap();
754 assert_eq!(
755 stats.unit_states.get("test.service").unwrap().active_state,
756 SystemdUnitActiveState::inactive
757 );
758
759 let metric2 = ListOutput {
761 name: "io.systemd.Manager.UnitActiveState".to_string(),
762 value: string_value("active"),
763 object: Some("test.service".to_string()),
764 fields: None,
765 };
766 parse_one_metric(&mut stats, &metric2, &config).unwrap();
767 assert_eq!(
768 stats.unit_states.get("test.service").unwrap().active_state,
769 SystemdUnitActiveState::active
770 );
771 }
772
773 #[tokio::test]
774 async fn test_unhealthy_computed() {
775 let mut stats = SystemdUnitStats::default();
776 let config = default_units_config();
777
778 let metric1 = ListOutput {
780 name: "io.systemd.Manager.UnitActiveState".to_string(),
781 value: string_value("failed"),
782 object: Some("broken.service".to_string()),
783 fields: None,
784 };
785 parse_one_metric(&mut stats, &metric1, &config).unwrap();
786
787 let metric2 = ListOutput {
789 name: "io.systemd.Manager.UnitLoadState".to_string(),
790 value: string_value("loaded"),
791 object: Some("broken.service".to_string()),
792 fields: None,
793 };
794 parse_one_metric(&mut stats, &metric2, &config).unwrap();
795
796 assert!(stats.unit_states.get("broken.service").unwrap().unhealthy);
798
799 let metric3 = ListOutput {
801 name: "io.systemd.Manager.UnitActiveState".to_string(),
802 value: string_value("active"),
803 object: Some("healthy.service".to_string()),
804 fields: None,
805 };
806 parse_one_metric(&mut stats, &metric3, &config).unwrap();
807
808 let metric4 = ListOutput {
810 name: "io.systemd.Manager.UnitLoadState".to_string(),
811 value: string_value("loaded"),
812 object: Some("healthy.service".to_string()),
813 fields: None,
814 };
815 parse_one_metric(&mut stats, &metric4, &config).unwrap();
816
817 assert!(!stats.unit_states.get("healthy.service").unwrap().unhealthy);
819 }
820
821 #[tokio::test]
822 async fn test_allowlist_filtering() {
823 let mut stats = SystemdUnitStats::default();
824 let config = crate::config::UnitsConfig {
825 enabled: true,
826 state_stats: true,
827 state_stats_allowlist: HashSet::from(["allowed.service".to_string()]),
828 state_stats_blocklist: HashSet::new(),
829 state_stats_time_in_state: false,
830 };
831
832 let metric1 = ListOutput {
834 name: "io.systemd.Manager.UnitActiveState".to_string(),
835 value: string_value("active"),
836 object: Some("allowed.service".to_string()),
837 fields: None,
838 };
839 parse_one_metric(&mut stats, &metric1, &config).unwrap();
840 assert!(stats.unit_states.contains_key("allowed.service"));
841
842 let metric2 = ListOutput {
844 name: "io.systemd.Manager.UnitActiveState".to_string(),
845 value: string_value("active"),
846 object: Some("not-allowed.service".to_string()),
847 fields: None,
848 };
849 parse_one_metric(&mut stats, &metric2, &config).unwrap();
850 assert!(!stats.unit_states.contains_key("not-allowed.service"));
851 }
852
853 #[tokio::test]
854 async fn test_blocklist_filtering() {
855 let mut stats = SystemdUnitStats::default();
856 let config = crate::config::UnitsConfig {
857 enabled: true,
858 state_stats: true,
859 state_stats_allowlist: HashSet::new(),
860 state_stats_blocklist: HashSet::from(["blocked.service".to_string()]),
861 state_stats_time_in_state: false,
862 };
863
864 let metric1 = ListOutput {
866 name: "io.systemd.Manager.UnitActiveState".to_string(),
867 value: string_value("active"),
868 object: Some("blocked.service".to_string()),
869 fields: None,
870 };
871 parse_one_metric(&mut stats, &metric1, &config).unwrap();
872 assert!(!stats.unit_states.contains_key("blocked.service"));
873
874 let metric2 = ListOutput {
876 name: "io.systemd.Manager.UnitActiveState".to_string(),
877 value: string_value("active"),
878 object: Some("ok.service".to_string()),
879 fields: None,
880 };
881 parse_one_metric(&mut stats, &metric2, &config).unwrap();
882 assert!(stats.unit_states.contains_key("ok.service"));
883 }
884
885 #[tokio::test]
886 async fn test_blocklist_overrides_allowlist() {
887 let mut stats = SystemdUnitStats::default();
888 let config = crate::config::UnitsConfig {
889 enabled: true,
890 state_stats: true,
891 state_stats_allowlist: HashSet::from(["both.service".to_string()]),
892 state_stats_blocklist: HashSet::from(["both.service".to_string()]),
893 state_stats_time_in_state: false,
894 };
895
896 let metric = ListOutput {
898 name: "io.systemd.Manager.UnitActiveState".to_string(),
899 value: string_value("active"),
900 object: Some("both.service".to_string()),
901 fields: None,
902 };
903 parse_one_metric(&mut stats, &metric, &config).unwrap();
904 assert!(!stats.unit_states.contains_key("both.service"));
905 }
906
907 #[test]
908 fn test_load_state_counts_bypass_allowlist() {
909 let config = crate::config::UnitsConfig {
912 enabled: true,
913 state_stats: true,
914 state_stats_allowlist: HashSet::from(["allowed.service".to_string()]),
916 state_stats_blocklist: HashSet::new(),
917 state_stats_time_in_state: false,
918 };
919 let mut stats = SystemdUnitStats::default();
920
921 let metrics = vec![
922 ListOutput {
924 name: "io.systemd.Manager.UnitLoadState".to_string(),
925 value: string_value("loaded"),
926 object: Some("allowed.service".to_string()),
927 fields: None,
928 },
929 ListOutput {
931 name: "io.systemd.Manager.UnitLoadState".to_string(),
932 value: string_value("loaded"),
933 object: Some("other.service".to_string()),
934 fields: None,
935 },
936 ListOutput {
937 name: "io.systemd.Manager.UnitLoadState".to_string(),
938 value: string_value("not-found"), object: Some("missing.service".to_string()),
940 fields: None,
941 },
942 ListOutput {
943 name: "io.systemd.Manager.UnitLoadState".to_string(),
944 value: string_value("masked"),
945 object: Some("masked.service".to_string()),
946 fields: None,
947 },
948 ];
949 for m in metrics {
950 parse_one_metric(&mut stats, &m, &config).unwrap();
951 }
952
953 assert_eq!(stats.loaded_units, 2);
955 assert_eq!(stats.not_found_units, 1);
956 assert_eq!(stats.masked_units, 1);
957 assert_eq!(stats.unit_states.len(), 1);
959 assert!(stats.unit_states.contains_key("allowed.service"));
960 }
961
962 #[test]
963 fn test_load_state_counts_when_state_stats_disabled() {
964 let config = crate::config::UnitsConfig {
966 enabled: true,
967 state_stats: false,
968 state_stats_allowlist: HashSet::new(),
969 state_stats_blocklist: HashSet::new(),
970 state_stats_time_in_state: false,
971 };
972 let mut stats = SystemdUnitStats::default();
973
974 let metrics = vec![
975 ListOutput {
976 name: "io.systemd.Manager.UnitLoadState".to_string(),
977 value: string_value("loaded"),
978 object: Some("svc1.service".to_string()),
979 fields: None,
980 },
981 ListOutput {
982 name: "io.systemd.Manager.UnitLoadState".to_string(),
983 value: string_value("not-found"), object: Some("svc2.service".to_string()),
985 fields: None,
986 },
987 ];
988 for m in metrics {
989 parse_one_metric(&mut stats, &m, &config).unwrap();
990 }
991
992 assert_eq!(stats.loaded_units, 1);
993 assert_eq!(stats.not_found_units, 1);
994 assert_eq!(stats.unit_states.len(), 0);
996 }
997}