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    ];
370
371    for (field_name, value) in fields {
372        if let Some(val) = value {
373            flat_stats.insert(format!("{base_metric_name}.{field_name}"), val.into());
374        }
375    }
376
377    if let Some(peer_accounting) = dbus_stats.peer_accounting() {
378        for peer in peer_accounting.values() {
379            let peer_name = peer.get_name();
380            let peer_fields = [
381                ("name_objects", peer.name_objects),
382                ("match_bytes", peer.match_bytes),
383                ("matches", peer.matches),
384                ("reply_objects", peer.reply_objects),
385                ("incoming_bytes", peer.incoming_bytes),
386                ("incoming_fds", peer.incoming_fds),
387                ("outgoing_bytes", peer.outgoing_bytes),
388                ("outgoing_fds", peer.outgoing_fds),
389                ("activation_request_bytes", peer.activation_request_bytes),
390                ("activation_request_fds", peer.activation_request_fds),
391            ];
392
393            for (field_name, value) in peer_fields {
394                if let Some(val) = value {
395                    flat_stats.insert(
396                        format!("{base_metric_name}.peer.{peer_name}.{field_name}"),
397                        val.into(),
398                    );
399                }
400            }
401        }
402    }
403
404    if let Some(cgroup_accounting) = dbus_stats.cgroup_accounting() {
405        for cgroup in cgroup_accounting.values() {
406            let cgroup_name = &cgroup.name;
407            let cgroup_fields = [
408                ("name_objects", cgroup.name_objects),
409                ("match_bytes", cgroup.match_bytes),
410                ("matches", cgroup.matches),
411                ("reply_objects", cgroup.reply_objects),
412                ("incoming_bytes", cgroup.incoming_bytes),
413                ("incoming_fds", cgroup.incoming_fds),
414                ("outgoing_bytes", cgroup.outgoing_bytes),
415                ("outgoing_fds", cgroup.outgoing_fds),
416                ("activation_request_bytes", cgroup.activation_request_bytes),
417                ("activation_request_fds", cgroup.activation_request_fds),
418            ];
419
420            for (field_name, value) in cgroup_fields {
421                if let Some(val) = value {
422                    flat_stats.insert(
423                        format!("{base_metric_name}.cgroup.{cgroup_name}.{field_name}"),
424                        val.into(),
425                    );
426                }
427            }
428        }
429    }
430
431    if let Some(user_accounting) = dbus_stats.user_accounting() {
432        // process user accounting if present
433        for user in user_accounting.values() {
434            let user_name = &user.username;
435            let user_fields = [
436                ("bytes", user.bytes.clone()),
437                ("fds", user.fds.clone()),
438                ("matches", user.matches.clone()),
439                ("objects", user.objects.clone()),
440            ];
441
442            for (field_name, value) in user_fields {
443                if let Some(val) = value {
444                    flat_stats.insert(
445                        format!("{base_metric_name}.user.{user_name}.{field_name}"),
446                        val.get_usage().into(),
447                    );
448                }
449            }
450        }
451    }
452
453    flat_stats
454}
455
456fn flatten_boot_blame(
457    optional_boot_blame: &Option<crate::boot::BootBlameStats>,
458    key_prefix: &str,
459) -> BTreeMap<String, serde_json::Value> {
460    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
461    let boot_blame_stats = match optional_boot_blame {
462        Some(bb) => bb,
463        None => {
464            debug!("Skipping flattening boot blame stats as we got None ...");
465            return flat_stats;
466        }
467    };
468
469    let base_metric_name = gen_base_metric_key(key_prefix, "boot.blame");
470
471    for (unit_name, activation_time) in boot_blame_stats.iter() {
472        let key = format!("{}.{}", base_metric_name, unit_name);
473        flat_stats.insert(key, (*activation_time).into());
474    }
475
476    flat_stats
477}
478
479fn flatten_verify_stats(
480    optional_verify_stats: &Option<crate::verify::VerifyStats>,
481    key_prefix: &str,
482) -> BTreeMap<String, serde_json::Value> {
483    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
484    let verify_stats = match optional_verify_stats {
485        Some(vs) => vs,
486        None => {
487            debug!("Skipping flattening verify stats as we got None ...");
488            return flat_stats;
489        }
490    };
491
492    let base_metric_name = gen_base_metric_key(key_prefix, "verify.failing");
493
494    // Add total count
495    flat_stats.insert(
496        format!("{base_metric_name}.total"),
497        verify_stats.total.into(),
498    );
499
500    // Add counts by type (only if they exist)
501    for (unit_type, count) in &verify_stats.by_type {
502        flat_stats.insert(format!("{base_metric_name}.{unit_type}"), (*count).into());
503    }
504
505    flat_stats
506}
507
508fn flatten_collector_timings(
509    timings: &[crate::CollectorTiming],
510    key_prefix: &str,
511) -> BTreeMap<String, serde_json::Value> {
512    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
513    let base_metric_name = gen_base_metric_key(key_prefix, "collector_timings");
514    for t in timings {
515        flat_stats.insert(
516            format!("{base_metric_name}.{}.start_offset_ms", t.name),
517            t.start_offset_ms.into(),
518        );
519        flat_stats.insert(
520            format!("{base_metric_name}.{}.elapsed_ms", t.name),
521            t.elapsed_ms.into(),
522        );
523        flat_stats.insert(
524            format!("{base_metric_name}.{}.success", t.name),
525            (if t.success { 1u64 } else { 0u64 }).into(),
526        );
527    }
528    flat_stats
529}
530
531fn flatten_units_collection_timings(
532    timings: &units::UnitsCollectionTimings,
533    key_prefix: &str,
534) -> BTreeMap<String, serde_json::Value> {
535    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
536    let base_metric_name = gen_base_metric_key(key_prefix, "collection_timings");
537    flat_stats.insert(
538        format!("{base_metric_name}.list_units_ms"),
539        timings.list_units_ms.into(),
540    );
541    flat_stats.insert(
542        format!("{base_metric_name}.per_unit_loop_ms"),
543        timings.per_unit_loop_ms.into(),
544    );
545    flat_stats.insert(
546        format!("{base_metric_name}.timer_dbus_fetches"),
547        timings.timer_dbus_fetches.into(),
548    );
549    flat_stats.insert(
550        format!("{base_metric_name}.state_dbus_fetches"),
551        timings.state_dbus_fetches.into(),
552    );
553    flat_stats.insert(
554        format!("{base_metric_name}.service_dbus_fetches"),
555        timings.service_dbus_fetches.into(),
556    );
557    flat_stats
558}
559
560/// Take the standard returned structs and move all to a flat BTreeMap<str, float|int> like JSON
561fn flatten_stats(
562    stats_struct: &MonitordStats,
563    key_prefix: &str,
564) -> BTreeMap<String, serde_json::Value> {
565    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
566    flat_stats.insert(
567        gen_base_metric_key(key_prefix, "stat_collection_run_time_ms"),
568        stats_struct.stat_collection_run_time_ms.into(),
569    );
570    flat_stats.extend(flatten_collector_timings(
571        &stats_struct.collector_timings,
572        key_prefix,
573    ));
574    flat_stats.extend(flatten_units_collection_timings(
575        &stats_struct.units.collection_timings,
576        key_prefix,
577    ));
578    flat_stats.extend(flatten_networkd(&stats_struct.networkd, key_prefix));
579    flat_stats.extend(flatten_pid1(&stats_struct.pid1, key_prefix));
580    flat_stats.insert(
581        gen_base_metric_key(key_prefix, "system-state"),
582        (stats_struct.system_state as u64).into(),
583    );
584    flat_stats.extend(flatten_services(
585        &stats_struct.units.service_stats,
586        key_prefix,
587    ));
588    flat_stats.extend(flatten_timers(&stats_struct.units.timer_stats, key_prefix));
589    flat_stats.extend(flatten_unit_states(
590        &stats_struct.units.unit_states,
591        key_prefix,
592    ));
593    flat_stats.extend(flatten_units(&stats_struct.units, key_prefix));
594    flat_stats.extend(flatten_unit_files(
595        &stats_struct.units.unit_files,
596        key_prefix,
597    ));
598    flat_stats.insert(
599        gen_base_metric_key(key_prefix, "version"),
600        stats_struct.version.to_string().into(),
601    );
602    flat_stats.extend(flatten_machines(&stats_struct.machines, key_prefix));
603    flat_stats.extend(flatten_dbus_stats(&stats_struct.dbus_stats, key_prefix));
604    flat_stats.extend(flatten_boot_blame(&stats_struct.boot_blame, key_prefix));
605    flat_stats.extend(flatten_verify_stats(&stats_struct.verify_stats, key_prefix));
606    flat_stats
607}
608
609/// Take the standard returned structs and move all to a flat JSON str
610pub fn flatten(
611    stats_struct: &MonitordStats,
612    key_prefix: &str,
613) -> Result<String, serde_json::Error> {
614    serde_json::to_string_pretty(&flatten_stats(stats_struct, key_prefix))
615}
616
617#[cfg(test)]
618mod tests {
619    use crate::timer;
620
621    use super::*;
622
623    // This will always be sorted / deterministic ...
624    const EXPECTED_FLAT_JSON: &str = r###"{
625  "boot.blame.cpe_chef.service": 103.05,
626  "boot.blame.dnf5-automatic.service": 204.159,
627  "boot.blame.sys-module-fuse.device": 16.21,
628  "collection_timings.list_units_ms": 5.0,
629  "collection_timings.per_unit_loop_ms": 37.0,
630  "collection_timings.service_dbus_fetches": 1,
631  "collection_timings.state_dbus_fetches": 0,
632  "collection_timings.timer_dbus_fetches": 4,
633  "collector_timings.boot_blame.elapsed_ms": 12.5,
634  "collector_timings.boot_blame.start_offset_ms": 0.25,
635  "collector_timings.boot_blame.success": 0,
636  "collector_timings.units.elapsed_ms": 42.0,
637  "collector_timings.units.start_offset_ms": 0.5,
638  "collector_timings.units.success": 1,
639  "machines.foo.collection_timings.list_units_ms": 0.0,
640  "machines.foo.collection_timings.per_unit_loop_ms": 0.0,
641  "machines.foo.collection_timings.service_dbus_fetches": 0,
642  "machines.foo.collection_timings.state_dbus_fetches": 0,
643  "machines.foo.collection_timings.timer_dbus_fetches": 0,
644  "machines.foo.networkd.managed_interfaces": 0,
645  "machines.foo.system-state": 0,
646  "machines.foo.timers.unittest.timer.accuracy_usec": 69,
647  "machines.foo.timers.unittest.timer.fixed_random_delay": 1,
648  "machines.foo.timers.unittest.timer.last_trigger_usec": 69,
649  "machines.foo.timers.unittest.timer.last_trigger_usec_monotonic": 69,
650  "machines.foo.timers.unittest.timer.next_elapse_usec_monotonic": 69,
651  "machines.foo.timers.unittest.timer.next_elapse_usec_realtime": 69,
652  "machines.foo.timers.unittest.timer.persistent": 0,
653  "machines.foo.timers.unittest.timer.randomized_delay_usec": 69,
654  "machines.foo.timers.unittest.timer.remain_after_elapse": 1,
655  "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec": 69,
656  "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
657  "machines.foo.units.activating_units": 0,
658  "machines.foo.units.active_units": 0,
659  "machines.foo.units.automount_units": 0,
660  "machines.foo.units.device_units": 0,
661  "machines.foo.units.failed_units": 0,
662  "machines.foo.units.inactive_units": 0,
663  "machines.foo.units.jobs_queued": 0,
664  "machines.foo.units.loaded_units": 0,
665  "machines.foo.units.masked_units": 0,
666  "machines.foo.units.mount_units": 0,
667  "machines.foo.units.not_found_units": 0,
668  "machines.foo.units.path_units": 0,
669  "machines.foo.units.scope_units": 0,
670  "machines.foo.units.service_units": 0,
671  "machines.foo.units.slice_units": 0,
672  "machines.foo.units.socket_units": 0,
673  "machines.foo.units.target_units": 0,
674  "machines.foo.units.timer_persistent_units": 0,
675  "machines.foo.units.timer_remain_after_elapse": 0,
676  "machines.foo.units.timer_units": 0,
677  "machines.foo.units.total_units": 0,
678  "networkd.eth0.address_state": 3,
679  "networkd.eth0.admin_state": 4,
680  "networkd.eth0.carrier_state": 5,
681  "networkd.eth0.ipv4_address_state": 3,
682  "networkd.eth0.ipv6_address_state": 2,
683  "networkd.eth0.oper_state": 9,
684  "networkd.eth0.required_for_online": 1,
685  "networkd.managed_interfaces": 1,
686  "pid1.cpu_time_kernel": 69,
687  "pid1.cpu_user_kernel": 69,
688  "pid1.fd_count": 69,
689  "pid1.memory_usage_bytes": 69,
690  "pid1.tasks": 1,
691  "services.unittest.service.active_enter_timestamp": 0,
692  "services.unittest.service.active_exit_timestamp": 0,
693  "services.unittest.service.cpuusage_nsec": 0,
694  "services.unittest.service.inactive_exit_timestamp": 0,
695  "services.unittest.service.ioread_bytes": 0,
696  "services.unittest.service.ioread_operations": 0,
697  "services.unittest.service.memory_available": 0,
698  "services.unittest.service.memory_current": 0,
699  "services.unittest.service.nrestarts": 0,
700  "services.unittest.service.processes": 0,
701  "services.unittest.service.restart_usec": 0,
702  "services.unittest.service.state_change_timestamp": 0,
703  "services.unittest.service.status_errno": -69,
704  "services.unittest.service.tasks_current": 0,
705  "services.unittest.service.timeout_clean_usec": 0,
706  "services.unittest.service.watchdog_usec": 0,
707  "stat_collection_run_time_ms": 69.0,
708  "system-state": 3,
709  "timers.unittest.timer.accuracy_usec": 69,
710  "timers.unittest.timer.fixed_random_delay": 1,
711  "timers.unittest.timer.last_trigger_usec": 69,
712  "timers.unittest.timer.last_trigger_usec_monotonic": 69,
713  "timers.unittest.timer.next_elapse_usec_monotonic": 69,
714  "timers.unittest.timer.next_elapse_usec_realtime": 69,
715  "timers.unittest.timer.persistent": 0,
716  "timers.unittest.timer.randomized_delay_usec": 69,
717  "timers.unittest.timer.remain_after_elapse": 1,
718  "timers.unittest.timer.service_unit_last_state_change_usec": 69,
719  "timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
720  "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.active_state": 1,
721  "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.load_state": 1,
722  "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.unhealthy": 0,
723  "unit_states.unittest.service.active_state": 1,
724  "unit_states.unittest.service.load_state": 1,
725  "unit_states.unittest.service.time_in_state_usecs": 69,
726  "unit_states.unittest.service.unhealthy": 0,
727  "units.activating_units": 0,
728  "units.active_units": 0,
729  "units.automount_units": 0,
730  "units.device_units": 0,
731  "units.failed_units": 0,
732  "units.inactive_units": 0,
733  "units.jobs_queued": 0,
734  "units.loaded_units": 0,
735  "units.masked_units": 0,
736  "units.mount_units": 0,
737  "units.not_found_units": 0,
738  "units.path_units": 0,
739  "units.scope_units": 0,
740  "units.service_units": 0,
741  "units.slice_units": 0,
742  "units.socket_units": 0,
743  "units.target_units": 0,
744  "units.timer_persistent_units": 0,
745  "units.timer_remain_after_elapse": 0,
746  "units.timer_units": 0,
747  "units.total_units": 0,
748  "verify.failing.service": 2,
749  "verify.failing.slice": 1,
750  "verify.failing.total": 3,
751  "version": "255.7-1.fc40"
752}"###;
753
754    fn return_monitord_stats() -> MonitordStats {
755        let mut stats = MonitordStats {
756            networkd: networkd::NetworkdState {
757                interfaces_state: vec![networkd::InterfaceState {
758                    address_state: networkd::AddressState::routable,
759                    admin_state: networkd::AdminState::configured,
760                    carrier_state: networkd::CarrierState::carrier,
761                    ipv4_address_state: networkd::AddressState::routable,
762                    ipv6_address_state: networkd::AddressState::degraded,
763                    name: "eth0".to_string(),
764                    network_file: "/etc/systemd/network/69-eno4.network".to_string(),
765                    oper_state: networkd::OperState::routable,
766                    required_for_online: networkd::BoolState::True,
767                }],
768                managed_interfaces: 1,
769            },
770            pid1: Some(crate::pid1::Pid1Stats {
771                cpu_time_kernel: 69,
772                cpu_time_user: 69,
773                memory_usage_bytes: 69,
774                fd_count: 69,
775                tasks: 1,
776            }),
777            system_state: crate::system::SystemdSystemState::running,
778            units: crate::units::SystemdUnitStats::default(),
779            version: String::from("255.7-1.fc40")
780                .try_into()
781                .expect("Unable to make SystemdVersion struct"),
782            machines: HashMap::from([(String::from("foo"), MachineStats::default())]),
783            dbus_stats: None,
784            boot_blame: None,
785            verify_stats: Some(crate::verify::VerifyStats {
786                total: 3,
787                by_type: HashMap::from([("service".to_string(), 2), ("slice".to_string(), 1)]),
788            }),
789            stat_collection_run_time_ms: 69.0,
790            collector_timings: vec![
791                crate::CollectorTiming {
792                    name: "units".to_string(),
793                    start_offset_ms: 0.5,
794                    elapsed_ms: 42.0,
795                    success: true,
796                },
797                crate::CollectorTiming {
798                    name: "boot_blame".to_string(),
799                    start_offset_ms: 0.25,
800                    elapsed_ms: 12.5,
801                    success: false,
802                },
803            ],
804        };
805        stats.units.collection_timings = units::UnitsCollectionTimings {
806            list_units_ms: 5.0,
807            unit_files_ms: 2.0,
808            per_unit_loop_ms: 37.0,
809            timer_dbus_fetches: 4,
810            state_dbus_fetches: 0,
811            service_dbus_fetches: 1,
812        };
813        let service_unit_name = String::from("unittest.service");
814        stats.units.service_stats.insert(
815            service_unit_name.clone(),
816            units::ServiceStats {
817                // Ensure json-flat handles negative i32s
818                status_errno: -69,
819                ..Default::default()
820            },
821        );
822        stats.units.unit_states.insert(
823            String::from("unittest.service"),
824            units::UnitStates {
825                active_state: units::SystemdUnitActiveState::active,
826                load_state: units::SystemdUnitLoadState::loaded,
827                unhealthy: false,
828                time_in_state_usecs: Some(69),
829            },
830        );
831        let timer_unit = String::from("unittest.timer");
832        let timer_stats = timer::TimerStats {
833            accuracy_usec: 69,
834            fixed_random_delay: true,
835            last_trigger_usec: 69,
836            last_trigger_usec_monotonic: 69,
837            next_elapse_usec_monotonic: 69,
838            next_elapse_usec_realtime: 69,
839            persistent: false,
840            randomized_delay_usec: 69,
841            remain_after_elapse: true,
842            service_unit_last_state_change_usec: 69,
843            service_unit_last_state_change_usec_monotonic: 69,
844        };
845        stats
846            .units
847            .timer_stats
848            .insert(timer_unit.clone(), timer_stats.clone());
849        stats
850            .machines
851            .get_mut("foo")
852            .expect("No machine foo? WTF")
853            .units
854            .timer_stats
855            .insert(timer_unit, timer_stats);
856        // Ensure we escape keys correctly
857        stats.units.unit_states.insert(
858            String::from(
859                r"nvme\x2dWDC_CL_SN730_SDBQNTY\x2d512G\x2d2020_37222H80070511\x2dpart3.device",
860            ),
861            units::UnitStates {
862                active_state: units::SystemdUnitActiveState::active,
863                load_state: units::SystemdUnitLoadState::loaded,
864                unhealthy: false,
865                time_in_state_usecs: None,
866            },
867        );
868        // Add boot blame stats
869        let mut boot_blame = crate::boot::BootBlameStats::new();
870        boot_blame.insert(String::from("dnf5-automatic.service"), 204.159);
871        boot_blame.insert(String::from("cpe_chef.service"), 103.050);
872        boot_blame.insert(String::from("sys-module-fuse.device"), 16.210);
873        stats.boot_blame = Some(boot_blame);
874        stats
875    }
876
877    #[test]
878    fn test_flatten_map() {
879        let json_flat_map = flatten_stats(&return_monitord_stats(), "");
880        assert_eq!(127, json_flat_map.len());
881    }
882
883    #[test]
884    fn test_flatten() {
885        let json_flat = flatten(&return_monitord_stats(), "").expect("JSON serialize failed");
886        assert_eq!(EXPECTED_FLAT_JSON, json_flat);
887    }
888
889    #[test]
890    fn test_flatten_prefixed() {
891        let json_flat =
892            flatten(&return_monitord_stats(), "monitord").expect("JSON serialize failed");
893        let json_flat_unserialized: BTreeMap<String, serde_json::Value> =
894            serde_json::from_str(&json_flat).expect("JSON from_str failed");
895        for (key, _value) in json_flat_unserialized.iter() {
896            assert!(key.starts_with("monitord."));
897        }
898    }
899
900    /// Ensure `UnitCounters` covers every scalar (non-hashmap) field of `SystemdUnitStats`.
901    ///
902    /// If a new counter field is added to `SystemdUnitStats` but not to `UnitCounters`
903    /// (and its `From` impl), this test will fail, preventing silent omissions from the
904    /// flat JSON output.
905    #[test]
906    fn test_unit_counters_covers_all_scalar_fields() {
907        // Fields of SystemdUnitStats that are nested maps, not scalar counters.
908        const NON_COUNTER_FIELDS: &[&str] = &[
909            "unit_files",
910            "service_stats",
911            "timer_stats",
912            "unit_states",
913            "collection_timings",
914        ];
915
916        // Scalar counter field names expected from SystemdUnitStats.
917        let expected: std::collections::BTreeSet<&str> = units::UNIT_FIELD_NAMES
918            .iter()
919            .copied()
920            .filter(|f| !NON_COUNTER_FIELDS.contains(f))
921            .collect();
922
923        // Field names actually present in UnitCounters (via serde serialization).
924        let counters_json =
925            serde_json::to_value(UnitCounters::from(&units::SystemdUnitStats::default()))
926                .expect("UnitCounters serialization failed");
927        let actual: std::collections::BTreeSet<&str> = counters_json
928            .as_object()
929            .expect("UnitCounters must serialize to a JSON object")
930            .keys()
931            .map(|s| s.as_str())
932            .collect();
933
934        assert_eq!(
935            expected,
936            actual,
937            "UnitCounters is out of sync with SystemdUnitStats scalar fields.\n\
938             Missing from UnitCounters: {:?}\n\
939             Extra in UnitCounters: {:?}",
940            expected.difference(&actual).collect::<Vec<_>>(),
941            actual.difference(&expected).collect::<Vec<_>>(),
942        );
943    }
944}