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_varlink_usage(
308            &stats.varlink_usage,
309            &machine_key_prefix,
310        ));
311        flat_stats.extend(flatten_units(&stats.units, &machine_key_prefix));
312        flat_stats.extend(flatten_unit_files(
313            &stats.units.unit_files,
314            &machine_key_prefix,
315        ));
316        flat_stats.extend(flatten_units_collection_timings(
317            &stats.units.collection_timings,
318            &machine_key_prefix,
319        ));
320        flat_stats.extend(flatten_pid1(&stats.pid1, &machine_key_prefix));
321        flat_stats.insert(
322            gen_base_metric_key(&machine_key_prefix, "system-state"),
323            (stats.system_state as u64).into(),
324        );
325        flat_stats.extend(flatten_services(
326            &stats.units.service_stats,
327            &machine_key_prefix,
328        ));
329        flat_stats.extend(flatten_timers(
330            &stats.units.timer_stats,
331            &machine_key_prefix,
332        ));
333        flat_stats.extend(flatten_boot_blame(&stats.boot_blame, &machine_key_prefix));
334        flat_stats.extend(flatten_verify_stats(
335            &stats.verify_stats,
336            &machine_key_prefix,
337        ));
338    }
339
340    flat_stats
341}
342
343fn flatten_dbus_stats(
344    optional_dbus_stats: &Option<dbus_stats::DBusStats>,
345    key_prefix: &str,
346) -> BTreeMap<String, serde_json::Value> {
347    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
348    let dbus_stats = match optional_dbus_stats {
349        Some(ds) => ds,
350        None => {
351            debug!("Skipping flattening dbus stats as we got None ...");
352            return flat_stats;
353        }
354    };
355
356    let base_metric_name = gen_base_metric_key(key_prefix, "dbus");
357    let fields = [
358        // ignore serial
359        ("active_connections", dbus_stats.active_connections),
360        ("incomplete_connections", dbus_stats.incomplete_connections),
361        ("bus_names", dbus_stats.bus_names),
362        ("peak_bus_names", dbus_stats.peak_bus_names),
363        (
364            "peak_bus_names_per_connection",
365            dbus_stats.peak_bus_names_per_connection,
366        ),
367        ("match_rules", dbus_stats.match_rules),
368        ("peak_match_rules", dbus_stats.peak_match_rules),
369        (
370            "peak_match_rules_per_connection",
371            dbus_stats.peak_match_rules_per_connection,
372        ),
373        ("stale_fds", dbus_stats.stale_fds),
374    ];
375
376    for (field_name, value) in fields {
377        if let Some(val) = value {
378            flat_stats.insert(format!("{base_metric_name}.{field_name}"), val.into());
379        }
380    }
381
382    if let Some(peer_accounting) = dbus_stats.peer_accounting() {
383        for peer in peer_accounting.values() {
384            let peer_name = peer.get_name();
385            let peer_fields = [
386                ("name_objects", peer.name_objects),
387                ("match_bytes", peer.match_bytes),
388                ("matches", peer.matches),
389                ("reply_objects", peer.reply_objects),
390                ("incoming_bytes", peer.incoming_bytes),
391                ("incoming_fds", peer.incoming_fds),
392                ("outgoing_bytes", peer.outgoing_bytes),
393                ("outgoing_fds", peer.outgoing_fds),
394                ("activation_request_bytes", peer.activation_request_bytes),
395                ("activation_request_fds", peer.activation_request_fds),
396            ];
397
398            for (field_name, value) in peer_fields {
399                if let Some(val) = value {
400                    flat_stats.insert(
401                        format!("{base_metric_name}.peer.{peer_name}.{field_name}"),
402                        val.into(),
403                    );
404                }
405            }
406        }
407    }
408
409    if let Some(cgroup_accounting) = dbus_stats.cgroup_accounting() {
410        for cgroup in cgroup_accounting.values() {
411            let cgroup_name = &cgroup.name;
412            let cgroup_fields = [
413                ("name_objects", cgroup.name_objects),
414                ("match_bytes", cgroup.match_bytes),
415                ("matches", cgroup.matches),
416                ("reply_objects", cgroup.reply_objects),
417                ("incoming_bytes", cgroup.incoming_bytes),
418                ("incoming_fds", cgroup.incoming_fds),
419                ("outgoing_bytes", cgroup.outgoing_bytes),
420                ("outgoing_fds", cgroup.outgoing_fds),
421                ("activation_request_bytes", cgroup.activation_request_bytes),
422                ("activation_request_fds", cgroup.activation_request_fds),
423            ];
424
425            for (field_name, value) in cgroup_fields {
426                if let Some(val) = value {
427                    flat_stats.insert(
428                        format!("{base_metric_name}.cgroup.{cgroup_name}.{field_name}"),
429                        val.into(),
430                    );
431                }
432            }
433        }
434    }
435
436    if let Some(user_accounting) = dbus_stats.user_accounting() {
437        // process user accounting if present
438        for user in user_accounting.values() {
439            let user_name = &user.username;
440            let user_fields = [
441                ("bytes", user.bytes.clone()),
442                ("fds", user.fds.clone()),
443                ("matches", user.matches.clone()),
444                ("objects", user.objects.clone()),
445            ];
446
447            for (field_name, value) in user_fields {
448                if let Some(val) = value {
449                    flat_stats.insert(
450                        format!("{base_metric_name}.user.{user_name}.{field_name}"),
451                        val.get_usage().into(),
452                    );
453                }
454            }
455
456            if let Some(stale_fds) = user.stale_fds {
457                flat_stats.insert(
458                    format!("{base_metric_name}.user.{user_name}.stale_fds"),
459                    stale_fds.into(),
460                );
461            }
462        }
463    }
464
465    flat_stats
466}
467
468fn flatten_boot_blame(
469    optional_boot_blame: &Option<crate::boot::BootBlameStats>,
470    key_prefix: &str,
471) -> BTreeMap<String, serde_json::Value> {
472    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
473    let boot_blame_stats = match optional_boot_blame {
474        Some(bb) => bb,
475        None => {
476            debug!("Skipping flattening boot blame stats as we got None ...");
477            return flat_stats;
478        }
479    };
480
481    let base_metric_name = gen_base_metric_key(key_prefix, "boot.blame");
482
483    for (unit_name, activation_time) in boot_blame_stats.iter() {
484        let key = format!("{}.{}", base_metric_name, unit_name);
485        flat_stats.insert(key, (*activation_time).into());
486    }
487
488    flat_stats
489}
490
491fn flatten_verify_stats(
492    optional_verify_stats: &Option<crate::verify::VerifyStats>,
493    key_prefix: &str,
494) -> BTreeMap<String, serde_json::Value> {
495    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
496    let verify_stats = match optional_verify_stats {
497        Some(vs) => vs,
498        None => {
499            debug!("Skipping flattening verify stats as we got None ...");
500            return flat_stats;
501        }
502    };
503
504    let base_metric_name = gen_base_metric_key(key_prefix, "verify.failing");
505
506    // Add total count
507    flat_stats.insert(
508        format!("{base_metric_name}.total"),
509        verify_stats.total.into(),
510    );
511
512    // Add counts by type (only if they exist)
513    for (unit_type, count) in &verify_stats.by_type {
514        flat_stats.insert(format!("{base_metric_name}.{unit_type}"), (*count).into());
515    }
516
517    flat_stats
518}
519
520/// Emit one `varlink_usage.<collector>` 0/1 gauge per collector that ran.
521///
522/// Only collectors whose `Option<CollectorTransport>` is `Some` emit a gauge:
523/// disabled collectors (and any re-validation the caller does over the
524/// enabled sections) stay absent, so `count()` over the `varlink_usage_*`
525/// gauges is the enabled set and `sum() / count()` is the share of
526/// collectors served by varlink this run. Downstream metric consumers (e.g.
527/// monitord-exporter) aggregate the gauges from there; monitord itself only
528/// makes them available in its output formats.
529fn flatten_varlink_usage(
530    usage: &crate::VarlinkUsage,
531    key_prefix: &str,
532) -> BTreeMap<String, serde_json::Value> {
533    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
534    let base_metric_name = gen_base_metric_key(key_prefix, "varlink_usage");
535    let fields: [(&str, Option<crate::CollectorTransport>); 7] = [
536        ("version", usage.version),
537        ("system_state", usage.system_state),
538        ("units", usage.units),
539        ("networkd", usage.networkd),
540        ("machines", usage.machines),
541        ("boot_blame", usage.boot_blame),
542        ("verify", usage.verify),
543    ];
544    for (name, transport) in fields {
545        if let Some(transport) = transport {
546            flat_stats.insert(
547                format!("{base_metric_name}.{name}"),
548                transport.as_u64().into(),
549            );
550        }
551    }
552    flat_stats
553}
554
555fn flatten_collector_timings(
556    timings: &[crate::CollectorTiming],
557    key_prefix: &str,
558) -> BTreeMap<String, serde_json::Value> {
559    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
560    let base_metric_name = gen_base_metric_key(key_prefix, "collector_timings");
561    for t in timings {
562        flat_stats.insert(
563            format!("{base_metric_name}.{}.start_offset_ms", t.name),
564            t.start_offset_ms.into(),
565        );
566        flat_stats.insert(
567            format!("{base_metric_name}.{}.elapsed_ms", t.name),
568            t.elapsed_ms.into(),
569        );
570        flat_stats.insert(
571            format!("{base_metric_name}.{}.success", t.name),
572            (if t.success { 1u64 } else { 0u64 }).into(),
573        );
574    }
575    flat_stats
576}
577
578fn flatten_units_collection_timings(
579    timings: &units::UnitsCollectionTimings,
580    key_prefix: &str,
581) -> BTreeMap<String, serde_json::Value> {
582    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
583    let base_metric_name = gen_base_metric_key(key_prefix, "collection_timings");
584    flat_stats.insert(
585        format!("{base_metric_name}.list_units_ms"),
586        timings.list_units_ms.into(),
587    );
588    flat_stats.insert(
589        format!("{base_metric_name}.per_unit_loop_ms"),
590        timings.per_unit_loop_ms.into(),
591    );
592    flat_stats.insert(
593        format!("{base_metric_name}.timer_dbus_fetches"),
594        timings.timer_dbus_fetches.into(),
595    );
596    flat_stats.insert(
597        format!("{base_metric_name}.state_dbus_fetches"),
598        timings.state_dbus_fetches.into(),
599    );
600    flat_stats.insert(
601        format!("{base_metric_name}.service_dbus_fetches"),
602        timings.service_dbus_fetches.into(),
603    );
604    // Rank is encoded in the key (not just the unit name) so ordering survives
605    // flattening; the leaf value stays a plain number like every other key here.
606    for (idx, (unit_name, duration_ms)) in timings.slowest_units.iter().enumerate() {
607        flat_stats.insert(
608            format!("{base_metric_name}.slowest_units.{idx}.{unit_name}"),
609            (*duration_ms).into(),
610        );
611    }
612    flat_stats
613}
614
615/// Take the standard returned structs and move all to a flat BTreeMap<str, float|int> like JSON
616fn flatten_stats(
617    stats_struct: &MonitordStats,
618    key_prefix: &str,
619) -> BTreeMap<String, serde_json::Value> {
620    let mut flat_stats: BTreeMap<String, serde_json::Value> = BTreeMap::new();
621    flat_stats.insert(
622        gen_base_metric_key(key_prefix, "stat_collection_run_time_ms"),
623        stats_struct.stat_collection_run_time_ms.into(),
624    );
625    flat_stats.extend(flatten_collector_timings(
626        &stats_struct.collector_timings,
627        key_prefix,
628    ));
629    flat_stats.extend(flatten_varlink_usage(
630        &stats_struct.varlink_usage,
631        key_prefix,
632    ));
633    flat_stats.extend(flatten_units_collection_timings(
634        &stats_struct.units.collection_timings,
635        key_prefix,
636    ));
637    flat_stats.extend(flatten_networkd(&stats_struct.networkd, key_prefix));
638    flat_stats.extend(flatten_pid1(&stats_struct.pid1, key_prefix));
639    flat_stats.insert(
640        gen_base_metric_key(key_prefix, "system-state"),
641        (stats_struct.system_state as u64).into(),
642    );
643    flat_stats.extend(flatten_services(
644        &stats_struct.units.service_stats,
645        key_prefix,
646    ));
647    flat_stats.extend(flatten_timers(&stats_struct.units.timer_stats, key_prefix));
648    flat_stats.extend(flatten_unit_states(
649        &stats_struct.units.unit_states,
650        key_prefix,
651    ));
652    flat_stats.extend(flatten_units(&stats_struct.units, key_prefix));
653    flat_stats.extend(flatten_unit_files(
654        &stats_struct.units.unit_files,
655        key_prefix,
656    ));
657    flat_stats.insert(
658        gen_base_metric_key(key_prefix, "version"),
659        stats_struct.version.to_string().into(),
660    );
661    flat_stats.extend(flatten_machines(&stats_struct.machines, key_prefix));
662    flat_stats.extend(flatten_dbus_stats(&stats_struct.dbus_stats, key_prefix));
663    flat_stats.extend(flatten_boot_blame(&stats_struct.boot_blame, key_prefix));
664    flat_stats.extend(flatten_verify_stats(&stats_struct.verify_stats, key_prefix));
665    flat_stats
666}
667
668/// Take the standard returned structs and move all to a flat JSON str
669pub fn flatten(
670    stats_struct: &MonitordStats,
671    key_prefix: &str,
672) -> Result<String, serde_json::Error> {
673    serde_json::to_string_pretty(&flatten_stats(stats_struct, key_prefix))
674}
675
676#[cfg(test)]
677mod tests {
678    use crate::timer;
679
680    use super::*;
681
682    // This will always be sorted / deterministic ...
683    const EXPECTED_FLAT_JSON: &str = r###"{
684  "boot.blame.cpe_chef.service": 103.05,
685  "boot.blame.dnf5-automatic.service": 204.159,
686  "boot.blame.sys-module-fuse.device": 16.21,
687  "collection_timings.list_units_ms": 5.0,
688  "collection_timings.per_unit_loop_ms": 37.0,
689  "collection_timings.service_dbus_fetches": 1,
690  "collection_timings.slowest_units.0.unittest.service": 12.5,
691  "collection_timings.slowest_units.1.unittest.timer": 8.25,
692  "collection_timings.state_dbus_fetches": 0,
693  "collection_timings.timer_dbus_fetches": 4,
694  "collector_timings.boot_blame.elapsed_ms": 12.5,
695  "collector_timings.boot_blame.start_offset_ms": 0.25,
696  "collector_timings.boot_blame.success": 0,
697  "collector_timings.units.elapsed_ms": 42.0,
698  "collector_timings.units.start_offset_ms": 0.5,
699  "collector_timings.units.success": 1,
700  "machines.foo.collection_timings.list_units_ms": 0.0,
701  "machines.foo.collection_timings.per_unit_loop_ms": 0.0,
702  "machines.foo.collection_timings.service_dbus_fetches": 0,
703  "machines.foo.collection_timings.state_dbus_fetches": 0,
704  "machines.foo.collection_timings.timer_dbus_fetches": 0,
705  "machines.foo.networkd.managed_interfaces": 0,
706  "machines.foo.system-state": 0,
707  "machines.foo.timers.unittest.timer.accuracy_usec": 69,
708  "machines.foo.timers.unittest.timer.fixed_random_delay": 1,
709  "machines.foo.timers.unittest.timer.last_trigger_usec": 69,
710  "machines.foo.timers.unittest.timer.last_trigger_usec_monotonic": 69,
711  "machines.foo.timers.unittest.timer.next_elapse_usec_monotonic": 69,
712  "machines.foo.timers.unittest.timer.next_elapse_usec_realtime": 69,
713  "machines.foo.timers.unittest.timer.persistent": 0,
714  "machines.foo.timers.unittest.timer.randomized_delay_usec": 69,
715  "machines.foo.timers.unittest.timer.remain_after_elapse": 1,
716  "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec": 69,
717  "machines.foo.timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
718  "machines.foo.units.activating_units": 0,
719  "machines.foo.units.active_units": 0,
720  "machines.foo.units.automount_units": 0,
721  "machines.foo.units.device_units": 0,
722  "machines.foo.units.failed_units": 0,
723  "machines.foo.units.inactive_units": 0,
724  "machines.foo.units.jobs_queued": 0,
725  "machines.foo.units.loaded_units": 0,
726  "machines.foo.units.masked_units": 0,
727  "machines.foo.units.mount_units": 0,
728  "machines.foo.units.not_found_units": 0,
729  "machines.foo.units.path_units": 0,
730  "machines.foo.units.scope_units": 0,
731  "machines.foo.units.service_units": 0,
732  "machines.foo.units.slice_units": 0,
733  "machines.foo.units.socket_units": 0,
734  "machines.foo.units.target_units": 0,
735  "machines.foo.units.timer_persistent_units": 0,
736  "machines.foo.units.timer_remain_after_elapse": 0,
737  "machines.foo.units.timer_units": 0,
738  "machines.foo.units.total_units": 0,
739  "networkd.eth0.address_state": 3,
740  "networkd.eth0.admin_state": 4,
741  "networkd.eth0.carrier_state": 5,
742  "networkd.eth0.ipv4_address_state": 3,
743  "networkd.eth0.ipv6_address_state": 2,
744  "networkd.eth0.oper_state": 9,
745  "networkd.eth0.required_for_online": 1,
746  "networkd.managed_interfaces": 1,
747  "pid1.cpu_time_kernel": 69,
748  "pid1.cpu_user_kernel": 69,
749  "pid1.fd_count": 69,
750  "pid1.memory_usage_bytes": 69,
751  "pid1.tasks": 1,
752  "services.unittest.service.active_enter_timestamp": 0,
753  "services.unittest.service.active_exit_timestamp": 0,
754  "services.unittest.service.cpuusage_nsec": 0,
755  "services.unittest.service.inactive_exit_timestamp": 0,
756  "services.unittest.service.ioread_bytes": 0,
757  "services.unittest.service.ioread_operations": 0,
758  "services.unittest.service.memory_available": 0,
759  "services.unittest.service.memory_current": 0,
760  "services.unittest.service.nrestarts": 0,
761  "services.unittest.service.processes": 0,
762  "services.unittest.service.restart_usec": 0,
763  "services.unittest.service.state_change_timestamp": 0,
764  "services.unittest.service.status_errno": -69,
765  "services.unittest.service.tasks_current": 0,
766  "services.unittest.service.timeout_clean_usec": 0,
767  "services.unittest.service.watchdog_usec": 0,
768  "stat_collection_run_time_ms": 69.0,
769  "system-state": 3,
770  "timers.unittest.timer.accuracy_usec": 69,
771  "timers.unittest.timer.fixed_random_delay": 1,
772  "timers.unittest.timer.last_trigger_usec": 69,
773  "timers.unittest.timer.last_trigger_usec_monotonic": 69,
774  "timers.unittest.timer.next_elapse_usec_monotonic": 69,
775  "timers.unittest.timer.next_elapse_usec_realtime": 69,
776  "timers.unittest.timer.persistent": 0,
777  "timers.unittest.timer.randomized_delay_usec": 69,
778  "timers.unittest.timer.remain_after_elapse": 1,
779  "timers.unittest.timer.service_unit_last_state_change_usec": 69,
780  "timers.unittest.timer.service_unit_last_state_change_usec_monotonic": 69,
781  "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.active_state": 1,
782  "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.load_state": 1,
783  "unit_states.nvme\\x2dWDC_CL_SN730_SDBQNTY\\x2d512G\\x2d2020_37222H80070511\\x2dpart3.device.unhealthy": 0,
784  "unit_states.unittest.service.active_state": 1,
785  "unit_states.unittest.service.load_state": 1,
786  "unit_states.unittest.service.time_in_state_usecs": 69,
787  "unit_states.unittest.service.unhealthy": 0,
788  "units.activating_units": 0,
789  "units.active_units": 0,
790  "units.automount_units": 0,
791  "units.device_units": 0,
792  "units.failed_units": 0,
793  "units.inactive_units": 0,
794  "units.jobs_queued": 0,
795  "units.loaded_units": 0,
796  "units.masked_units": 0,
797  "units.mount_units": 0,
798  "units.not_found_units": 0,
799  "units.path_units": 0,
800  "units.scope_units": 0,
801  "units.service_units": 0,
802  "units.slice_units": 0,
803  "units.socket_units": 0,
804  "units.target_units": 0,
805  "units.timer_persistent_units": 0,
806  "units.timer_remain_after_elapse": 0,
807  "units.timer_units": 0,
808  "units.total_units": 0,
809  "varlink_usage.boot_blame": 1,
810  "varlink_usage.machines": 0,
811  "varlink_usage.networkd": 1,
812  "varlink_usage.system_state": 1,
813  "varlink_usage.units": 0,
814  "varlink_usage.verify": 0,
815  "varlink_usage.version": 1,
816  "verify.failing.service": 2,
817  "verify.failing.slice": 1,
818  "verify.failing.total": 3,
819  "version": "255.7-1.fc40"
820}"###;
821
822    fn return_monitord_stats() -> MonitordStats {
823        let mut stats = MonitordStats {
824            networkd: networkd::NetworkdState {
825                interfaces_state: vec![networkd::InterfaceState {
826                    address_state: networkd::AddressState::routable,
827                    admin_state: networkd::AdminState::configured,
828                    carrier_state: networkd::CarrierState::carrier,
829                    ipv4_address_state: networkd::AddressState::routable,
830                    ipv6_address_state: networkd::AddressState::degraded,
831                    name: "eth0".to_string(),
832                    network_file: "/etc/systemd/network/69-eno4.network".to_string(),
833                    oper_state: networkd::OperState::routable,
834                    required_for_online: networkd::BoolState::True,
835                }],
836                managed_interfaces: 1,
837            },
838            pid1: Some(crate::pid1::Pid1Stats {
839                cpu_time_kernel: 69,
840                cpu_time_user: 69,
841                memory_usage_bytes: 69,
842                fd_count: 69,
843                tasks: 1,
844            }),
845            system_state: crate::system::SystemdSystemState::running,
846            units: crate::units::SystemdUnitStats::default(),
847            version: String::from("255.7-1.fc40")
848                .try_into()
849                .expect("Unable to make SystemdVersion struct"),
850            machines: HashMap::from([(String::from("foo"), MachineStats::default())]),
851            dbus_stats: None,
852            boot_blame: None,
853            verify_stats: Some(crate::verify::VerifyStats {
854                total: 3,
855                by_type: HashMap::from([("service".to_string(), 2), ("slice".to_string(), 1)]),
856            }),
857            stat_collection_run_time_ms: 69.0,
858            collector_timings: vec![
859                crate::CollectorTiming {
860                    name: "units".to_string(),
861                    start_offset_ms: 0.5,
862                    elapsed_ms: 42.0,
863                    success: true,
864                },
865                crate::CollectorTiming {
866                    name: "boot_blame".to_string(),
867                    start_offset_ms: 0.25,
868                    elapsed_ms: 12.5,
869                    success: false,
870                },
871            ],
872            // Exhaustive init (no `..Default::default()`): adding a new
873            // gauge forces this fixture — and EXPECTED_FLAT_JSON — to be
874            // updated, so the example flat output can't silently go stale.
875            varlink_usage: crate::VarlinkUsage {
876                version: Some(crate::CollectorTransport::Varlink),
877                system_state: Some(crate::CollectorTransport::Varlink),
878                units: Some(crate::CollectorTransport::Dbus),
879                networkd: Some(crate::CollectorTransport::Varlink),
880                machines: Some(crate::CollectorTransport::Dbus),
881                boot_blame: Some(crate::CollectorTransport::Varlink),
882                verify: Some(crate::CollectorTransport::Dbus),
883            },
884        };
885        stats.units.collection_timings = units::UnitsCollectionTimings {
886            list_units_ms: 5.0,
887            unit_files_ms: 2.0,
888            per_unit_loop_ms: 37.0,
889            timer_dbus_fetches: 4,
890            state_dbus_fetches: 0,
891            service_dbus_fetches: 1,
892            slowest_units: vec![
893                ("unittest.service".to_string(), 12.5),
894                ("unittest.timer".to_string(), 8.25),
895            ],
896        };
897        let service_unit_name = String::from("unittest.service");
898        stats.units.service_stats.insert(
899            service_unit_name.clone(),
900            units::ServiceStats {
901                // Ensure json-flat handles negative i32s
902                status_errno: -69,
903                ..Default::default()
904            },
905        );
906        stats.units.unit_states.insert(
907            String::from("unittest.service"),
908            units::UnitStates {
909                active_state: units::SystemdUnitActiveState::active,
910                load_state: units::SystemdUnitLoadState::loaded,
911                unhealthy: false,
912                time_in_state_usecs: Some(69),
913            },
914        );
915        let timer_unit = String::from("unittest.timer");
916        let timer_stats = timer::TimerStats {
917            accuracy_usec: 69,
918            fixed_random_delay: true,
919            last_trigger_usec: 69,
920            last_trigger_usec_monotonic: 69,
921            next_elapse_usec_monotonic: 69,
922            next_elapse_usec_realtime: 69,
923            persistent: false,
924            randomized_delay_usec: 69,
925            remain_after_elapse: true,
926            service_unit_last_state_change_usec: 69,
927            service_unit_last_state_change_usec_monotonic: 69,
928        };
929        stats
930            .units
931            .timer_stats
932            .insert(timer_unit.clone(), timer_stats.clone());
933        stats
934            .machines
935            .get_mut("foo")
936            .expect("No machine foo? WTF")
937            .units
938            .timer_stats
939            .insert(timer_unit, timer_stats);
940        // Ensure we escape keys correctly
941        stats.units.unit_states.insert(
942            String::from(
943                r"nvme\x2dWDC_CL_SN730_SDBQNTY\x2d512G\x2d2020_37222H80070511\x2dpart3.device",
944            ),
945            units::UnitStates {
946                active_state: units::SystemdUnitActiveState::active,
947                load_state: units::SystemdUnitLoadState::loaded,
948                unhealthy: false,
949                time_in_state_usecs: None,
950            },
951        );
952        // Add boot blame stats
953        let mut boot_blame = crate::boot::BootBlameStats::new();
954        boot_blame.insert(String::from("dnf5-automatic.service"), 204.159);
955        boot_blame.insert(String::from("cpe_chef.service"), 103.050);
956        boot_blame.insert(String::from("sys-module-fuse.device"), 16.210);
957        stats.boot_blame = Some(boot_blame);
958        stats
959    }
960
961    #[test]
962    fn test_flatten_map() {
963        let json_flat_map = flatten_stats(&return_monitord_stats(), "");
964        assert_eq!(136, json_flat_map.len());
965    }
966
967    #[test]
968    fn test_collector_transport_serializes_as_integer_everywhere() {
969        // The repr discriminants are the gauge values, so json/json-pretty
970        // report the same integers as json-flat — not strings.
971        let usage = crate::VarlinkUsage {
972            version: Some(crate::CollectorTransport::Varlink),
973            units: Some(crate::CollectorTransport::Dbus),
974            ..Default::default()
975        };
976        let value = serde_json::to_value(&usage).expect("serialize");
977        assert_eq!(value["version"], serde_json::Value::from(1u64));
978        assert_eq!(value["units"], serde_json::Value::from(0u64));
979        assert!(value.get("networkd").is_none());
980    }
981
982    #[test]
983    fn test_flatten_varlink_usage_only_emits_ran_collectors() {
984        // Disabled collectors stay None and emit no gauge: count() over the
985        // gauges is the enabled set, which is what makes sum/count a valid
986        // Grafana adoption ratio without a separate enabled counter.
987        let flat = flatten_varlink_usage(
988            &crate::VarlinkUsage {
989                version: Some(crate::CollectorTransport::Varlink),
990                units: Some(crate::CollectorTransport::Dbus),
991                ..Default::default()
992            },
993            "",
994        );
995        assert_eq!(flat.len(), 2);
996        assert_eq!(flat["varlink_usage.version"], serde_json::Value::from(1u64));
997        assert_eq!(flat["varlink_usage.units"], serde_json::Value::from(0u64));
998        assert!(flatten_varlink_usage(&crate::VarlinkUsage::default(), "").is_empty());
999    }
1000
1001    #[test]
1002    fn test_flatten() {
1003        let json_flat = flatten(&return_monitord_stats(), "").expect("JSON serialize failed");
1004        assert_eq!(EXPECTED_FLAT_JSON, json_flat);
1005    }
1006
1007    #[test]
1008    fn test_flatten_prefixed() {
1009        let json_flat =
1010            flatten(&return_monitord_stats(), "monitord").expect("JSON serialize failed");
1011        let json_flat_unserialized: BTreeMap<String, serde_json::Value> =
1012            serde_json::from_str(&json_flat).expect("JSON from_str failed");
1013        for (key, _value) in json_flat_unserialized.iter() {
1014            assert!(key.starts_with("monitord."));
1015        }
1016    }
1017
1018    /// Ensure `UnitCounters` covers every scalar (non-hashmap) field of `SystemdUnitStats`.
1019    ///
1020    /// If a new counter field is added to `SystemdUnitStats` but not to `UnitCounters`
1021    /// (and its `From` impl), this test will fail, preventing silent omissions from the
1022    /// flat JSON output.
1023    #[test]
1024    fn test_unit_counters_covers_all_scalar_fields() {
1025        // Fields of SystemdUnitStats that are nested maps, not scalar counters.
1026        const NON_COUNTER_FIELDS: &[&str] = &[
1027            "unit_files",
1028            "service_stats",
1029            "timer_stats",
1030            "unit_states",
1031            "collection_timings",
1032        ];
1033
1034        // Scalar counter field names expected from SystemdUnitStats.
1035        let expected: std::collections::BTreeSet<&str> = units::UNIT_FIELD_NAMES
1036            .iter()
1037            .copied()
1038            .filter(|f| !NON_COUNTER_FIELDS.contains(f))
1039            .collect();
1040
1041        // Field names actually present in UnitCounters (via serde serialization).
1042        let counters_json =
1043            serde_json::to_value(UnitCounters::from(&units::SystemdUnitStats::default()))
1044                .expect("UnitCounters serialization failed");
1045        let actual: std::collections::BTreeSet<&str> = counters_json
1046            .as_object()
1047            .expect("UnitCounters must serialize to a JSON object")
1048            .keys()
1049            .map(|s| s.as_str())
1050            .collect();
1051
1052        assert_eq!(
1053            expected,
1054            actual,
1055            "UnitCounters is out of sync with SystemdUnitStats scalar fields.\n\
1056             Missing from UnitCounters: {:?}\n\
1057             Extra in UnitCounters: {:?}",
1058            expected.difference(&actual).collect::<Vec<_>>(),
1059            actual.difference(&expected).collect::<Vec<_>>(),
1060        );
1061    }
1062}