1use std::str::FromStr;
7use std::sync::Arc;
8
9use tokio::sync::RwLock;
10use tracing::debug;
11
12use tracing::warn;
13
14use crate::unit_constants::{is_unit_unhealthy, SystemdUnitActiveState, SystemdUnitLoadState};
15use crate::units::SystemdUnitStats;
16use crate::varlink::metrics::{ListOutput, Metrics};
17use crate::MachineStats;
18use futures_util::stream::TryStreamExt;
19use zlink::unix;
20
21pub const METRICS_SOCKET_PATH: &str = "/run/systemd/report/io.systemd.Manager";
22
23fn parse_metric_enum<T: FromStr>(metric: &ListOutput) -> Option<T> {
25 if !metric.value().is_string() {
26 warn!(
27 "Metric {} has non-string value: {:?}",
28 metric.name(),
29 metric.value()
30 );
31 return None;
32 }
33 let value_str = metric.value_as_string();
34 match T::from_str(value_str) {
35 Ok(v) => Some(v),
36 Err(_) => {
37 warn!(
38 "Metric {} has unrecognized value: {:?}",
39 metric.name(),
40 value_str
41 );
42 None
43 }
44 }
45}
46
47fn should_skip_unit(object_name: &str, config: &crate::config::UnitsConfig) -> bool {
49 if config.state_stats_blocklist.contains(object_name) {
50 debug!("Skipping state stats for {} due to blocklist", object_name);
51 return true;
52 }
53 if !config.state_stats_allowlist.is_empty()
54 && !config.state_stats_allowlist.contains(object_name)
55 {
56 return true;
57 }
58 false
59}
60
61pub fn parse_one_metric(
63 stats: &mut SystemdUnitStats,
64 metric: &ListOutput,
65 config: &crate::config::UnitsConfig,
66) -> anyhow::Result<()> {
67 let metric_name_suffix = metric.name_suffix();
68 let object_name = metric.object_name();
69
70 match metric_name_suffix {
71 "UnitActiveState" => {
72 if should_skip_unit(&object_name, config) {
73 return Ok(());
74 }
75 let active_state: SystemdUnitActiveState = match parse_metric_enum(metric) {
76 Some(v) => v,
77 None => return Ok(()),
78 };
79 let unit_state = stats
80 .unit_states
81 .entry(object_name.to_string())
82 .or_default();
83 unit_state.active_state = active_state;
84 unit_state.unhealthy =
85 is_unit_unhealthy(unit_state.active_state, unit_state.load_state);
86 }
87 "UnitLoadState" => {
88 if should_skip_unit(&object_name, config) {
89 return Ok(());
90 }
91 let load_state: SystemdUnitLoadState = match parse_metric_enum(metric) {
92 Some(v) => v,
93 None => return Ok(()),
94 };
95 let unit_state = stats
96 .unit_states
97 .entry(object_name.to_string())
98 .or_default();
99 unit_state.load_state = load_state;
100 unit_state.unhealthy =
101 is_unit_unhealthy(unit_state.active_state, unit_state.load_state);
102 }
103 "NRestarts" => {
104 if should_skip_unit(&object_name, config) {
105 return Ok(());
106 }
107 if !metric.value().is_i64() {
108 warn!(
109 "Metric {} has non-integer value: {:?}",
110 metric.name(),
111 metric.value()
112 );
113 return Ok(());
114 }
115 let value = metric.value_as_int();
116 let nrestarts: u32 = match value.try_into() {
117 Ok(v) => v,
118 Err(_) => {
119 warn!(
120 "Metric {} has out-of-range value for u32: {}",
121 metric.name(),
122 value
123 );
124 return Ok(());
125 }
126 };
127 stats
128 .service_stats
129 .entry(object_name.to_string())
130 .or_default()
131 .nrestarts = nrestarts;
132 }
133 "UnitsByTypeTotal" => {
134 if let Some(type_str) = metric.get_field_as_str("type") {
135 if !metric.value().is_i64() {
136 warn!(
137 "Metric {} has non-integer value: {:?}",
138 metric.name(),
139 metric.value()
140 );
141 return Ok(());
142 }
143 let value = metric.value_as_int();
144 let value: u64 = match value.try_into() {
145 Ok(v) => v,
146 Err(_) => {
147 warn!("Metric {} has negative value: {}", metric.name(), value);
148 return Ok(());
149 }
150 };
151 match type_str {
152 "automount" => stats.automount_units = value,
153 "device" => stats.device_units = value,
154 "mount" => stats.mount_units = value,
155 "path" => stats.path_units = value,
156 "scope" => stats.scope_units = value,
157 "service" => stats.service_units = value,
158 "slice" => stats.slice_units = value,
159 "socket" => stats.socket_units = value,
160 "target" => stats.target_units = value,
161 "timer" => stats.timer_units = value,
162 _ => debug!("Found unhandled unit type: {:?}", type_str),
163 }
164 }
165 }
166 "UnitsByStateTotal" => {
167 if let Some(state_str) = metric.get_field_as_str("state") {
168 if !metric.value().is_i64() {
169 warn!(
170 "Metric {} has non-integer value: {:?}",
171 metric.name(),
172 metric.value()
173 );
174 return Ok(());
175 }
176 let value = metric.value_as_int();
177 let value: u64 = match value.try_into() {
178 Ok(v) => v,
179 Err(_) => {
180 warn!("Metric {} has negative value: {}", metric.name(), value);
181 return Ok(());
182 }
183 };
184 match state_str {
185 "active" => stats.active_units = value,
186 "failed" => stats.failed_units = value,
187 "inactive" => stats.inactive_units = value,
188 _ => debug!("Found unhandled unit state: {:?}", state_str),
189 }
190 }
191 }
192 _ => debug!("Found unhandled metric: {:?}", metric.name()),
193 }
194
195 Ok(())
196}
197
198async fn collect_metrics(socket_path: String) -> anyhow::Result<Vec<ListOutput>> {
202 tokio::task::spawn_blocking(move || {
203 let rt = tokio::runtime::Builder::new_current_thread()
204 .enable_all()
205 .build()?;
206 rt.block_on(async move {
207 let mut conn = unix::connect(&socket_path).await?;
208 let stream = conn.list().await?;
209 futures_util::pin_mut!(stream);
210
211 let mut metrics = Vec::new();
212 let mut count = 0;
213 while let Some(result) = stream.try_next().await? {
214 let result: std::result::Result<ListOutput, _> = result;
215 match result {
216 Ok(metric) => {
217 debug!("Metrics {}: {:?}", count, metric);
218 count += 1;
219 metrics.push(metric);
220 }
221 Err(e) => {
222 debug!("Error deserializing metric {}: {:?}", count, e);
223 return Err(anyhow::anyhow!(e));
224 }
225 }
226 }
227 Ok(metrics)
228 })
229 })
230 .await?
231}
232
233pub async fn parse_metrics(
234 stats: &mut SystemdUnitStats,
235 socket_path: &str,
236 config: &crate::config::UnitsConfig,
237) -> anyhow::Result<()> {
238 let metrics = collect_metrics(socket_path.to_string()).await?;
239
240 for metric in &metrics {
241 parse_one_metric(stats, metric, config)?;
242 }
243
244 Ok(())
245}
246
247pub async fn get_unit_stats(
248 config: &crate::config::Config,
249 socket_path: &str,
250) -> anyhow::Result<SystemdUnitStats> {
251 if !config.units.state_stats_allowlist.is_empty() {
252 debug!(
253 "Using unit state allowlist: {:?}",
254 config.units.state_stats_allowlist
255 );
256 }
257
258 if !config.units.state_stats_blocklist.is_empty() {
259 debug!(
260 "Using unit state blocklist: {:?}",
261 config.units.state_stats_blocklist,
262 );
263 }
264
265 let mut stats = SystemdUnitStats::default();
266
267 if config.units.state_stats {
269 parse_metrics(&mut stats, socket_path, &config.units).await?;
270 }
271
272 debug!("unit stats: {:?}", stats);
273 Ok(stats)
274}
275
276pub async fn update_unit_stats(
278 config: Arc<crate::config::Config>,
279 locked_machine_stats: Arc<RwLock<MachineStats>>,
280 socket_path: String,
281) -> anyhow::Result<()> {
282 let units_stats = get_unit_stats(&config, &socket_path).await?;
283 let mut machine_stats = locked_machine_stats.write().await;
284 machine_stats.units = units_stats;
285 Ok(())
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use std::collections::HashSet;
292
293 fn string_value(s: &str) -> serde_json::Value {
294 serde_json::json!(s)
295 }
296
297 fn int_value(i: i64) -> serde_json::Value {
298 serde_json::json!(i)
299 }
300
301 fn empty_value() -> serde_json::Value {
302 serde_json::Value::Null
303 }
304
305 fn default_units_config() -> crate::config::UnitsConfig {
306 crate::config::UnitsConfig {
307 enabled: true,
308 state_stats: true,
309 state_stats_allowlist: HashSet::new(),
310 state_stats_blocklist: HashSet::new(),
311 state_stats_time_in_state: false,
312 }
313 }
314
315 #[tokio::test]
316 async fn test_parse_one_metric_unit_active_state() {
317 let mut stats = SystemdUnitStats::default();
318 let config = default_units_config();
319
320 let metric = ListOutput {
321 name: "io.systemd.Manager.UnitActiveState".to_string(),
322 value: string_value("active"),
323 object: Some("my-service.service".to_string()),
324 fields: None,
325 };
326
327 parse_one_metric(&mut stats, &metric, &config).unwrap();
328
329 assert_eq!(
330 stats
331 .unit_states
332 .get("my-service.service")
333 .unwrap()
334 .active_state,
335 SystemdUnitActiveState::active
336 );
337 }
338
339 #[tokio::test]
340 async fn test_parse_one_metric_unit_load_state() {
341 let mut stats = SystemdUnitStats::default();
342 let config = default_units_config();
343
344 let metric = ListOutput {
345 name: "io.systemd.Manager.UnitLoadState".to_string(),
346 value: string_value("not_found"), object: Some("missing.service".to_string()),
348 fields: None,
349 };
350
351 parse_one_metric(&mut stats, &metric, &config).unwrap();
352
353 assert_eq!(
354 stats.unit_states.get("missing.service").unwrap().load_state,
355 SystemdUnitLoadState::not_found
356 );
357 }
358
359 #[tokio::test]
360 async fn test_parse_one_metric_nrestarts() {
361 let mut stats = SystemdUnitStats::default();
362 let config = default_units_config();
363
364 let metric = ListOutput {
365 name: "io.systemd.Manager.NRestarts".to_string(),
366 value: int_value(5),
367 object: Some("my-service.service".to_string()),
368 fields: None,
369 };
370
371 parse_one_metric(&mut stats, &metric, &config).unwrap();
372
373 assert_eq!(
374 stats
375 .service_stats
376 .get("my-service.service")
377 .unwrap()
378 .nrestarts,
379 5
380 );
381 }
382
383 #[tokio::test]
384 async fn test_parse_aggregated_metrics() {
385 let mut stats = SystemdUnitStats::default();
386 let config = default_units_config();
387
388 let type_metric = ListOutput {
390 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
391 value: int_value(42),
392 object: None,
393 fields: Some(std::collections::HashMap::from([(
394 "type".to_string(),
395 serde_json::json!("service"),
396 )])),
397 };
398 parse_one_metric(&mut stats, &type_metric, &config).unwrap();
399 assert_eq!(stats.service_units, 42);
400
401 let state_metric = ListOutput {
403 name: "io.systemd.Manager.UnitsByStateTotal".to_string(),
404 value: int_value(10),
405 object: None,
406 fields: Some(std::collections::HashMap::from([(
407 "state".to_string(),
408 serde_json::json!("active"),
409 )])),
410 };
411 parse_one_metric(&mut stats, &state_metric, &config).unwrap();
412 assert_eq!(stats.active_units, 10);
413 }
414
415 #[tokio::test]
416 async fn test_parse_multiple_units() {
417 let mut stats = SystemdUnitStats::default();
418 let config = default_units_config();
419
420 let metrics = vec![
421 ListOutput {
422 name: "io.systemd.Manager.UnitActiveState".to_string(),
423 value: string_value("active"),
424 object: Some("service1.service".to_string()),
425 fields: None,
426 },
427 ListOutput {
428 name: "io.systemd.Manager.UnitLoadState".to_string(),
429 value: string_value("loaded"),
430 object: Some("service1.service".to_string()),
431 fields: None,
432 },
433 ListOutput {
434 name: "io.systemd.Manager.UnitActiveState".to_string(),
435 value: string_value("failed"),
436 object: Some("service-2.service".to_string()),
437 fields: None,
438 },
439 ];
440
441 for metric in metrics {
442 parse_one_metric(&mut stats, &metric, &config).unwrap();
443 }
444
445 assert_eq!(stats.unit_states.len(), 2);
446 assert_eq!(
447 stats
448 .unit_states
449 .get("service1.service")
450 .unwrap()
451 .active_state,
452 SystemdUnitActiveState::active
453 );
454 assert_eq!(
455 stats
456 .unit_states
457 .get("service1.service")
458 .unwrap()
459 .load_state,
460 SystemdUnitLoadState::loaded
461 );
462 assert_eq!(
463 stats
464 .unit_states
465 .get("service-2.service")
466 .unwrap()
467 .active_state,
468 SystemdUnitActiveState::failed
469 );
470 }
471
472 #[tokio::test]
473 async fn test_parse_unknown_and_missing_values() {
474 let mut stats = SystemdUnitStats::default();
475 let config = default_units_config();
476
477 let metric1 = ListOutput {
479 name: "io.systemd.Manager.UnitActiveState".to_string(),
480 value: string_value("invalid_state"),
481 object: Some("test.service".to_string()),
482 fields: None,
483 };
484 parse_one_metric(&mut stats, &metric1, &config).unwrap();
485 assert!(
486 !stats.unit_states.contains_key("test.service"),
487 "invalid state should be skipped"
488 );
489
490 let metric2 = ListOutput {
492 name: "io.systemd.Manager.NRestarts".to_string(),
493 value: empty_value(),
494 object: Some("test2.service".to_string()),
495 fields: None,
496 };
497 parse_one_metric(&mut stats, &metric2, &config).unwrap();
498 assert!(
499 !stats.service_stats.contains_key("test2.service"),
500 "null value should be skipped"
501 );
502 }
503
504 #[tokio::test]
505 async fn test_parse_edge_cases() {
506 let mut stats = SystemdUnitStats::default();
507 let config = default_units_config();
508
509 let metric1 = ListOutput {
511 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
512 value: int_value(999),
513 object: None,
514 fields: Some(std::collections::HashMap::from([(
515 "type".to_string(),
516 serde_json::json!("unknown_type"),
517 )])),
518 };
519 parse_one_metric(&mut stats, &metric1, &config).unwrap();
520 assert_eq!(stats.service_units, 0);
521
522 let metric2 = ListOutput {
524 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
525 value: int_value(42),
526 object: None,
527 fields: None,
528 };
529 parse_one_metric(&mut stats, &metric2, &config).unwrap();
530
531 let metric3 = ListOutput {
533 name: "io.systemd.Manager.UnitsByTypeTotal".to_string(),
534 value: int_value(42),
535 object: None,
536 fields: Some(std::collections::HashMap::from([(
537 "type".to_string(),
538 serde_json::json!(123),
539 )])),
540 };
541 parse_one_metric(&mut stats, &metric3, &config).unwrap();
542
543 let metric4 = ListOutput {
545 name: "io.systemd.Manager.UnknownMetric".to_string(),
546 value: int_value(999),
547 object: Some("test.service".to_string()),
548 fields: None,
549 };
550 parse_one_metric(&mut stats, &metric4, &config).unwrap();
551 }
552
553 #[tokio::test]
554 async fn test_get_unit_stats_with_state_stats_disabled() {
555 let config = crate::config::Config {
556 units: crate::config::UnitsConfig {
557 enabled: true,
558 state_stats: false,
559 state_stats_allowlist: HashSet::new(),
560 state_stats_blocklist: HashSet::new(),
561 state_stats_time_in_state: true,
562 },
563 ..Default::default()
564 };
565
566 let result = get_unit_stats(&config, METRICS_SOCKET_PATH).await;
567 assert!(result.is_ok());
568
569 let stats = result.unwrap();
570 assert_eq!(stats.unit_states.len(), 0);
571 assert_eq!(stats.service_stats.len(), 0);
572 }
573
574 #[test]
575 fn test_parse_metric_enum() {
576 let metric_active = ListOutput {
577 name: "io.systemd.Manager.UnitActiveState".to_string(),
578 value: string_value("active"),
579 object: Some("test.service".to_string()),
580 fields: None,
581 };
582 assert_eq!(
583 parse_metric_enum::<SystemdUnitActiveState>(&metric_active),
584 Some(SystemdUnitActiveState::active)
585 );
586
587 let metric_loaded = ListOutput {
588 name: "io.systemd.Manager.UnitLoadState".to_string(),
589 value: string_value("loaded"),
590 object: Some("test.service".to_string()),
591 fields: None,
592 };
593 assert_eq!(
594 parse_metric_enum::<SystemdUnitLoadState>(&metric_loaded),
595 Some(SystemdUnitLoadState::loaded)
596 );
597
598 let metric_invalid = ListOutput {
600 name: "io.systemd.Manager.UnitActiveState".to_string(),
601 value: string_value("invalid"),
602 object: Some("test.service".to_string()),
603 fields: None,
604 };
605 assert_eq!(
606 parse_metric_enum::<SystemdUnitActiveState>(&metric_invalid),
607 None
608 );
609
610 let metric_empty = ListOutput {
612 name: "io.systemd.Manager.UnitActiveState".to_string(),
613 value: empty_value(),
614 object: Some("test.service".to_string()),
615 fields: None,
616 };
617 assert_eq!(
618 parse_metric_enum::<SystemdUnitActiveState>(&metric_empty),
619 None
620 );
621 }
622
623 #[test]
624 fn test_parse_metric_enum_all_states() {
625 let active_states = vec![
627 ("active", SystemdUnitActiveState::active),
628 ("reloading", SystemdUnitActiveState::reloading),
629 ("inactive", SystemdUnitActiveState::inactive),
630 ("failed", SystemdUnitActiveState::failed),
631 ("activating", SystemdUnitActiveState::activating),
632 ("deactivating", SystemdUnitActiveState::deactivating),
633 ];
634
635 for (state_str, expected) in active_states {
636 let metric = ListOutput {
637 name: "io.systemd.Manager.UnitActiveState".to_string(),
638 value: string_value(state_str),
639 object: Some("test.service".to_string()),
640 fields: None,
641 };
642 assert_eq!(
643 parse_metric_enum::<SystemdUnitActiveState>(&metric),
644 Some(expected)
645 );
646 }
647
648 let load_states = vec![
650 ("loaded", SystemdUnitLoadState::loaded),
651 ("error", SystemdUnitLoadState::error),
652 ("masked", SystemdUnitLoadState::masked),
653 ("not_found", SystemdUnitLoadState::not_found),
654 ];
655
656 for (state_str, expected) in load_states {
657 let metric = ListOutput {
658 name: "io.systemd.Manager.UnitLoadState".to_string(),
659 value: string_value(state_str),
660 object: Some("test.service".to_string()),
661 fields: None,
662 };
663 assert_eq!(
664 parse_metric_enum::<SystemdUnitLoadState>(&metric),
665 Some(expected)
666 );
667 }
668 }
669
670 #[tokio::test]
671 async fn test_parse_state_updates() {
672 let mut stats = SystemdUnitStats::default();
673 let config = default_units_config();
674
675 let metric1 = ListOutput {
677 name: "io.systemd.Manager.UnitActiveState".to_string(),
678 value: string_value("inactive"),
679 object: Some("test.service".to_string()),
680 fields: None,
681 };
682 parse_one_metric(&mut stats, &metric1, &config).unwrap();
683 assert_eq!(
684 stats.unit_states.get("test.service").unwrap().active_state,
685 SystemdUnitActiveState::inactive
686 );
687
688 let metric2 = 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 parse_one_metric(&mut stats, &metric2, &config).unwrap();
696 assert_eq!(
697 stats.unit_states.get("test.service").unwrap().active_state,
698 SystemdUnitActiveState::active
699 );
700 }
701
702 #[tokio::test]
703 async fn test_unhealthy_computed() {
704 let mut stats = SystemdUnitStats::default();
705 let config = default_units_config();
706
707 let metric1 = ListOutput {
709 name: "io.systemd.Manager.UnitActiveState".to_string(),
710 value: string_value("failed"),
711 object: Some("broken.service".to_string()),
712 fields: None,
713 };
714 parse_one_metric(&mut stats, &metric1, &config).unwrap();
715
716 let metric2 = ListOutput {
718 name: "io.systemd.Manager.UnitLoadState".to_string(),
719 value: string_value("loaded"),
720 object: Some("broken.service".to_string()),
721 fields: None,
722 };
723 parse_one_metric(&mut stats, &metric2, &config).unwrap();
724
725 assert!(stats.unit_states.get("broken.service").unwrap().unhealthy);
727
728 let metric3 = ListOutput {
730 name: "io.systemd.Manager.UnitActiveState".to_string(),
731 value: string_value("active"),
732 object: Some("healthy.service".to_string()),
733 fields: None,
734 };
735 parse_one_metric(&mut stats, &metric3, &config).unwrap();
736
737 let metric4 = ListOutput {
739 name: "io.systemd.Manager.UnitLoadState".to_string(),
740 value: string_value("loaded"),
741 object: Some("healthy.service".to_string()),
742 fields: None,
743 };
744 parse_one_metric(&mut stats, &metric4, &config).unwrap();
745
746 assert!(!stats.unit_states.get("healthy.service").unwrap().unhealthy);
748 }
749
750 #[tokio::test]
751 async fn test_allowlist_filtering() {
752 let mut stats = SystemdUnitStats::default();
753 let config = crate::config::UnitsConfig {
754 enabled: true,
755 state_stats: true,
756 state_stats_allowlist: HashSet::from(["allowed.service".to_string()]),
757 state_stats_blocklist: HashSet::new(),
758 state_stats_time_in_state: false,
759 };
760
761 let metric1 = ListOutput {
763 name: "io.systemd.Manager.UnitActiveState".to_string(),
764 value: string_value("active"),
765 object: Some("allowed.service".to_string()),
766 fields: None,
767 };
768 parse_one_metric(&mut stats, &metric1, &config).unwrap();
769 assert!(stats.unit_states.contains_key("allowed.service"));
770
771 let metric2 = ListOutput {
773 name: "io.systemd.Manager.UnitActiveState".to_string(),
774 value: string_value("active"),
775 object: Some("not-allowed.service".to_string()),
776 fields: None,
777 };
778 parse_one_metric(&mut stats, &metric2, &config).unwrap();
779 assert!(!stats.unit_states.contains_key("not-allowed.service"));
780 }
781
782 #[tokio::test]
783 async fn test_blocklist_filtering() {
784 let mut stats = SystemdUnitStats::default();
785 let config = crate::config::UnitsConfig {
786 enabled: true,
787 state_stats: true,
788 state_stats_allowlist: HashSet::new(),
789 state_stats_blocklist: HashSet::from(["blocked.service".to_string()]),
790 state_stats_time_in_state: false,
791 };
792
793 let metric1 = ListOutput {
795 name: "io.systemd.Manager.UnitActiveState".to_string(),
796 value: string_value("active"),
797 object: Some("blocked.service".to_string()),
798 fields: None,
799 };
800 parse_one_metric(&mut stats, &metric1, &config).unwrap();
801 assert!(!stats.unit_states.contains_key("blocked.service"));
802
803 let metric2 = ListOutput {
805 name: "io.systemd.Manager.UnitActiveState".to_string(),
806 value: string_value("active"),
807 object: Some("ok.service".to_string()),
808 fields: None,
809 };
810 parse_one_metric(&mut stats, &metric2, &config).unwrap();
811 assert!(stats.unit_states.contains_key("ok.service"));
812 }
813
814 #[tokio::test]
815 async fn test_blocklist_overrides_allowlist() {
816 let mut stats = SystemdUnitStats::default();
817 let config = crate::config::UnitsConfig {
818 enabled: true,
819 state_stats: true,
820 state_stats_allowlist: HashSet::from(["both.service".to_string()]),
821 state_stats_blocklist: HashSet::from(["both.service".to_string()]),
822 state_stats_time_in_state: false,
823 };
824
825 let metric = ListOutput {
827 name: "io.systemd.Manager.UnitActiveState".to_string(),
828 value: string_value("active"),
829 object: Some("both.service".to_string()),
830 fields: None,
831 };
832 parse_one_metric(&mut stats, &metric, &config).unwrap();
833 assert!(!stats.unit_states.contains_key("both.service"));
834 }
835}