Skip to main content

faucet_cli/schedule/
metrics.rs

1//! Scheduler metrics, emitted via the `metrics` facade. `pipeline` is the only
2//! label (low cardinality); per-row outcomes are covered by the pipeline-run
3//! metrics in `faucet-core`. No-ops when no recorder is installed.
4
5use chrono::{DateTime, Utc};
6use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram};
7use std::time::Duration;
8
9/// Register HELP text for every scheduler metric. Idempotent — safe to call
10/// more than once. Called from the scheduler loop at startup so the series'
11/// descriptions are present in `/metrics` from t=0, even before the first tick.
12pub fn describe() {
13    describe_counter!(
14        "faucet_schedule_runs_total",
15        "Scheduled pipeline runs, by outcome (ok|err|skipped)."
16    );
17    describe_counter!(
18        "faucet_schedule_overlaps_total",
19        "Scheduler ticks that overlapped an in-flight run, by policy (skip|queue|forbid)."
20    );
21    describe_gauge!(
22        "faucet_schedule_next_tick_unix_seconds",
23        "Unix timestamp of the next scheduled tick."
24    );
25    describe_gauge!(
26        "faucet_schedule_runs_in_flight",
27        "Scheduled runs currently executing (0 or 1)."
28    );
29    describe_gauge!(
30        "faucet_schedule_consecutive_failures",
31        "Consecutive failed scheduled runs since the last success."
32    );
33    describe_gauge!(
34        "faucet_schedule_heartbeat_unix_seconds",
35        "Unix timestamp the scheduler loop last ran (alert if it stalls)."
36    );
37    describe_gauge!(
38        "faucet_schedule_last_run_started_unix_seconds",
39        "Unix timestamp the most recent run started."
40    );
41    describe_gauge!(
42        "faucet_schedule_last_run_completed_unix_seconds",
43        "Unix timestamp the most recent run completed."
44    );
45    describe_gauge!(
46        "faucet_schedule_last_run_duration_seconds",
47        "Wall-clock duration of the most recent run."
48    );
49    describe_histogram!(
50        "faucet_schedule_run_lateness_seconds",
51        "How late each run started relative to its scheduled tick."
52    );
53}
54
55/// `outcome ∈ {"ok", "err", "skipped"}`.
56pub fn run_outcome(pipeline: &str, outcome: &'static str) {
57    counter!("faucet_schedule_runs_total", "pipeline" => pipeline.to_string(), "outcome" => outcome)
58        .increment(1);
59}
60
61/// `policy ∈ {"skip", "queue", "forbid"}`.
62pub fn overlap(pipeline: &str, policy: &'static str) {
63    counter!("faucet_schedule_overlaps_total", "pipeline" => pipeline.to_string(), "policy" => policy)
64        .increment(1);
65}
66
67pub fn next_tick(pipeline: &str, when: DateTime<Utc>) {
68    gauge!("faucet_schedule_next_tick_unix_seconds", "pipeline" => pipeline.to_string())
69        .set(when.timestamp() as f64);
70}
71
72pub fn in_flight(pipeline: &str, n: u64) {
73    gauge!("faucet_schedule_runs_in_flight", "pipeline" => pipeline.to_string()).set(n as f64);
74}
75
76pub fn consecutive_failures(pipeline: &str, n: u64) {
77    gauge!("faucet_schedule_consecutive_failures", "pipeline" => pipeline.to_string())
78        .set(n as f64);
79}
80
81pub fn heartbeat(pipeline: &str, now: DateTime<Utc>) {
82    gauge!("faucet_schedule_heartbeat_unix_seconds", "pipeline" => pipeline.to_string())
83        .set(now.timestamp() as f64);
84}
85
86pub fn last_run_started(pipeline: &str, when: DateTime<Utc>) {
87    gauge!("faucet_schedule_last_run_started_unix_seconds", "pipeline" => pipeline.to_string())
88        .set(when.timestamp() as f64);
89}
90
91pub fn last_run_completed(pipeline: &str, when: DateTime<Utc>) {
92    gauge!("faucet_schedule_last_run_completed_unix_seconds", "pipeline" => pipeline.to_string())
93        .set(when.timestamp() as f64);
94}
95
96pub fn last_run_duration(pipeline: &str, d: Duration) {
97    gauge!("faucet_schedule_last_run_duration_seconds", "pipeline" => pipeline.to_string())
98        .set(d.as_secs_f64());
99}
100
101/// `late` may be negative if a run started slightly early; clamp to 0.
102pub fn lateness(pipeline: &str, late: chrono::Duration) {
103    let secs = (late.num_milliseconds() as f64 / 1000.0).max(0.0);
104    histogram!("faucet_schedule_run_lateness_seconds", "pipeline" => pipeline.to_string())
105        .record(secs);
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use metrics::with_local_recorder;
112    use metrics_util::debugging::{DebugValue, DebuggingRecorder};
113
114    /// Find the latest gauge value emitted for `name` with the given `pipeline`
115    /// label in a `DebuggingRecorder` snapshot.
116    fn gauge_value(
117        snapshot: metrics_util::debugging::Snapshot,
118        name: &str,
119        pipeline: &str,
120    ) -> Option<f64> {
121        snapshot
122            .into_vec()
123            .into_iter()
124            .find_map(|(key, _u, _d, v)| {
125                let k = key.key();
126                let labelled = k
127                    .labels()
128                    .any(|l| l.key() == "pipeline" && l.value() == pipeline);
129                if k.name() == name && labelled {
130                    match v {
131                        DebugValue::Gauge(g) => Some(g.into_inner()),
132                        _ => None,
133                    }
134                } else {
135                    None
136                }
137            })
138    }
139
140    #[test]
141    fn in_flight_sets_gauge() {
142        let recorder = DebuggingRecorder::new();
143        let snap = recorder.snapshotter();
144        with_local_recorder(&recorder, || {
145            in_flight("p", 1);
146            in_flight("p", 0);
147        });
148        assert_eq!(
149            gauge_value(snap.snapshot(), "faucet_schedule_runs_in_flight", "p"),
150            Some(0.0),
151            "in_flight(0) must leave the gauge at 0"
152        );
153    }
154
155    #[test]
156    fn consecutive_failures_sets_gauge() {
157        let recorder = DebuggingRecorder::new();
158        let snap = recorder.snapshotter();
159        with_local_recorder(&recorder, || {
160            consecutive_failures("p", 0);
161        });
162        assert_eq!(
163            gauge_value(snap.snapshot(), "faucet_schedule_consecutive_failures", "p"),
164            Some(0.0),
165            "consecutive_failures(0) must register the series at 0"
166        );
167    }
168
169    /// Mirrors what the scheduler does at startup (item 2): pre-emit both gauges
170    /// so the series exist in `/metrics` before the first dispatch.
171    #[test]
172    fn startup_preemit_registers_both_gauges_at_zero() {
173        let recorder = DebuggingRecorder::new();
174        let snap = recorder.snapshotter();
175        with_local_recorder(&recorder, || {
176            describe();
177            in_flight("p", 0);
178            consecutive_failures("p", 0);
179        });
180        assert_eq!(
181            gauge_value(snap.snapshot(), "faucet_schedule_runs_in_flight", "p"),
182            Some(0.0),
183            "runs_in_flight must exist at 0 from startup"
184        );
185        assert_eq!(
186            gauge_value(snap.snapshot(), "faucet_schedule_consecutive_failures", "p"),
187            Some(0.0),
188            "consecutive_failures must exist at 0 from startup"
189        );
190    }
191}