Skip to main content

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