Skip to main content

monitord/
varlink_fallback.rs

1//! # varlink_fallback module
2//!
3//! The single enforcement point for `[varlink] no_fallback` (see #37).
4//!
5//! Every varlink-first collector funnels its failure through
6//! [`report_varlink_failure`]: by default it logs the familiar `falling back`
7//! warning and the caller proceeds to its D-Bus (or, for networkd,
8//! file-based) fallback; with `[varlink] no_fallback = true` it returns an
9//! error instead, so the collector fails loudly and CI proves the collector
10//! set is varlink-clean rather than silently D-Bus-served.
11//!
12//! Scope notes (so the "varlink-clean" claim is not over-read): `[dbus]`
13//! stats have no varlink path (they are statistics about the D-Bus daemon
14//! itself), so they never report a fallback — they just use the bus. And skipped-but-warned metric
15//! parses are not fallbacks either, so a clean run can still under-report;
16//! completeness assertions cover that, not this flag.
17//!
18//! The `collector` argument names the collector for the log line (e.g.
19//! `"units"`, `"networkd"`, `"container <name> units"`); `fallback` names
20//! what would have served the data (`"D-Bus"`, `"file-based"`). Keeping both
21//! as plain strings keeps this module dependency-free and lets container
22//! paths interpolate the machine name without a structured type.
23
24use thiserror::Error;
25
26#[derive(Error, Debug)]
27pub enum NoFallbackError {
28    #[error(
29        "varlink {collector} failed and [varlink] no_fallback=true forbids the {fallback} fallback: {source:?}"
30    )]
31    FallbackForbidden {
32        collector: String,
33        fallback: String,
34        source: anyhow::Error,
35    },
36}
37
38/// Record a varlink failure and decide whether the caller may fall back.
39///
40/// Returns `Ok(())` (after logging the `falling back` warning) by default,
41/// so the caller proceeds to its fallback. Returns `Err` when `no_fallback`
42/// is true, turning the failure into the collector's error — which
43/// `stat_collector` logs per-collector and counts in `collector_timings`
44/// `success`, so a no-fallback CI run fails visibly rather than earning a
45/// quiet `varlink_usage.* = 0` gauge.
46///
47/// `source` is taken as `anyhow::Error` because every varlink call site
48/// already produces one (sockets, protocol and version-gate failures all
49/// funnel through `anyhow`).
50pub fn report_varlink_failure(
51    no_fallback: bool,
52    collector: &str,
53    fallback: &str,
54    source: anyhow::Error,
55) -> Result<(), NoFallbackError> {
56    if no_fallback {
57        return Err(NoFallbackError::FallbackForbidden {
58            collector: collector.to_string(),
59            fallback: fallback.to_string(),
60            source,
61        });
62    }
63    // `fallback` is passed in display form ("D-Bus", "file-based"), so no
64    // case transform here — #222 was specifically about these lines
65    // reading well.
66    tracing::warn!(
67        "Varlink {} failed, falling back to {}: {:?}",
68        collector,
69        fallback,
70        source,
71    );
72    Ok(())
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_fallback_allowed_by_default() {
81        assert!(report_varlink_failure(false, "units", "D-Bus", anyhow::anyhow!("boom")).is_ok());
82    }
83
84    #[test]
85    fn test_no_fallback_forbids_fallback_with_context() {
86        let err = report_varlink_failure(
87            true,
88            "container demo units",
89            "D-Bus",
90            anyhow::anyhow!("boom"),
91        )
92        .expect_err("no_fallback must forbid the fallback");
93        let rendered = err.to_string();
94        assert!(
95            rendered.contains("container demo units"),
96            "error names the collector: {rendered}"
97        );
98        assert!(
99            rendered.contains("D-Bus"),
100            "error names the forbidden fallback: {rendered}"
101        );
102    }
103}