1use std::collections::BTreeMap;
7use std::collections::HashMap;
8
9use tracing::debug;
10
11use crate::dbus_stats;
12use crate::networkd;
13use crate::pid1;
14use crate::units;
15use crate::MachineStats;
16use crate::MonitordStats;
17
18fn gen_base_metric_key(key_prefix: &str, metric_name: &str) -> String {
20 match key_prefix.is_empty() {
21 true => String::from(metric_name),
22 false => format!("{}.{}", key_prefix, metric_name),
23 }
24}
25
26fn flatten_networkd(
27 networkd_stats: &networkd::NetworkdState,
28 key_prefix: &str,
29) -> Vec<(String, serde_json::Value)> {
30 let mut flat_stats = vec![];
31 let base_metric_name = gen_base_metric_key(key_prefix, "networkd");
32
33 let managed_interfaces_key = format!("{}.managed_interfaces", base_metric_name);
34 flat_stats.push((
35 managed_interfaces_key,
36 networkd_stats.managed_interfaces.into(),
37 ));
38
39 if networkd_stats.interfaces_state.is_empty() {
40 debug!("No networkd interfaces to add to flat JSON");
41 return flat_stats;
42 }
43
44 for interface in &networkd_stats.interfaces_state {
45 let interface_base = format!("{}.{}", base_metric_name, interface.name);
46 flat_stats.push((
47 format!("{interface_base}.address_state"),
48 (interface.address_state as u64).into(),
49 ));
50 flat_stats.push((
51 format!("{interface_base}.admin_state"),
52 (interface.admin_state as u64).into(),
53 ));
54 flat_stats.push((
55 format!("{interface_base}.carrier_state"),
56 (interface.carrier_state as u64).into(),
57 ));
58 flat_stats.push((
59 format!("{interface_base}.ipv4_address_state"),
60 (interface.ipv4_address_state as u64).into(),
61 ));
62 flat_stats.push((
63 format!("{interface_base}.ipv6_address_state"),
64 (interface.ipv6_address_state as u64).into(),
65 ));
66 flat_stats.push((
67 format!("{interface_base}.oper_state"),
68 (interface.oper_state as u64).into(),
69 ));
70 flat_stats.push((
71 format!("{interface_base}.required_for_online"),
72 (interface.required_for_online as u64).into(),
73 ));
74 }
75 flat_stats
76}
77
78fn flatten_pid1(
79 optional_pid1_stats: &Option<pid1::Pid1Stats>,
80 key_prefix: &str,
81) -> Vec<(String, serde_json::Value)> {
82 let pid1_stats = match optional_pid1_stats {
84 Some(ps) => ps,
85 None => {
86 debug!("Skipping flattening pid1 stats as we got None ...");
87 return Vec::new();
88 }
89 };
90
91 let base_metric_name = gen_base_metric_key(key_prefix, "pid1");
92
93 vec![
94 (
95 format!("{}.cpu_time_kernel", base_metric_name),
96 pid1_stats.cpu_time_kernel.into(),
97 ),
98 (
99 format!("{}.cpu_user_kernel", base_metric_name),
100 pid1_stats.cpu_time_user.into(),
101 ),
102 (
103 format!("{}.memory_usage_bytes", base_metric_name),
104 pid1_stats.memory_usage_bytes.into(),
105 ),
106 (
107 format!("{}.fd_count", base_metric_name),
108 pid1_stats.fd_count.into(),
109 ),
110 (
111 format!("{}.tasks", base_metric_name),
112 pid1_stats.tasks.into(),
113 ),
114 ]
115}
116
117fn flatten_services(
118 service_stats_hash: &HashMap<String, units::ServiceStats>,
119 key_prefix: &str,
120) -> Vec<(String, serde_json::Value)> {
121 let mut flat_stats = Vec::new();
122 let base_metric_name = gen_base_metric_key(key_prefix, "services");
123
124 for (service_name, service_stats) in service_stats_hash.iter() {
125 if let Ok(serde_json::Value::Object(map)) = serde_json::to_value(service_stats) {
126 for (field_name, value) in map {
127 if value.is_number() {
128 let key = format!("{base_metric_name}.{service_name}.{field_name}");
129 flat_stats.push((key, value));
130 }
131 }
132 }
133 }
134 flat_stats
135}
136
137fn flatten_timers(
138 timer_stats_hash: &HashMap<String, crate::timer::TimerStats>,
139 key_prefix: &str,
140) -> Vec<(String, serde_json::Value)> {
141 let mut flat_stats = Vec::new();
142 let base_metric_name = gen_base_metric_key(key_prefix, "timers");
143
144 for (timer_name, timer_stats) in timer_stats_hash.iter() {
145 if let Ok(serde_json::Value::Object(map)) = serde_json::to_value(timer_stats) {
146 for (field_name, value) in map {
147 let key = format!("{base_metric_name}.{timer_name}.{field_name}");
148 if value.is_number() {
149 flat_stats.push((key, value));
150 } else if let Some(b) = value.as_bool() {
151 flat_stats.push((key, (b as u64).into()));
152 }
153 }
154 }
155 }
156 flat_stats
157}
158
159fn flatten_unit_states(
160 unit_states_hash: &HashMap<String, units::UnitStates>,
161 key_prefix: &str,
162) -> Vec<(String, serde_json::Value)> {
163 let mut flat_stats = Vec::new();
164 let base_metric_name = gen_base_metric_key(key_prefix, "unit_states");
165
166 for (unit_name, unit_state_stats) in unit_states_hash.iter() {
167 if let Ok(serde_json::Value::Object(map)) = serde_json::to_value(unit_state_stats) {
168 for (field_name, value) in map {
169 let key = format!("{base_metric_name}.{unit_name}.{field_name}");
170 if value.is_number() {
171 flat_stats.push((key, value));
172 } else if let Some(b) = value.as_bool() {
173 flat_stats.push((key, (b as u64).into()));
174 }
175 }
176 }
177 }
178
179 flat_stats
180}
181
182#[derive(serde::Serialize)]
186struct UnitCounters {
187 activating_units: u64,
188 active_units: u64,
189 automount_units: u64,
190 device_units: u64,
191 failed_units: u64,
192 inactive_units: u64,
193 jobs_queued: u64,
194 loaded_units: u64,
195 masked_units: u64,
196 mount_units: u64,
197 not_found_units: u64,
198 path_units: u64,
199 scope_units: u64,
200 service_units: u64,
201 slice_units: u64,
202 socket_units: u64,
203 target_units: u64,
204 timer_units: u64,
205 timer_persistent_units: u64,
206 timer_remain_after_elapse: u64,
207 total_units: u64,
208}
209
210impl From<&units::SystemdUnitStats> for UnitCounters {
211 fn from(s: &units::SystemdUnitStats) -> Self {
212 Self {
213 activating_units: s.activating_units,
214 active_units: s.active_units,
215 automount_units: s.automount_units,
216 device_units: s.device_units,
217 failed_units: s.failed_units,
218 inactive_units: s.inactive_units,
219 jobs_queued: s.jobs_queued,
220 loaded_units: s.loaded_units,
221 masked_units: s.masked_units,
222 mount_units: s.mount_units,
223 not_found_units: s.not_found_units,
224 path_units: s.path_units,
225 scope_units: s.scope_units,
226 service_units: s.service_units,
227 slice_units: s.slice_units,
228 socket_units: s.socket_units,
229 target_units: s.target_units,
230 timer_units: s.timer_units,
231 timer_persistent_units: s.timer_persistent_units,
232 timer_remain_after_elapse: s.timer_remain_after_elapse,
233 total_units: s.total_units,
234 }
235 }
236}
237
238fn flatten_units(
239 units_stats: &units::SystemdUnitStats,
240 key_prefix: &str,
241) -> Vec<(String, serde_json::Value)> {
242 let mut flat_stats = Vec::new();
243 let base_metric_name = gen_base_metric_key(key_prefix, "units");
244
245 if let Ok(serde_json::Value::Object(map)) =
246 serde_json::to_value(UnitCounters::from(units_stats))
247 {
248 for (field_name, value) in map {
249 if value.is_number() {
250 let key = format!("{base_metric_name}.{field_name}");
251 flat_stats.push((key, value));
252 }
253 }
254 }
255 flat_stats
256}
257
258fn flatten_machines(
259 machines_stats: &HashMap<String, MachineStats>,
260 key_prefix: &str,
261) -> BTreeMap<String, serde_json::Value> {
262 let mut flat_stats = BTreeMap::new();
263
264 if machines_stats.is_empty() {
265 return flat_stats;
266 }
267
268 for (machine, stats) in machines_stats {
269 let machine_key_prefix = match key_prefix.is_empty() {
270 true => format!("machines.{}", machine),
271 false => format!("{}.machines.{}", key_prefix, machine),
272 };
273 flat_stats.extend(flatten_networkd(&stats.networkd, &machine_key_prefix));
274 flat_stats.extend(flatten_units(&stats.units, &machine_key_prefix));
275 flat_stats.extend(flatten_pid1(&stats.pid1, &machine_key_prefix));
276 flat_stats.insert(
277 gen_base_metric_key(&machine_key_prefix, "system-state"),
278 (stats.system_state as u64).into(),
279 );
280 flat_stats.extend(flatten_services(
281 &stats.units.service_stats,
282 &machine_key_prefix,
283 ));
284 flat_stats.extend(flatten_timers(
285 &stats.units.timer_stats,
286 &machine_key_prefix,
287 ));
288 flat_stats.extend(flatten_boot_blame(&stats.boot_blame, &machine_key_prefix));
289 flat_stats.extend(flatten_verify_stats(
290 &stats.verify_stats,
291 &machine_key_prefix,
292 ));
293 }
294
295 flat_stats
296}
297
298fn flatten_dbus_stats(
299 optional_dbus_stats: &Option<dbus_stats::DBusStats>,
300 key_prefix: &str,
301) -> BTreeMap<String, serde_json::Value> {
302 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
303 let dbus_stats = match optional_dbus_stats {
304 Some(ds) => ds,
305 None => {
306 debug!("Skipping flattening dbus stats as we got None ...");
307 return flat_stats;
308 }
309 };
310
311 let base_metric_name = gen_base_metric_key(key_prefix, "dbus");
312 let fields = [
313 ("active_connections", dbus_stats.active_connections),
315 ("incomplete_connections", dbus_stats.incomplete_connections),
316 ("bus_names", dbus_stats.bus_names),
317 ("peak_bus_names", dbus_stats.peak_bus_names),
318 (
319 "peak_bus_names_per_connection",
320 dbus_stats.peak_bus_names_per_connection,
321 ),
322 ("match_rules", dbus_stats.match_rules),
323 ("peak_match_rules", dbus_stats.peak_match_rules),
324 (
325 "peak_match_rules_per_connection",
326 dbus_stats.peak_match_rules_per_connection,
327 ),
328 ];
329
330 for (field_name, value) in fields {
331 if let Some(val) = value {
332 flat_stats.insert(format!("{base_metric_name}.{field_name}"), val.into());
333 }
334 }
335
336 if let Some(peer_accounting) = dbus_stats.peer_accounting() {
337 for peer in peer_accounting.values() {
338 let peer_name = peer.get_name();
339 let peer_fields = [
340 ("name_objects", peer.name_objects),
341 ("match_bytes", peer.match_bytes),
342 ("matches", peer.matches),
343 ("reply_objects", peer.reply_objects),
344 ("incoming_bytes", peer.incoming_bytes),
345 ("incoming_fds", peer.incoming_fds),
346 ("outgoing_bytes", peer.outgoing_bytes),
347 ("outgoing_fds", peer.outgoing_fds),
348 ("activation_request_bytes", peer.activation_request_bytes),
349 ("activation_request_fds", peer.activation_request_fds),
350 ];
351
352 for (field_name, value) in peer_fields {
353 if let Some(val) = value {
354 flat_stats.insert(
355 format!("{base_metric_name}.peer.{peer_name}.{field_name}"),
356 val.into(),
357 );
358 }
359 }
360 }
361 }
362
363 if let Some(cgroup_accounting) = dbus_stats.cgroup_accounting() {
364 for cgroup in cgroup_accounting.values() {
365 let cgroup_name = &cgroup.name;
366 let cgroup_fields = [
367 ("name_objects", cgroup.name_objects),
368 ("match_bytes", cgroup.match_bytes),
369 ("matches", cgroup.matches),
370 ("reply_objects", cgroup.reply_objects),
371 ("incoming_bytes", cgroup.incoming_bytes),
372 ("incoming_fds", cgroup.incoming_fds),
373 ("outgoing_bytes", cgroup.outgoing_bytes),
374 ("outgoing_fds", cgroup.outgoing_fds),
375 ("activation_request_bytes", cgroup.activation_request_bytes),
376 ("activation_request_fds", cgroup.activation_request_fds),
377 ];
378
379 for (field_name, value) in cgroup_fields {
380 if let Some(val) = value {
381 flat_stats.insert(
382 format!("{base_metric_name}.cgroup.{cgroup_name}.{field_name}"),
383 val.into(),
384 );
385 }
386 }
387 }
388 }
389
390 if let Some(user_accounting) = dbus_stats.user_accounting() {
391 for user in user_accounting.values() {
393 let user_name = &user.username;
394 let user_fields = [
395 ("bytes", user.bytes.clone()),
396 ("fds", user.fds.clone()),
397 ("matches", user.matches.clone()),
398 ("objects", user.objects.clone()),
399 ];
400
401 for (field_name, value) in user_fields {
402 if let Some(val) = value {
403 flat_stats.insert(
404 format!("{base_metric_name}.user.{user_name}.{field_name}"),
405 val.get_usage().into(),
406 );
407 }
408 }
409 }
410 }
411
412 flat_stats
413}
414
415fn flatten_boot_blame(
416 optional_boot_blame: &Option<crate::boot::BootBlameStats>,
417 key_prefix: &str,
418) -> BTreeMap<String, serde_json::Value> {
419 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
420 let boot_blame_stats = match optional_boot_blame {
421 Some(bb) => bb,
422 None => {
423 debug!("Skipping flattening boot blame stats as we got None ...");
424 return flat_stats;
425 }
426 };
427
428 let base_metric_name = gen_base_metric_key(key_prefix, "boot.blame");
429
430 for (unit_name, activation_time) in boot_blame_stats.iter() {
431 let key = format!("{}.{}", base_metric_name, unit_name);
432 flat_stats.insert(key, (*activation_time).into());
433 }
434
435 flat_stats
436}
437
438fn flatten_verify_stats(
439 optional_verify_stats: &Option<crate::verify::VerifyStats>,
440 key_prefix: &str,
441) -> BTreeMap<String, serde_json::Value> {
442 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
443 let verify_stats = match optional_verify_stats {
444 Some(vs) => vs,
445 None => {
446 debug!("Skipping flattening verify stats as we got None ...");
447 return flat_stats;
448 }
449 };
450
451 let base_metric_name = gen_base_metric_key(key_prefix, "verify.failing");
452
453 flat_stats.insert(
455 format!("{base_metric_name}.total"),
456 verify_stats.total.into(),
457 );
458
459 for (unit_type, count) in &verify_stats.by_type {
461 flat_stats.insert(format!("{base_metric_name}.{unit_type}"), (*count).into());
462 }
463
464 flat_stats
465}
466
467fn flatten_stats(
469 stats_struct: &MonitordStats,
470 key_prefix: &str,
471) -> BTreeMap<String, serde_json::Value> {
472 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
473 flat_stats.extend(flatten_networkd(&stats_struct.networkd, key_prefix));
474 flat_stats.extend(flatten_pid1(&stats_struct.pid1, key_prefix));
475 flat_stats.insert(
476 gen_base_metric_key(key_prefix, "system-state"),
477 (stats_struct.system_state as u64).into(),
478 );
479 flat_stats.extend(flatten_services(
480 &stats_struct.units.service_stats,
481 key_prefix,
482 ));
483 flat_stats.extend(flatten_timers(&stats_struct.units.timer_stats, key_prefix));
484 flat_stats.extend(flatten_unit_states(
485 &stats_struct.units.unit_states,
486 key_prefix,
487 ));
488 flat_stats.extend(flatten_units(&stats_struct.units, key_prefix));
489 flat_stats.insert(
490 gen_base_metric_key(key_prefix, "version"),
491 stats_struct.version.to_string().into(),
492 );
493 flat_stats.extend(flatten_machines(&stats_struct.machines, key_prefix));
494 flat_stats.extend(flatten_dbus_stats(&stats_struct.dbus_stats, key_prefix));
495 flat_stats.extend(flatten_boot_blame(&stats_struct.boot_blame, key_prefix));
496 flat_stats.extend(flatten_verify_stats(&stats_struct.verify_stats, key_prefix));
497 flat_stats
498}
499
500pub fn flatten(
502 stats_struct: &MonitordStats,
503 key_prefix: &str,
504) -> Result<String, serde_json::Error> {
505 serde_json::to_string_pretty(&flatten_stats(stats_struct, key_prefix))
506}
507
508#[cfg(test)]
509mod tests {
510 use crate::timer;
511
512 use super::*;
513
514 const EXPECTED_FLAT_JSON: &str = r###"{
516 "boot.blame.cpe_chef.service": 103.05,
517 "boot.blame.dnf5-automatic.service": 204.159,
518 "boot.blame.sys-module-fuse.device": 16.21,
519 "machines.foo.networkd.managed_interfaces": 0,
520 "machines.foo.system-state": 0,
521 "machines.foo.timers.unittest.timer.accuracy_usec": 69,
522 "machines.foo.timers.unittest.timer.fixed_random_delay": 1,
523 "machines.foo.timers.unittest.timer.last_trigger_usec": 69,
524 "machines.foo.timers.unittest.timer.last_trigger_usec_monotonic": 69,
525 "machines.foo.timers.unittest.timer.next_elapse_usec_monotonic": 69,
526 "machines.foo.timers.unittest.timer.next_elapse_usec_realtime": 69,
527 "machines.foo.timers.unittest.timer.persistent": 0,
528 "machines.foo.timers.unittest.timer.randomized_delay_usec": 69,
529 "machines.foo.timers.unittest.timer.remain_after_elapse": 1,
530 "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec": 69,
531 "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
532 "machines.foo.units.activating_units": 0,
533 "machines.foo.units.active_units": 0,
534 "machines.foo.units.automount_units": 0,
535 "machines.foo.units.device_units": 0,
536 "machines.foo.units.failed_units": 0,
537 "machines.foo.units.inactive_units": 0,
538 "machines.foo.units.jobs_queued": 0,
539 "machines.foo.units.loaded_units": 0,
540 "machines.foo.units.masked_units": 0,
541 "machines.foo.units.mount_units": 0,
542 "machines.foo.units.not_found_units": 0,
543 "machines.foo.units.path_units": 0,
544 "machines.foo.units.scope_units": 0,
545 "machines.foo.units.service_units": 0,
546 "machines.foo.units.slice_units": 0,
547 "machines.foo.units.socket_units": 0,
548 "machines.foo.units.target_units": 0,
549 "machines.foo.units.timer_persistent_units": 0,
550 "machines.foo.units.timer_remain_after_elapse": 0,
551 "machines.foo.units.timer_units": 0,
552 "machines.foo.units.total_units": 0,
553 "networkd.eth0.address_state": 3,
554 "networkd.eth0.admin_state": 4,
555 "networkd.eth0.carrier_state": 5,
556 "networkd.eth0.ipv4_address_state": 3,
557 "networkd.eth0.ipv6_address_state": 2,
558 "networkd.eth0.oper_state": 9,
559 "networkd.eth0.required_for_online": 1,
560 "networkd.managed_interfaces": 1,
561 "pid1.cpu_time_kernel": 69,
562 "pid1.cpu_user_kernel": 69,
563 "pid1.fd_count": 69,
564 "pid1.memory_usage_bytes": 69,
565 "pid1.tasks": 1,
566 "services.unittest.service.active_enter_timestamp": 0,
567 "services.unittest.service.active_exit_timestamp": 0,
568 "services.unittest.service.cpuusage_nsec": 0,
569 "services.unittest.service.inactive_exit_timestamp": 0,
570 "services.unittest.service.ioread_bytes": 0,
571 "services.unittest.service.ioread_operations": 0,
572 "services.unittest.service.memory_available": 0,
573 "services.unittest.service.memory_current": 0,
574 "services.unittest.service.nrestarts": 0,
575 "services.unittest.service.processes": 0,
576 "services.unittest.service.restart_usec": 0,
577 "services.unittest.service.state_change_timestamp": 0,
578 "services.unittest.service.status_errno": -69,
579 "services.unittest.service.tasks_current": 0,
580 "services.unittest.service.timeout_clean_usec": 0,
581 "services.unittest.service.watchdog_usec": 0,
582 "system-state": 3,
583 "timers.unittest.timer.accuracy_usec": 69,
584 "timers.unittest.timer.fixed_random_delay": 1,
585 "timers.unittest.timer.last_trigger_usec": 69,
586 "timers.unittest.timer.last_trigger_usec_monotonic": 69,
587 "timers.unittest.timer.next_elapse_usec_monotonic": 69,
588 "timers.unittest.timer.next_elapse_usec_realtime": 69,
589 "timers.unittest.timer.persistent": 0,
590 "timers.unittest.timer.randomized_delay_usec": 69,
591 "timers.unittest.timer.remain_after_elapse": 1,
592 "timers.unittest.timer.service_unit_last_state_change_usec": 69,
593 "timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
594 "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.active_state": 1,
595 "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.load_state": 1,
596 "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.unhealthy": 0,
597 "unit_states.unittest.service.active_state": 1,
598 "unit_states.unittest.service.load_state": 1,
599 "unit_states.unittest.service.time_in_state_usecs": 69,
600 "unit_states.unittest.service.unhealthy": 0,
601 "units.activating_units": 0,
602 "units.active_units": 0,
603 "units.automount_units": 0,
604 "units.device_units": 0,
605 "units.failed_units": 0,
606 "units.inactive_units": 0,
607 "units.jobs_queued": 0,
608 "units.loaded_units": 0,
609 "units.masked_units": 0,
610 "units.mount_units": 0,
611 "units.not_found_units": 0,
612 "units.path_units": 0,
613 "units.scope_units": 0,
614 "units.service_units": 0,
615 "units.slice_units": 0,
616 "units.socket_units": 0,
617 "units.target_units": 0,
618 "units.timer_persistent_units": 0,
619 "units.timer_remain_after_elapse": 0,
620 "units.timer_units": 0,
621 "units.total_units": 0,
622 "verify.failing.service": 2,
623 "verify.failing.slice": 1,
624 "verify.failing.total": 3,
625 "version": "255.7-1.fc40"
626}"###;
627
628 fn return_monitord_stats() -> MonitordStats {
629 let mut stats = MonitordStats {
630 networkd: networkd::NetworkdState {
631 interfaces_state: vec![networkd::InterfaceState {
632 address_state: networkd::AddressState::routable,
633 admin_state: networkd::AdminState::configured,
634 carrier_state: networkd::CarrierState::carrier,
635 ipv4_address_state: networkd::AddressState::routable,
636 ipv6_address_state: networkd::AddressState::degraded,
637 name: "eth0".to_string(),
638 network_file: "/etc/systemd/network/69-eno4.network".to_string(),
639 oper_state: networkd::OperState::routable,
640 required_for_online: networkd::BoolState::True,
641 }],
642 managed_interfaces: 1,
643 },
644 pid1: Some(crate::pid1::Pid1Stats {
645 cpu_time_kernel: 69,
646 cpu_time_user: 69,
647 memory_usage_bytes: 69,
648 fd_count: 69,
649 tasks: 1,
650 }),
651 system_state: crate::system::SystemdSystemState::running,
652 units: crate::units::SystemdUnitStats::default(),
653 version: String::from("255.7-1.fc40")
654 .try_into()
655 .expect("Unable to make SystemdVersion struct"),
656 machines: HashMap::from([(String::from("foo"), MachineStats::default())]),
657 dbus_stats: None,
658 boot_blame: None,
659 verify_stats: Some(crate::verify::VerifyStats {
660 total: 3,
661 by_type: HashMap::from([("service".to_string(), 2), ("slice".to_string(), 1)]),
662 }),
663 };
664 let service_unit_name = String::from("unittest.service");
665 stats.units.service_stats.insert(
666 service_unit_name.clone(),
667 units::ServiceStats {
668 status_errno: -69,
670 ..Default::default()
671 },
672 );
673 stats.units.unit_states.insert(
674 String::from("unittest.service"),
675 units::UnitStates {
676 active_state: units::SystemdUnitActiveState::active,
677 load_state: units::SystemdUnitLoadState::loaded,
678 unhealthy: false,
679 time_in_state_usecs: Some(69),
680 },
681 );
682 let timer_unit = String::from("unittest.timer");
683 let timer_stats = timer::TimerStats {
684 accuracy_usec: 69,
685 fixed_random_delay: true,
686 last_trigger_usec: 69,
687 last_trigger_usec_monotonic: 69,
688 next_elapse_usec_monotonic: 69,
689 next_elapse_usec_realtime: 69,
690 persistent: false,
691 randomized_delay_usec: 69,
692 remain_after_elapse: true,
693 service_unit_last_state_change_usec: 69,
694 service_unit_last_state_change_usec_monotonic: 69,
695 };
696 stats
697 .units
698 .timer_stats
699 .insert(timer_unit.clone(), timer_stats.clone());
700 stats
701 .machines
702 .get_mut("foo")
703 .expect("No machine foo? WTF")
704 .units
705 .timer_stats
706 .insert(timer_unit, timer_stats);
707 stats.units.unit_states.insert(
709 String::from(
710 r"nvme\x2dWDC_CL_SN730_SDBQNTY\x2d512G\x2d2020_37222H80070511\x2dpart3.device",
711 ),
712 units::UnitStates {
713 active_state: units::SystemdUnitActiveState::active,
714 load_state: units::SystemdUnitLoadState::loaded,
715 unhealthy: false,
716 time_in_state_usecs: None,
717 },
718 );
719 let mut boot_blame = crate::boot::BootBlameStats::new();
721 boot_blame.insert(String::from("dnf5-automatic.service"), 204.159);
722 boot_blame.insert(String::from("cpe_chef.service"), 103.050);
723 boot_blame.insert(String::from("sys-module-fuse.device"), 16.210);
724 stats.boot_blame = Some(boot_blame);
725 stats
726 }
727
728 #[test]
729 fn test_flatten_map() {
730 let json_flat_map = flatten_stats(&return_monitord_stats(), "");
731 assert_eq!(110, json_flat_map.len());
732 }
733
734 #[test]
735 fn test_flatten() {
736 let json_flat = flatten(&return_monitord_stats(), "").expect("JSON serialize failed");
737 assert_eq!(EXPECTED_FLAT_JSON, json_flat);
738 }
739
740 #[test]
741 fn test_flatten_prefixed() {
742 let json_flat =
743 flatten(&return_monitord_stats(), "monitord").expect("JSON serialize failed");
744 let json_flat_unserialized: BTreeMap<String, serde_json::Value> =
745 serde_json::from_str(&json_flat).expect("JSON from_str failed");
746 for (key, _value) in json_flat_unserialized.iter() {
747 assert!(key.starts_with("monitord."));
748 }
749 }
750
751 #[test]
757 fn test_unit_counters_covers_all_scalar_fields() {
758 const NON_COUNTER_FIELDS: &[&str] = &["service_stats", "timer_stats", "unit_states"];
760
761 let expected: std::collections::BTreeSet<&str> = units::UNIT_FIELD_NAMES
763 .iter()
764 .copied()
765 .filter(|f| !NON_COUNTER_FIELDS.contains(f))
766 .collect();
767
768 let counters_json =
770 serde_json::to_value(UnitCounters::from(&units::SystemdUnitStats::default()))
771 .expect("UnitCounters serialization failed");
772 let actual: std::collections::BTreeSet<&str> = counters_json
773 .as_object()
774 .expect("UnitCounters must serialize to a JSON object")
775 .keys()
776 .map(|s| s.as_str())
777 .collect();
778
779 assert_eq!(
780 expected,
781 actual,
782 "UnitCounters is out of sync with SystemdUnitStats scalar fields.\n\
783 Missing from UnitCounters: {:?}\n\
784 Extra in UnitCounters: {:?}",
785 expected.difference(&actual).collect::<Vec<_>>(),
786 actual.difference(&expected).collect::<Vec<_>>(),
787 );
788 }
789}