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_unit_files_scope(
118 scope: &units::UnitFilesScope,
119 base: &str,
120) -> Vec<(String, serde_json::Value)> {
121 let mut flat_stats = Vec::new();
122 for (unit_type, count) in &scope.generated {
123 flat_stats.push((
124 format!("{base}.generated.{unit_type}_units"),
125 (*count).into(),
126 ));
127 }
128 for (unit_type, count) in &scope.transient {
129 flat_stats.push((
130 format!("{base}.transient.{unit_type}_units"),
131 (*count).into(),
132 ));
133 }
134 flat_stats
135}
136
137fn flatten_unit_files(
138 unit_files: &units::UnitFilesStats,
139 key_prefix: &str,
140) -> Vec<(String, serde_json::Value)> {
141 let base = gen_base_metric_key(key_prefix, "unit_files");
142 let mut flat_stats = flatten_unit_files_scope(&unit_files.root, &format!("{base}.root"));
143 flat_stats.extend(flatten_unit_files_scope(
144 &unit_files.user,
145 &format!("{base}.user"),
146 ));
147 flat_stats
148}
149
150fn flatten_services(
151 service_stats_hash: &HashMap<String, units::ServiceStats>,
152 key_prefix: &str,
153) -> Vec<(String, serde_json::Value)> {
154 let mut flat_stats = Vec::new();
155 let base_metric_name = gen_base_metric_key(key_prefix, "services");
156
157 for (service_name, service_stats) in service_stats_hash.iter() {
158 if let Ok(serde_json::Value::Object(map)) = serde_json::to_value(service_stats) {
159 for (field_name, value) in map {
160 if value.is_number() {
161 let key = format!("{base_metric_name}.{service_name}.{field_name}");
162 flat_stats.push((key, value));
163 }
164 }
165 }
166 }
167 flat_stats
168}
169
170fn flatten_timers(
171 timer_stats_hash: &HashMap<String, crate::timer::TimerStats>,
172 key_prefix: &str,
173) -> Vec<(String, serde_json::Value)> {
174 let mut flat_stats = Vec::new();
175 let base_metric_name = gen_base_metric_key(key_prefix, "timers");
176
177 for (timer_name, timer_stats) in timer_stats_hash.iter() {
178 if let Ok(serde_json::Value::Object(map)) = serde_json::to_value(timer_stats) {
179 for (field_name, value) in map {
180 let key = format!("{base_metric_name}.{timer_name}.{field_name}");
181 if value.is_number() {
182 flat_stats.push((key, value));
183 } else if let Some(b) = value.as_bool() {
184 flat_stats.push((key, (b as u64).into()));
185 }
186 }
187 }
188 }
189 flat_stats
190}
191
192fn flatten_unit_states(
193 unit_states_hash: &HashMap<String, units::UnitStates>,
194 key_prefix: &str,
195) -> Vec<(String, serde_json::Value)> {
196 let mut flat_stats = Vec::new();
197 let base_metric_name = gen_base_metric_key(key_prefix, "unit_states");
198
199 for (unit_name, unit_state_stats) in unit_states_hash.iter() {
200 if let Ok(serde_json::Value::Object(map)) = serde_json::to_value(unit_state_stats) {
201 for (field_name, value) in map {
202 let key = format!("{base_metric_name}.{unit_name}.{field_name}");
203 if value.is_number() {
204 flat_stats.push((key, value));
205 } else if let Some(b) = value.as_bool() {
206 flat_stats.push((key, (b as u64).into()));
207 }
208 }
209 }
210 }
211
212 flat_stats
213}
214
215#[derive(serde::Serialize)]
219struct UnitCounters {
220 activating_units: u64,
221 active_units: u64,
222 automount_units: u64,
223 device_units: u64,
224 failed_units: u64,
225 inactive_units: u64,
226 jobs_queued: u64,
227 loaded_units: u64,
228 masked_units: u64,
229 mount_units: u64,
230 not_found_units: u64,
231 path_units: u64,
232 scope_units: u64,
233 service_units: u64,
234 slice_units: u64,
235 socket_units: u64,
236 target_units: u64,
237 timer_units: u64,
238 timer_persistent_units: u64,
239 timer_remain_after_elapse: u64,
240 total_units: u64,
241}
242
243impl From<&units::SystemdUnitStats> for UnitCounters {
244 fn from(s: &units::SystemdUnitStats) -> Self {
245 Self {
246 activating_units: s.activating_units,
247 active_units: s.active_units,
248 automount_units: s.automount_units,
249 device_units: s.device_units,
250 failed_units: s.failed_units,
251 inactive_units: s.inactive_units,
252 jobs_queued: s.jobs_queued,
253 loaded_units: s.loaded_units,
254 masked_units: s.masked_units,
255 mount_units: s.mount_units,
256 not_found_units: s.not_found_units,
257 path_units: s.path_units,
258 scope_units: s.scope_units,
259 service_units: s.service_units,
260 slice_units: s.slice_units,
261 socket_units: s.socket_units,
262 target_units: s.target_units,
263 timer_units: s.timer_units,
264 timer_persistent_units: s.timer_persistent_units,
265 timer_remain_after_elapse: s.timer_remain_after_elapse,
266 total_units: s.total_units,
267 }
268 }
269}
270
271fn flatten_units(
272 units_stats: &units::SystemdUnitStats,
273 key_prefix: &str,
274) -> Vec<(String, serde_json::Value)> {
275 let mut flat_stats = Vec::new();
276 let base_metric_name = gen_base_metric_key(key_prefix, "units");
277
278 if let Ok(serde_json::Value::Object(map)) =
279 serde_json::to_value(UnitCounters::from(units_stats))
280 {
281 for (field_name, value) in map {
282 if value.is_number() {
283 let key = format!("{base_metric_name}.{field_name}");
284 flat_stats.push((key, value));
285 }
286 }
287 }
288 flat_stats
289}
290
291fn flatten_machines(
292 machines_stats: &HashMap<String, MachineStats>,
293 key_prefix: &str,
294) -> BTreeMap<String, serde_json::Value> {
295 let mut flat_stats = BTreeMap::new();
296
297 if machines_stats.is_empty() {
298 return flat_stats;
299 }
300
301 for (machine, stats) in machines_stats {
302 let machine_key_prefix = match key_prefix.is_empty() {
303 true => format!("machines.{}", machine),
304 false => format!("{}.machines.{}", key_prefix, machine),
305 };
306 flat_stats.extend(flatten_networkd(&stats.networkd, &machine_key_prefix));
307 flat_stats.extend(flatten_units(&stats.units, &machine_key_prefix));
308 flat_stats.extend(flatten_unit_files(
309 &stats.units.unit_files,
310 &machine_key_prefix,
311 ));
312 flat_stats.extend(flatten_units_collection_timings(
313 &stats.units.collection_timings,
314 &machine_key_prefix,
315 ));
316 flat_stats.extend(flatten_pid1(&stats.pid1, &machine_key_prefix));
317 flat_stats.insert(
318 gen_base_metric_key(&machine_key_prefix, "system-state"),
319 (stats.system_state as u64).into(),
320 );
321 flat_stats.extend(flatten_services(
322 &stats.units.service_stats,
323 &machine_key_prefix,
324 ));
325 flat_stats.extend(flatten_timers(
326 &stats.units.timer_stats,
327 &machine_key_prefix,
328 ));
329 flat_stats.extend(flatten_boot_blame(&stats.boot_blame, &machine_key_prefix));
330 flat_stats.extend(flatten_verify_stats(
331 &stats.verify_stats,
332 &machine_key_prefix,
333 ));
334 }
335
336 flat_stats
337}
338
339fn flatten_dbus_stats(
340 optional_dbus_stats: &Option<dbus_stats::DBusStats>,
341 key_prefix: &str,
342) -> BTreeMap<String, serde_json::Value> {
343 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
344 let dbus_stats = match optional_dbus_stats {
345 Some(ds) => ds,
346 None => {
347 debug!("Skipping flattening dbus stats as we got None ...");
348 return flat_stats;
349 }
350 };
351
352 let base_metric_name = gen_base_metric_key(key_prefix, "dbus");
353 let fields = [
354 ("active_connections", dbus_stats.active_connections),
356 ("incomplete_connections", dbus_stats.incomplete_connections),
357 ("bus_names", dbus_stats.bus_names),
358 ("peak_bus_names", dbus_stats.peak_bus_names),
359 (
360 "peak_bus_names_per_connection",
361 dbus_stats.peak_bus_names_per_connection,
362 ),
363 ("match_rules", dbus_stats.match_rules),
364 ("peak_match_rules", dbus_stats.peak_match_rules),
365 (
366 "peak_match_rules_per_connection",
367 dbus_stats.peak_match_rules_per_connection,
368 ),
369 ("stale_fds", dbus_stats.stale_fds),
370 ];
371
372 for (field_name, value) in fields {
373 if let Some(val) = value {
374 flat_stats.insert(format!("{base_metric_name}.{field_name}"), val.into());
375 }
376 }
377
378 if let Some(peer_accounting) = dbus_stats.peer_accounting() {
379 for peer in peer_accounting.values() {
380 let peer_name = peer.get_name();
381 let peer_fields = [
382 ("name_objects", peer.name_objects),
383 ("match_bytes", peer.match_bytes),
384 ("matches", peer.matches),
385 ("reply_objects", peer.reply_objects),
386 ("incoming_bytes", peer.incoming_bytes),
387 ("incoming_fds", peer.incoming_fds),
388 ("outgoing_bytes", peer.outgoing_bytes),
389 ("outgoing_fds", peer.outgoing_fds),
390 ("activation_request_bytes", peer.activation_request_bytes),
391 ("activation_request_fds", peer.activation_request_fds),
392 ];
393
394 for (field_name, value) in peer_fields {
395 if let Some(val) = value {
396 flat_stats.insert(
397 format!("{base_metric_name}.peer.{peer_name}.{field_name}"),
398 val.into(),
399 );
400 }
401 }
402 }
403 }
404
405 if let Some(cgroup_accounting) = dbus_stats.cgroup_accounting() {
406 for cgroup in cgroup_accounting.values() {
407 let cgroup_name = &cgroup.name;
408 let cgroup_fields = [
409 ("name_objects", cgroup.name_objects),
410 ("match_bytes", cgroup.match_bytes),
411 ("matches", cgroup.matches),
412 ("reply_objects", cgroup.reply_objects),
413 ("incoming_bytes", cgroup.incoming_bytes),
414 ("incoming_fds", cgroup.incoming_fds),
415 ("outgoing_bytes", cgroup.outgoing_bytes),
416 ("outgoing_fds", cgroup.outgoing_fds),
417 ("activation_request_bytes", cgroup.activation_request_bytes),
418 ("activation_request_fds", cgroup.activation_request_fds),
419 ];
420
421 for (field_name, value) in cgroup_fields {
422 if let Some(val) = value {
423 flat_stats.insert(
424 format!("{base_metric_name}.cgroup.{cgroup_name}.{field_name}"),
425 val.into(),
426 );
427 }
428 }
429 }
430 }
431
432 if let Some(user_accounting) = dbus_stats.user_accounting() {
433 for user in user_accounting.values() {
435 let user_name = &user.username;
436 let user_fields = [
437 ("bytes", user.bytes.clone()),
438 ("fds", user.fds.clone()),
439 ("matches", user.matches.clone()),
440 ("objects", user.objects.clone()),
441 ];
442
443 for (field_name, value) in user_fields {
444 if let Some(val) = value {
445 flat_stats.insert(
446 format!("{base_metric_name}.user.{user_name}.{field_name}"),
447 val.get_usage().into(),
448 );
449 }
450 }
451
452 if let Some(stale_fds) = user.stale_fds {
453 flat_stats.insert(
454 format!("{base_metric_name}.user.{user_name}.stale_fds"),
455 stale_fds.into(),
456 );
457 }
458 }
459 }
460
461 flat_stats
462}
463
464fn flatten_boot_blame(
465 optional_boot_blame: &Option<crate::boot::BootBlameStats>,
466 key_prefix: &str,
467) -> BTreeMap<String, serde_json::Value> {
468 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
469 let boot_blame_stats = match optional_boot_blame {
470 Some(bb) => bb,
471 None => {
472 debug!("Skipping flattening boot blame stats as we got None ...");
473 return flat_stats;
474 }
475 };
476
477 let base_metric_name = gen_base_metric_key(key_prefix, "boot.blame");
478
479 for (unit_name, activation_time) in boot_blame_stats.iter() {
480 let key = format!("{}.{}", base_metric_name, unit_name);
481 flat_stats.insert(key, (*activation_time).into());
482 }
483
484 flat_stats
485}
486
487fn flatten_verify_stats(
488 optional_verify_stats: &Option<crate::verify::VerifyStats>,
489 key_prefix: &str,
490) -> BTreeMap<String, serde_json::Value> {
491 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
492 let verify_stats = match optional_verify_stats {
493 Some(vs) => vs,
494 None => {
495 debug!("Skipping flattening verify stats as we got None ...");
496 return flat_stats;
497 }
498 };
499
500 let base_metric_name = gen_base_metric_key(key_prefix, "verify.failing");
501
502 flat_stats.insert(
504 format!("{base_metric_name}.total"),
505 verify_stats.total.into(),
506 );
507
508 for (unit_type, count) in &verify_stats.by_type {
510 flat_stats.insert(format!("{base_metric_name}.{unit_type}"), (*count).into());
511 }
512
513 flat_stats
514}
515
516fn flatten_collector_timings(
517 timings: &[crate::CollectorTiming],
518 key_prefix: &str,
519) -> BTreeMap<String, serde_json::Value> {
520 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
521 let base_metric_name = gen_base_metric_key(key_prefix, "collector_timings");
522 for t in timings {
523 flat_stats.insert(
524 format!("{base_metric_name}.{}.start_offset_ms", t.name),
525 t.start_offset_ms.into(),
526 );
527 flat_stats.insert(
528 format!("{base_metric_name}.{}.elapsed_ms", t.name),
529 t.elapsed_ms.into(),
530 );
531 flat_stats.insert(
532 format!("{base_metric_name}.{}.success", t.name),
533 (if t.success { 1u64 } else { 0u64 }).into(),
534 );
535 }
536 flat_stats
537}
538
539fn flatten_units_collection_timings(
540 timings: &units::UnitsCollectionTimings,
541 key_prefix: &str,
542) -> BTreeMap<String, serde_json::Value> {
543 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
544 let base_metric_name = gen_base_metric_key(key_prefix, "collection_timings");
545 flat_stats.insert(
546 format!("{base_metric_name}.list_units_ms"),
547 timings.list_units_ms.into(),
548 );
549 flat_stats.insert(
550 format!("{base_metric_name}.per_unit_loop_ms"),
551 timings.per_unit_loop_ms.into(),
552 );
553 flat_stats.insert(
554 format!("{base_metric_name}.timer_dbus_fetches"),
555 timings.timer_dbus_fetches.into(),
556 );
557 flat_stats.insert(
558 format!("{base_metric_name}.state_dbus_fetches"),
559 timings.state_dbus_fetches.into(),
560 );
561 flat_stats.insert(
562 format!("{base_metric_name}.service_dbus_fetches"),
563 timings.service_dbus_fetches.into(),
564 );
565 for (idx, (unit_name, duration_ms)) in timings.slowest_units.iter().enumerate() {
568 flat_stats.insert(
569 format!("{base_metric_name}.slowest_units.{idx}.{unit_name}"),
570 (*duration_ms).into(),
571 );
572 }
573 flat_stats
574}
575
576fn flatten_stats(
578 stats_struct: &MonitordStats,
579 key_prefix: &str,
580) -> BTreeMap<String, serde_json::Value> {
581 let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
582 flat_stats.insert(
583 gen_base_metric_key(key_prefix, "stat_collection_run_time_ms"),
584 stats_struct.stat_collection_run_time_ms.into(),
585 );
586 flat_stats.extend(flatten_collector_timings(
587 &stats_struct.collector_timings,
588 key_prefix,
589 ));
590 flat_stats.extend(flatten_units_collection_timings(
591 &stats_struct.units.collection_timings,
592 key_prefix,
593 ));
594 flat_stats.extend(flatten_networkd(&stats_struct.networkd, key_prefix));
595 flat_stats.extend(flatten_pid1(&stats_struct.pid1, key_prefix));
596 flat_stats.insert(
597 gen_base_metric_key(key_prefix, "system-state"),
598 (stats_struct.system_state as u64).into(),
599 );
600 flat_stats.extend(flatten_services(
601 &stats_struct.units.service_stats,
602 key_prefix,
603 ));
604 flat_stats.extend(flatten_timers(&stats_struct.units.timer_stats, key_prefix));
605 flat_stats.extend(flatten_unit_states(
606 &stats_struct.units.unit_states,
607 key_prefix,
608 ));
609 flat_stats.extend(flatten_units(&stats_struct.units, key_prefix));
610 flat_stats.extend(flatten_unit_files(
611 &stats_struct.units.unit_files,
612 key_prefix,
613 ));
614 flat_stats.insert(
615 gen_base_metric_key(key_prefix, "version"),
616 stats_struct.version.to_string().into(),
617 );
618 flat_stats.extend(flatten_machines(&stats_struct.machines, key_prefix));
619 flat_stats.extend(flatten_dbus_stats(&stats_struct.dbus_stats, key_prefix));
620 flat_stats.extend(flatten_boot_blame(&stats_struct.boot_blame, key_prefix));
621 flat_stats.extend(flatten_verify_stats(&stats_struct.verify_stats, key_prefix));
622 flat_stats
623}
624
625pub fn flatten(
627 stats_struct: &MonitordStats,
628 key_prefix: &str,
629) -> Result<String, serde_json::Error> {
630 serde_json::to_string_pretty(&flatten_stats(stats_struct, key_prefix))
631}
632
633#[cfg(test)]
634mod tests {
635 use crate::timer;
636
637 use super::*;
638
639 const EXPECTED_FLAT_JSON: &str = r###"{
641 "boot.blame.cpe_chef.service": 103.05,
642 "boot.blame.dnf5-automatic.service": 204.159,
643 "boot.blame.sys-module-fuse.device": 16.21,
644 "collection_timings.list_units_ms": 5.0,
645 "collection_timings.per_unit_loop_ms": 37.0,
646 "collection_timings.service_dbus_fetches": 1,
647 "collection_timings.slowest_units.0.unittest.service": 12.5,
648 "collection_timings.slowest_units.1.unittest.timer": 8.25,
649 "collection_timings.state_dbus_fetches": 0,
650 "collection_timings.timer_dbus_fetches": 4,
651 "collector_timings.boot_blame.elapsed_ms": 12.5,
652 "collector_timings.boot_blame.start_offset_ms": 0.25,
653 "collector_timings.boot_blame.success": 0,
654 "collector_timings.units.elapsed_ms": 42.0,
655 "collector_timings.units.start_offset_ms": 0.5,
656 "collector_timings.units.success": 1,
657 "machines.foo.collection_timings.list_units_ms": 0.0,
658 "machines.foo.collection_timings.per_unit_loop_ms": 0.0,
659 "machines.foo.collection_timings.service_dbus_fetches": 0,
660 "machines.foo.collection_timings.state_dbus_fetches": 0,
661 "machines.foo.collection_timings.timer_dbus_fetches": 0,
662 "machines.foo.networkd.managed_interfaces": 0,
663 "machines.foo.system-state": 0,
664 "machines.foo.timers.unittest.timer.accuracy_usec": 69,
665 "machines.foo.timers.unittest.timer.fixed_random_delay": 1,
666 "machines.foo.timers.unittest.timer.last_trigger_usec": 69,
667 "machines.foo.timers.unittest.timer.last_trigger_usec_monotonic": 69,
668 "machines.foo.timers.unittest.timer.next_elapse_usec_monotonic": 69,
669 "machines.foo.timers.unittest.timer.next_elapse_usec_realtime": 69,
670 "machines.foo.timers.unittest.timer.persistent": 0,
671 "machines.foo.timers.unittest.timer.randomized_delay_usec": 69,
672 "machines.foo.timers.unittest.timer.remain_after_elapse": 1,
673 "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec": 69,
674 "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
675 "machines.foo.units.activating_units": 0,
676 "machines.foo.units.active_units": 0,
677 "machines.foo.units.automount_units": 0,
678 "machines.foo.units.device_units": 0,
679 "machines.foo.units.failed_units": 0,
680 "machines.foo.units.inactive_units": 0,
681 "machines.foo.units.jobs_queued": 0,
682 "machines.foo.units.loaded_units": 0,
683 "machines.foo.units.masked_units": 0,
684 "machines.foo.units.mount_units": 0,
685 "machines.foo.units.not_found_units": 0,
686 "machines.foo.units.path_units": 0,
687 "machines.foo.units.scope_units": 0,
688 "machines.foo.units.service_units": 0,
689 "machines.foo.units.slice_units": 0,
690 "machines.foo.units.socket_units": 0,
691 "machines.foo.units.target_units": 0,
692 "machines.foo.units.timer_persistent_units": 0,
693 "machines.foo.units.timer_remain_after_elapse": 0,
694 "machines.foo.units.timer_units": 0,
695 "machines.foo.units.total_units": 0,
696 "networkd.eth0.address_state": 3,
697 "networkd.eth0.admin_state": 4,
698 "networkd.eth0.carrier_state": 5,
699 "networkd.eth0.ipv4_address_state": 3,
700 "networkd.eth0.ipv6_address_state": 2,
701 "networkd.eth0.oper_state": 9,
702 "networkd.eth0.required_for_online": 1,
703 "networkd.managed_interfaces": 1,
704 "pid1.cpu_time_kernel": 69,
705 "pid1.cpu_user_kernel": 69,
706 "pid1.fd_count": 69,
707 "pid1.memory_usage_bytes": 69,
708 "pid1.tasks": 1,
709 "services.unittest.service.active_enter_timestamp": 0,
710 "services.unittest.service.active_exit_timestamp": 0,
711 "services.unittest.service.cpuusage_nsec": 0,
712 "services.unittest.service.inactive_exit_timestamp": 0,
713 "services.unittest.service.ioread_bytes": 0,
714 "services.unittest.service.ioread_operations": 0,
715 "services.unittest.service.memory_available": 0,
716 "services.unittest.service.memory_current": 0,
717 "services.unittest.service.nrestarts": 0,
718 "services.unittest.service.processes": 0,
719 "services.unittest.service.restart_usec": 0,
720 "services.unittest.service.state_change_timestamp": 0,
721 "services.unittest.service.status_errno": -69,
722 "services.unittest.service.tasks_current": 0,
723 "services.unittest.service.timeout_clean_usec": 0,
724 "services.unittest.service.watchdog_usec": 0,
725 "stat_collection_run_time_ms": 69.0,
726 "system-state": 3,
727 "timers.unittest.timer.accuracy_usec": 69,
728 "timers.unittest.timer.fixed_random_delay": 1,
729 "timers.unittest.timer.last_trigger_usec": 69,
730 "timers.unittest.timer.last_trigger_usec_monotonic": 69,
731 "timers.unittest.timer.next_elapse_usec_monotonic": 69,
732 "timers.unittest.timer.next_elapse_usec_realtime": 69,
733 "timers.unittest.timer.persistent": 0,
734 "timers.unittest.timer.randomized_delay_usec": 69,
735 "timers.unittest.timer.remain_after_elapse": 1,
736 "timers.unittest.timer.service_unit_last_state_change_usec": 69,
737 "timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
738 "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.active_state": 1,
739 "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.load_state": 1,
740 "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.unhealthy": 0,
741 "unit_states.unittest.service.active_state": 1,
742 "unit_states.unittest.service.load_state": 1,
743 "unit_states.unittest.service.time_in_state_usecs": 69,
744 "unit_states.unittest.service.unhealthy": 0,
745 "units.activating_units": 0,
746 "units.active_units": 0,
747 "units.automount_units": 0,
748 "units.device_units": 0,
749 "units.failed_units": 0,
750 "units.inactive_units": 0,
751 "units.jobs_queued": 0,
752 "units.loaded_units": 0,
753 "units.masked_units": 0,
754 "units.mount_units": 0,
755 "units.not_found_units": 0,
756 "units.path_units": 0,
757 "units.scope_units": 0,
758 "units.service_units": 0,
759 "units.slice_units": 0,
760 "units.socket_units": 0,
761 "units.target_units": 0,
762 "units.timer_persistent_units": 0,
763 "units.timer_remain_after_elapse": 0,
764 "units.timer_units": 0,
765 "units.total_units": 0,
766 "verify.failing.service": 2,
767 "verify.failing.slice": 1,
768 "verify.failing.total": 3,
769 "version": "255.7-1.fc40"
770}"###;
771
772 fn return_monitord_stats() -> MonitordStats {
773 let mut stats = MonitordStats {
774 networkd: networkd::NetworkdState {
775 interfaces_state: vec![networkd::InterfaceState {
776 address_state: networkd::AddressState::routable,
777 admin_state: networkd::AdminState::configured,
778 carrier_state: networkd::CarrierState::carrier,
779 ipv4_address_state: networkd::AddressState::routable,
780 ipv6_address_state: networkd::AddressState::degraded,
781 name: "eth0".to_string(),
782 network_file: "/etc/systemd/network/69-eno4.network".to_string(),
783 oper_state: networkd::OperState::routable,
784 required_for_online: networkd::BoolState::True,
785 }],
786 managed_interfaces: 1,
787 },
788 pid1: Some(crate::pid1::Pid1Stats {
789 cpu_time_kernel: 69,
790 cpu_time_user: 69,
791 memory_usage_bytes: 69,
792 fd_count: 69,
793 tasks: 1,
794 }),
795 system_state: crate::system::SystemdSystemState::running,
796 units: crate::units::SystemdUnitStats::default(),
797 version: String::from("255.7-1.fc40")
798 .try_into()
799 .expect("Unable to make SystemdVersion struct"),
800 machines: HashMap::from([(String::from("foo"), MachineStats::default())]),
801 dbus_stats: None,
802 boot_blame: None,
803 verify_stats: Some(crate::verify::VerifyStats {
804 total: 3,
805 by_type: HashMap::from([("service".to_string(), 2), ("slice".to_string(), 1)]),
806 }),
807 stat_collection_run_time_ms: 69.0,
808 collector_timings: vec![
809 crate::CollectorTiming {
810 name: "units".to_string(),
811 start_offset_ms: 0.5,
812 elapsed_ms: 42.0,
813 success: true,
814 },
815 crate::CollectorTiming {
816 name: "boot_blame".to_string(),
817 start_offset_ms: 0.25,
818 elapsed_ms: 12.5,
819 success: false,
820 },
821 ],
822 };
823 stats.units.collection_timings = units::UnitsCollectionTimings {
824 list_units_ms: 5.0,
825 unit_files_ms: 2.0,
826 per_unit_loop_ms: 37.0,
827 timer_dbus_fetches: 4,
828 state_dbus_fetches: 0,
829 service_dbus_fetches: 1,
830 slowest_units: vec![
831 ("unittest.service".to_string(), 12.5),
832 ("unittest.timer".to_string(), 8.25),
833 ],
834 };
835 let service_unit_name = String::from("unittest.service");
836 stats.units.service_stats.insert(
837 service_unit_name.clone(),
838 units::ServiceStats {
839 status_errno: -69,
841 ..Default::default()
842 },
843 );
844 stats.units.unit_states.insert(
845 String::from("unittest.service"),
846 units::UnitStates {
847 active_state: units::SystemdUnitActiveState::active,
848 load_state: units::SystemdUnitLoadState::loaded,
849 unhealthy: false,
850 time_in_state_usecs: Some(69),
851 },
852 );
853 let timer_unit = String::from("unittest.timer");
854 let timer_stats = timer::TimerStats {
855 accuracy_usec: 69,
856 fixed_random_delay: true,
857 last_trigger_usec: 69,
858 last_trigger_usec_monotonic: 69,
859 next_elapse_usec_monotonic: 69,
860 next_elapse_usec_realtime: 69,
861 persistent: false,
862 randomized_delay_usec: 69,
863 remain_after_elapse: true,
864 service_unit_last_state_change_usec: 69,
865 service_unit_last_state_change_usec_monotonic: 69,
866 };
867 stats
868 .units
869 .timer_stats
870 .insert(timer_unit.clone(), timer_stats.clone());
871 stats
872 .machines
873 .get_mut("foo")
874 .expect("No machine foo? WTF")
875 .units
876 .timer_stats
877 .insert(timer_unit, timer_stats);
878 stats.units.unit_states.insert(
880 String::from(
881 r"nvme\x2dWDC_CL_SN730_SDBQNTY\x2d512G\x2d2020_37222H80070511\x2dpart3.device",
882 ),
883 units::UnitStates {
884 active_state: units::SystemdUnitActiveState::active,
885 load_state: units::SystemdUnitLoadState::loaded,
886 unhealthy: false,
887 time_in_state_usecs: None,
888 },
889 );
890 let mut boot_blame = crate::boot::BootBlameStats::new();
892 boot_blame.insert(String::from("dnf5-automatic.service"), 204.159);
893 boot_blame.insert(String::from("cpe_chef.service"), 103.050);
894 boot_blame.insert(String::from("sys-module-fuse.device"), 16.210);
895 stats.boot_blame = Some(boot_blame);
896 stats
897 }
898
899 #[test]
900 fn test_flatten_map() {
901 let json_flat_map = flatten_stats(&return_monitord_stats(), "");
902 assert_eq!(129, json_flat_map.len());
903 }
904
905 #[test]
906 fn test_flatten() {
907 let json_flat = flatten(&return_monitord_stats(), "").expect("JSON serialize failed");
908 assert_eq!(EXPECTED_FLAT_JSON, json_flat);
909 }
910
911 #[test]
912 fn test_flatten_prefixed() {
913 let json_flat =
914 flatten(&return_monitord_stats(), "monitord").expect("JSON serialize failed");
915 let json_flat_unserialized: BTreeMap<String, serde_json::Value> =
916 serde_json::from_str(&json_flat).expect("JSON from_str failed");
917 for (key, _value) in json_flat_unserialized.iter() {
918 assert!(key.starts_with("monitord."));
919 }
920 }
921
922 #[test]
928 fn test_unit_counters_covers_all_scalar_fields() {
929 const NON_COUNTER_FIELDS: &[&str] = &[
931 "unit_files",
932 "service_stats",
933 "timer_stats",
934 "unit_states",
935 "collection_timings",
936 ];
937
938 let expected: std::collections::BTreeSet<&str> = units::UNIT_FIELD_NAMES
940 .iter()
941 .copied()
942 .filter(|f| !NON_COUNTER_FIELDS.contains(f))
943 .collect();
944
945 let counters_json =
947 serde_json::to_value(UnitCounters::from(&units::SystemdUnitStats::default()))
948 .expect("UnitCounters serialization failed");
949 let actual: std::collections::BTreeSet<&str> = counters_json
950 .as_object()
951 .expect("UnitCounters must serialize to a JSON object")
952 .keys()
953 .map(|s| s.as_str())
954 .collect();
955
956 assert_eq!(
957 expected,
958 actual,
959 "UnitCounters is out of sync with SystemdUnitStats scalar fields.\n\
960 Missing from UnitCounters: {:?}\n\
961 Extra in UnitCounters: {:?}",
962 expected.difference(&actual).collect::<Vec<_>>(),
963 actual.difference(&expected).collect::<Vec<_>>(),
964 );
965 }
966}