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.dbus_broker_peer_accounting {
444        // process peer accounting if present
445        for peer in peer_accounting.values() {
446            let peer_name = peer.get_name_for_metric();
447
448            let peer_fields = [
449                ("name_objects", peer.name_objects),
450                ("match_bytes", peer.match_bytes),
451                ("matches", peer.matches),
452                ("reply_objects", peer.reply_objects),
453                ("incoming_bytes", peer.incoming_bytes),
454                ("incoming_fds", peer.incoming_fds),
455                ("outgoing_bytes", peer.outgoing_bytes),
456                ("outgoing_fds", peer.outgoing_fds),
457                ("activation_request_bytes", peer.activation_request_bytes),
458                ("activation_request_fds", peer.activation_request_fds),
459            ];
460
461            for (field_name, value) in peer_fields {
462                if let Some(val) = value {
463                    flat_stats.insert(
464                        format!("{base_metric_name}.peer.{peer_name}.{field_name}"),
465                        val.into(),
466                    );
467                }
468            }
469        }
470    }
471
472    if let Some(user_accounting) = &dbus_stats.dbus_broker_user_accounting {
473        // process user accounting if present
474        for user in user_accounting.values() {
475            let user_name = user.get_name_for_metric();
476            let user_fields = [
477                ("bytes", user.bytes.clone()),
478                ("fds", user.fds.clone()),
479                ("matches", user.matches.clone()),
480                ("objects", user.objects.clone()),
481            ];
482
483            for (field_name, value) in user_fields {
484                if let Some(val) = value {
485                    flat_stats.insert(
486                        format!("{base_metric_name}.user.{user_name}.{field_name}"),
487                        val.cur.into(),
488                    );
489
490                    // little value in reporting max values in real life
491                }
492            }
493        }
494    }
495
496    flat_stats
497}
498
499/// Take the standard returned structs and move all to a flat BTreeMap<str, float|int> like JSON
500fn flatten_stats(
501    stats_struct: &MonitordStats,
502    key_prefix: &String,
503) -> BTreeMap<String, serde_json::Value> {
504    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
505    flat_stats.extend(flatten_networkd(&stats_struct.networkd, key_prefix));
506    flat_stats.extend(flatten_pid1(&stats_struct.pid1, key_prefix));
507    flat_stats.insert(
508        gen_base_metric_key(key_prefix, &String::from("system-state")),
509        (stats_struct.system_state as u64).into(),
510    );
511    flat_stats.extend(flatten_services(
512        &stats_struct.units.service_stats,
513        key_prefix,
514    ));
515    flat_stats.extend(flatten_timers(&stats_struct.units.timer_stats, key_prefix));
516    flat_stats.extend(flatten_unit_states(
517        &stats_struct.units.unit_states,
518        key_prefix,
519    ));
520    flat_stats.extend(flatten_units(&stats_struct.units, key_prefix));
521    flat_stats.insert(
522        gen_base_metric_key(key_prefix, &String::from("version")),
523        stats_struct.version.to_string().into(),
524    );
525    flat_stats.extend(flatten_machines(&stats_struct.machines, key_prefix));
526    flat_stats.extend(flatten_dbus_stats(&stats_struct.dbus_stats, key_prefix));
527    flat_stats
528}
529
530/// Take the standard returned structs and move all to a flat JSON str
531pub fn flatten(
532    stats_struct: &MonitordStats,
533    key_prefix: &String,
534) -> Result<String, serde_json::Error> {
535    serde_json::to_string_pretty(&flatten_stats(stats_struct, key_prefix))
536}
537
538#[cfg(test)]
539mod tests {
540    use crate::timer;
541
542    use super::*;
543
544    // This will always be sorted / deterministic ...
545    const EXPECTED_FLAT_JSON: &str = r###"{
546  "machines.foo.networkd.managed_interfaces": 0,
547  "machines.foo.system-state": 0,
548  "machines.foo.timers.unittest.timer.accuracy_usec": 69,
549  "machines.foo.timers.unittest.timer.fixed_random_delay": 1,
550  "machines.foo.timers.unittest.timer.last_trigger_usec": 69,
551  "machines.foo.timers.unittest.timer.last_trigger_usec_monotonic": 69,
552  "machines.foo.timers.unittest.timer.next_elapse_usec_monotonic": 69,
553  "machines.foo.timers.unittest.timer.next_elapse_usec_realtime": 69,
554  "machines.foo.timers.unittest.timer.persistent": 0,
555  "machines.foo.timers.unittest.timer.randomized_delay_usec": 69,
556  "machines.foo.timers.unittest.timer.remain_after_elapse": 1,
557  "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec": 69,
558  "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
559  "machines.foo.units.active_units": 0,
560  "machines.foo.units.automount_units": 0,
561  "machines.foo.units.device_units": 0,
562  "machines.foo.units.failed_units": 0,
563  "machines.foo.units.inactive_units": 0,
564  "machines.foo.units.jobs_queued": 0,
565  "machines.foo.units.loaded_units": 0,
566  "machines.foo.units.masked_units": 0,
567  "machines.foo.units.mount_units": 0,
568  "machines.foo.units.not_found_units": 0,
569  "machines.foo.units.path_units": 0,
570  "machines.foo.units.scope_units": 0,
571  "machines.foo.units.service_units": 0,
572  "machines.foo.units.slice_units": 0,
573  "machines.foo.units.socket_units": 0,
574  "machines.foo.units.target_units": 0,
575  "machines.foo.units.timer_persistent_units": 0,
576  "machines.foo.units.timer_remain_after_elapse": 0,
577  "machines.foo.units.timer_units": 0,
578  "machines.foo.units.total_units": 0,
579  "networkd.eth0.address_state": 3,
580  "networkd.eth0.admin_state": 4,
581  "networkd.eth0.carrier_state": 5,
582  "networkd.eth0.ipv4_address_state": 3,
583  "networkd.eth0.ipv6_address_state": 2,
584  "networkd.eth0.oper_state": 9,
585  "networkd.eth0.required_for_online": 1,
586  "networkd.managed_interfaces": 1,
587  "pid1.cpu_time_kernel": 69,
588  "pid1.cpu_user_kernel": 69,
589  "pid1.fd_count": 69,
590  "pid1.memory_usage_bytes": 69,
591  "pid1.tasks": 1,
592  "services.unittest.service.active_enter_timestamp": 0,
593  "services.unittest.service.active_exit_timestamp": 0,
594  "services.unittest.service.cpuusage_nsec": 0,
595  "services.unittest.service.inactive_exit_timestamp": 0,
596  "services.unittest.service.ioread_bytes": 0,
597  "services.unittest.service.ioread_operations": 0,
598  "services.unittest.service.memory_available": 0,
599  "services.unittest.service.memory_current": 0,
600  "services.unittest.service.nrestarts": 0,
601  "services.unittest.service.processes": 0,
602  "services.unittest.service.restart_usec": 0,
603  "services.unittest.service.state_change_timestamp": 0,
604  "services.unittest.service.status_errno": -69,
605  "services.unittest.service.tasks_current": 0,
606  "services.unittest.service.timeout_clean_usec": 0,
607  "services.unittest.service.watchdog_usec": 0,
608  "system-state": 3,
609  "timers.unittest.timer.accuracy_usec": 69,
610  "timers.unittest.timer.fixed_random_delay": 1,
611  "timers.unittest.timer.last_trigger_usec": 69,
612  "timers.unittest.timer.last_trigger_usec_monotonic": 69,
613  "timers.unittest.timer.next_elapse_usec_monotonic": 69,
614  "timers.unittest.timer.next_elapse_usec_realtime": 69,
615  "timers.unittest.timer.persistent": 0,
616  "timers.unittest.timer.randomized_delay_usec": 69,
617  "timers.unittest.timer.remain_after_elapse": 1,
618  "timers.unittest.timer.service_unit_last_state_change_usec": 69,
619  "timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
620  "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.active_state": 1,
621  "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.load_state": 1,
622  "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.unhealthy": 0,
623  "unit_states.unittest.service.active_state": 1,
624  "unit_states.unittest.service.load_state": 1,
625  "unit_states.unittest.service.time_in_state_usecs": 69,
626  "unit_states.unittest.service.unhealthy": 0,
627  "units.active_units": 0,
628  "units.automount_units": 0,
629  "units.device_units": 0,
630  "units.failed_units": 0,
631  "units.inactive_units": 0,
632  "units.jobs_queued": 0,
633  "units.loaded_units": 0,
634  "units.masked_units": 0,
635  "units.mount_units": 0,
636  "units.not_found_units": 0,
637  "units.path_units": 0,
638  "units.scope_units": 0,
639  "units.service_units": 0,
640  "units.slice_units": 0,
641  "units.socket_units": 0,
642  "units.target_units": 0,
643  "units.timer_persistent_units": 0,
644  "units.timer_remain_after_elapse": 0,
645  "units.timer_units": 0,
646  "units.total_units": 0,
647  "version": "255.7-1.fc40"
648}"###;
649
650    fn return_monitord_stats() -> MonitordStats {
651        let mut stats = MonitordStats {
652            networkd: networkd::NetworkdState {
653                interfaces_state: vec![networkd::InterfaceState {
654                    address_state: networkd::AddressState::routable,
655                    admin_state: networkd::AdminState::configured,
656                    carrier_state: networkd::CarrierState::carrier,
657                    ipv4_address_state: networkd::AddressState::routable,
658                    ipv6_address_state: networkd::AddressState::degraded,
659                    name: "eth0".to_string(),
660                    network_file: "/etc/systemd/network/69-eno4.network".to_string(),
661                    oper_state: networkd::OperState::routable,
662                    required_for_online: networkd::BoolState::True,
663                }],
664                managed_interfaces: 1,
665            },
666            pid1: Some(crate::pid1::Pid1Stats {
667                cpu_time_kernel: 69,
668                cpu_time_user: 69,
669                memory_usage_bytes: 69,
670                fd_count: 69,
671                tasks: 1,
672            }),
673            system_state: crate::system::SystemdSystemState::running,
674            units: crate::units::SystemdUnitStats::default(),
675            version: String::from("255.7-1.fc40")
676                .try_into()
677                .expect("Unable to make SystemdVersion struct"),
678            machines: HashMap::from([(String::from("foo"), MachineStats::default())]),
679            dbus_stats: None,
680        };
681        let service_unit_name = String::from("unittest.service");
682        stats.units.service_stats.insert(
683            service_unit_name.clone(),
684            units::ServiceStats {
685                // Ensure json-flat handles negative i32s
686                status_errno: -69,
687                ..Default::default()
688            },
689        );
690        stats.units.unit_states.insert(
691            String::from("unittest.service"),
692            units::UnitStates {
693                active_state: units::SystemdUnitActiveState::active,
694                load_state: units::SystemdUnitLoadState::loaded,
695                unhealthy: false,
696                time_in_state_usecs: Some(69),
697            },
698        );
699        let timer_unit = String::from("unittest.timer");
700        let timer_stats = timer::TimerStats {
701            accuracy_usec: 69,
702            fixed_random_delay: true,
703            last_trigger_usec: 69,
704            last_trigger_usec_monotonic: 69,
705            next_elapse_usec_monotonic: 69,
706            next_elapse_usec_realtime: 69,
707            persistent: false,
708            randomized_delay_usec: 69,
709            remain_after_elapse: true,
710            service_unit_last_state_change_usec: 69,
711            service_unit_last_state_change_usec_monotonic: 69,
712        };
713        stats
714            .units
715            .timer_stats
716            .insert(timer_unit.clone(), timer_stats.clone());
717        stats
718            .machines
719            .get_mut("foo")
720            .expect("No machine foo? WTF")
721            .units
722            .timer_stats
723            .insert(timer_unit, timer_stats);
724        // Ensure we escape keys correctly
725        stats.units.unit_states.insert(
726            String::from(
727                r"nvme\x2dWDC_CL_SN730_SDBQNTY\x2d512G\x2d2020_37222H80070511\x2dpart3.device",
728            ),
729            units::UnitStates {
730                active_state: units::SystemdUnitActiveState::active,
731                load_state: units::SystemdUnitLoadState::loaded,
732                unhealthy: false,
733                time_in_state_usecs: None,
734            },
735        );
736        stats
737    }
738
739    #[test]
740    fn test_flatten_map() {
741        let json_flat_map = flatten_stats(
742            &return_monitord_stats(),
743            &String::from("JSON serialize failed"),
744        );
745        assert_eq!(102, json_flat_map.len());
746    }
747
748    #[test]
749    fn test_flatten() {
750        let json_flat =
751            flatten(&return_monitord_stats(), &String::from("")).expect("JSON serialize failed");
752        assert_eq!(EXPECTED_FLAT_JSON, json_flat);
753    }
754
755    #[test]
756    fn test_flatten_prefixed() {
757        let json_flat = flatten(&return_monitord_stats(), &String::from("monitord"))
758            .expect("JSON serialize failed");
759        let json_flat_unserialized: BTreeMap<String, serde_json::Value> =
760            serde_json::from_str(&json_flat).expect("JSON from_str failed");
761        for (key, _value) in json_flat_unserialized.iter() {
762            assert!(key.starts_with("monitord."));
763        }
764    }
765}