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