Skip to main content

faucet_cli/serve/triggers/
metrics.rs

1//! `faucet_serve_trigger_*` metrics. Low-cardinality labels only (`trigger`
2//! name, `type`). Mirrors `crate::schedule::metrics`.
3
4use metrics::{counter, describe_counter, describe_gauge, gauge};
5
6pub fn describe() {
7    describe_counter!(
8        "faucet_serve_triggers_fired_total",
9        "Events that warranted a fire, by trigger/type"
10    );
11    describe_counter!(
12        "faucet_serve_trigger_runs_enqueued_total",
13        "Fires that enqueued a run"
14    );
15    describe_counter!(
16        "faucet_serve_trigger_runs_coalesced_total",
17        "Fires deduped/coalesced (idempotency/debounce)"
18    );
19    describe_counter!(
20        "faucet_serve_trigger_runs_dropped_total",
21        "Fires dropped (e.g. queue_full)"
22    );
23    describe_counter!(
24        "faucet_serve_trigger_errors_total",
25        "Watcher poll/serve/fire errors"
26    );
27    describe_gauge!(
28        "faucet_serve_triggers_active",
29        "Number of active triggers (including webhook routes, which spawn no watcher task)"
30    );
31    describe_gauge!(
32        "faucet_serve_trigger_healthy",
33        "1 if the watcher is healthy, else 0"
34    );
35    describe_gauge!(
36        "faucet_serve_trigger_last_fire_unix_seconds",
37        "Unix time of the watcher's last fire"
38    );
39}
40
41pub fn active(n: usize) {
42    gauge!("faucet_serve_triggers_active").set(n as f64);
43}
44
45/// Pre-emit every per-trigger series at zero so they exist in `/metrics` from
46/// startup — the `metrics` exporter only renders a series after its first
47/// emission, so otherwise a pre-first-fire scrape shows "no data" for the
48/// trigger. Mirrors `schedule::metrics`' pre-emit step. Called once per enabled
49/// trigger from `spawn_watchers` (including webhooks, which spawn no task).
50/// `last_fire` is intentionally NOT pre-emitted — there has been no fire yet.
51pub fn preinit(trigger: &str, kind: &'static str) {
52    counter!("faucet_serve_triggers_fired_total", "trigger" => trigger.to_string(), "type" => kind)
53        .increment(0);
54    counter!("faucet_serve_trigger_runs_enqueued_total", "trigger" => trigger.to_string())
55        .increment(0);
56    counter!("faucet_serve_trigger_runs_coalesced_total", "trigger" => trigger.to_string())
57        .increment(0);
58    counter!("faucet_serve_trigger_runs_dropped_total", "trigger" => trigger.to_string(), "reason" => "queue_full")
59        .increment(0);
60    counter!("faucet_serve_trigger_errors_total", "trigger" => trigger.to_string(), "type" => kind)
61        .increment(0);
62    healthy(trigger, true);
63}
64pub fn fired(trigger: &str, kind: &'static str) {
65    counter!("faucet_serve_triggers_fired_total", "trigger" => trigger.to_string(), "type" => kind)
66        .increment(1);
67}
68pub fn enqueued(trigger: &str) {
69    counter!("faucet_serve_trigger_runs_enqueued_total", "trigger" => trigger.to_string())
70        .increment(1);
71}
72pub fn coalesced(trigger: &str) {
73    counter!("faucet_serve_trigger_runs_coalesced_total", "trigger" => trigger.to_string())
74        .increment(1);
75}
76pub fn dropped(trigger: &str, reason: &'static str) {
77    counter!("faucet_serve_trigger_runs_dropped_total", "trigger" => trigger.to_string(), "reason" => reason).increment(1);
78}
79pub fn error(trigger: &str, kind: &'static str) {
80    counter!("faucet_serve_trigger_errors_total", "trigger" => trigger.to_string(), "type" => kind)
81        .increment(1);
82}
83pub fn healthy(trigger: &str, ok: bool) {
84    gauge!("faucet_serve_trigger_healthy", "trigger" => trigger.to_string()).set(if ok {
85        1.0
86    } else {
87        0.0
88    });
89}
90pub fn last_fire(trigger: &str, unix_secs: i64) {
91    gauge!("faucet_serve_trigger_last_fire_unix_seconds", "trigger" => trigger.to_string())
92        .set(unix_secs as f64);
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use metrics::with_local_recorder;
99    use metrics_util::debugging::{DebuggingRecorder, Snapshotter};
100
101    #[test]
102    fn emits_fired_counter() {
103        let recorder = DebuggingRecorder::new();
104        let snap: Snapshotter = recorder.snapshotter();
105        with_local_recorder(&recorder, || {
106            fired("t", "webhook");
107        });
108        let metrics = snap.snapshot().into_vec();
109        assert!(
110            metrics
111                .iter()
112                .any(|(k, _, _, _)| k.key().name() == "faucet_serve_triggers_fired_total"),
113            "fired counter not emitted"
114        );
115    }
116
117    #[test]
118    fn preinit_emits_every_per_trigger_series_at_zero() {
119        let recorder = DebuggingRecorder::new();
120        let snap: Snapshotter = recorder.snapshotter();
121        with_local_recorder(&recorder, || {
122            preinit("t", "webhook");
123        });
124        let metrics = snap.snapshot().into_vec();
125        let names: std::collections::BTreeSet<_> =
126            metrics.iter().map(|(k, _, _, _)| k.key().name()).collect();
127        for expected in [
128            "faucet_serve_triggers_fired_total",
129            "faucet_serve_trigger_runs_enqueued_total",
130            "faucet_serve_trigger_runs_coalesced_total",
131            "faucet_serve_trigger_runs_dropped_total",
132            "faucet_serve_trigger_errors_total",
133            "faucet_serve_trigger_healthy",
134        ] {
135            assert!(
136                names.contains(expected),
137                "preinit missing series {expected}"
138            );
139        }
140        // last_fire must NOT be pre-emitted (no fire yet).
141        assert!(
142            !names.contains("faucet_serve_trigger_last_fire_unix_seconds"),
143            "last_fire must not be pre-emitted"
144        );
145    }
146}