faucet_cli/schedule/
metrics.rs1use chrono::{DateTime, Utc};
6use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram};
7use std::time::Duration;
8
9pub 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_counter!(
22 "faucet_schedule_reloads_total",
23 "Hot config reloads (SIGHUP), by outcome (ok|error)."
24 );
25 describe_gauge!(
26 "faucet_schedule_next_tick_unix_seconds",
27 "Unix timestamp of the next scheduled tick."
28 );
29 describe_gauge!(
30 "faucet_schedule_runs_in_flight",
31 "Scheduled runs currently executing (0 or 1)."
32 );
33 describe_gauge!(
34 "faucet_schedule_consecutive_failures",
35 "Consecutive failed scheduled runs since the last success."
36 );
37 describe_gauge!(
38 "faucet_schedule_heartbeat_unix_seconds",
39 "Unix timestamp the scheduler loop last ran (alert if it stalls)."
40 );
41 describe_gauge!(
42 "faucet_schedule_last_run_started_unix_seconds",
43 "Unix timestamp the most recent run started."
44 );
45 describe_gauge!(
46 "faucet_schedule_last_run_completed_unix_seconds",
47 "Unix timestamp the most recent run completed."
48 );
49 describe_gauge!(
50 "faucet_schedule_last_run_duration_seconds",
51 "Wall-clock duration of the most recent run."
52 );
53 describe_histogram!(
54 "faucet_schedule_run_lateness_seconds",
55 "How late each run started relative to its scheduled tick."
56 );
57}
58
59pub fn run_outcome(pipeline: &str, outcome: &'static str) {
61 counter!("faucet_schedule_runs_total", "pipeline" => pipeline.to_string(), "outcome" => outcome)
62 .increment(1);
63}
64
65pub fn reload(pipeline: &str, outcome: &'static str) {
68 counter!("faucet_schedule_reloads_total", "pipeline" => pipeline.to_string(), "outcome" => outcome)
69 .increment(1);
70}
71
72pub fn overlap(pipeline: &str, policy: &'static str) {
73 counter!("faucet_schedule_overlaps_total", "pipeline" => pipeline.to_string(), "policy" => policy)
74 .increment(1);
75}
76
77pub fn next_tick(pipeline: &str, when: DateTime<Utc>) {
78 gauge!("faucet_schedule_next_tick_unix_seconds", "pipeline" => pipeline.to_string())
79 .set(when.timestamp() as f64);
80}
81
82pub fn in_flight(pipeline: &str, n: u64) {
83 gauge!("faucet_schedule_runs_in_flight", "pipeline" => pipeline.to_string()).set(n as f64);
84}
85
86pub fn consecutive_failures(pipeline: &str, n: u64) {
87 gauge!("faucet_schedule_consecutive_failures", "pipeline" => pipeline.to_string())
88 .set(n as f64);
89}
90
91pub fn heartbeat(pipeline: &str, now: DateTime<Utc>) {
92 gauge!("faucet_schedule_heartbeat_unix_seconds", "pipeline" => pipeline.to_string())
93 .set(now.timestamp() as f64);
94}
95
96pub fn last_run_started(pipeline: &str, when: DateTime<Utc>) {
97 gauge!("faucet_schedule_last_run_started_unix_seconds", "pipeline" => pipeline.to_string())
98 .set(when.timestamp() as f64);
99}
100
101pub fn last_run_completed(pipeline: &str, when: DateTime<Utc>) {
102 gauge!("faucet_schedule_last_run_completed_unix_seconds", "pipeline" => pipeline.to_string())
103 .set(when.timestamp() as f64);
104}
105
106pub fn last_run_duration(pipeline: &str, d: Duration) {
107 gauge!("faucet_schedule_last_run_duration_seconds", "pipeline" => pipeline.to_string())
108 .set(d.as_secs_f64());
109}
110
111pub fn lateness(pipeline: &str, late: chrono::Duration) {
113 let secs = (late.num_milliseconds() as f64 / 1000.0).max(0.0);
114 histogram!("faucet_schedule_run_lateness_seconds", "pipeline" => pipeline.to_string())
115 .record(secs);
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use metrics::with_local_recorder;
122 use metrics_util::debugging::{DebugValue, DebuggingRecorder};
123
124 fn gauge_value(
127 snapshot: metrics_util::debugging::Snapshot,
128 name: &str,
129 pipeline: &str,
130 ) -> Option<f64> {
131 snapshot
132 .into_vec()
133 .into_iter()
134 .find_map(|(key, _u, _d, v)| {
135 let k = key.key();
136 let labelled = k
137 .labels()
138 .any(|l| l.key() == "pipeline" && l.value() == pipeline);
139 if k.name() == name && labelled {
140 match v {
141 DebugValue::Gauge(g) => Some(g.into_inner()),
142 _ => None,
143 }
144 } else {
145 None
146 }
147 })
148 }
149
150 #[test]
151 fn in_flight_sets_gauge() {
152 let recorder = DebuggingRecorder::new();
153 let snap = recorder.snapshotter();
154 with_local_recorder(&recorder, || {
155 in_flight("p", 1);
156 in_flight("p", 0);
157 });
158 assert_eq!(
159 gauge_value(snap.snapshot(), "faucet_schedule_runs_in_flight", "p"),
160 Some(0.0),
161 "in_flight(0) must leave the gauge at 0"
162 );
163 }
164
165 #[test]
166 fn consecutive_failures_sets_gauge() {
167 let recorder = DebuggingRecorder::new();
168 let snap = recorder.snapshotter();
169 with_local_recorder(&recorder, || {
170 consecutive_failures("p", 0);
171 });
172 assert_eq!(
173 gauge_value(snap.snapshot(), "faucet_schedule_consecutive_failures", "p"),
174 Some(0.0),
175 "consecutive_failures(0) must register the series at 0"
176 );
177 }
178
179 #[test]
182 fn startup_preemit_registers_both_gauges_at_zero() {
183 let recorder = DebuggingRecorder::new();
184 let snap = recorder.snapshotter();
185 with_local_recorder(&recorder, || {
186 describe();
187 in_flight("p", 0);
188 consecutive_failures("p", 0);
189 });
190 assert_eq!(
191 gauge_value(snap.snapshot(), "faucet_schedule_runs_in_flight", "p"),
192 Some(0.0),
193 "runs_in_flight must exist at 0 from startup"
194 );
195 assert_eq!(
196 gauge_value(snap.snapshot(), "faucet_schedule_consecutive_failures", "p"),
197 Some(0.0),
198 "consecutive_failures must exist at 0 from startup"
199 );
200 }
201}