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