monitord/
json.rs

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