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}