use metrics::{counter, describe_counter, describe_gauge, describe_histogram, gauge, histogram};
pub fn describe() {
describe_counter!(
"faucet_resilience_retries_total",
"Retry attempts, by op (sink_write|flush|state_put|source) and error class."
);
describe_histogram!(
"faucet_resilience_retry_sleep_seconds",
"Backoff sleep duration before a retry attempt, by op."
);
describe_counter!(
"faucet_resilience_giveup_total",
"Operations that exhausted their retry budget and failed, by op."
);
describe_gauge!(
"faucet_resilience_circuit_state",
"Circuit breaker state (0 closed, 1 open)."
);
describe_counter!(
"faucet_resilience_circuit_opened_total",
"Circuit breaker open events."
);
describe_counter!(
"faucet_resilience_poison_rows_total",
"Poison-pill rows resolved by terminal action (dlq|drop|fail)."
);
}
pub fn retry(pipeline: &str, row: &str, op: &'static str, class: &'static str) {
counter!(
"faucet_resilience_retries_total",
"pipeline" => pipeline.to_string(),
"row" => row.to_string(),
"op" => op,
"class" => class,
)
.increment(1);
}
pub fn retry_sleep(pipeline: &str, row: &str, op: &'static str, seconds: f64) {
histogram!(
"faucet_resilience_retry_sleep_seconds",
"pipeline" => pipeline.to_string(),
"row" => row.to_string(),
"op" => op,
)
.record(seconds);
}
pub fn giveup(pipeline: &str, row: &str, op: &'static str) {
counter!(
"faucet_resilience_giveup_total",
"pipeline" => pipeline.to_string(),
"row" => row.to_string(),
"op" => op,
)
.increment(1);
}
pub fn circuit_opened(pipeline: &str, row: &str) {
counter!(
"faucet_resilience_circuit_opened_total",
"pipeline" => pipeline.to_string(),
"row" => row.to_string(),
)
.increment(1);
gauge!(
"faucet_resilience_circuit_state",
"pipeline" => pipeline.to_string(),
"row" => row.to_string(),
)
.set(1.0);
}
pub fn circuit_closed(pipeline: &str, row: &str) {
gauge!(
"faucet_resilience_circuit_state",
"pipeline" => pipeline.to_string(),
"row" => row.to_string(),
)
.set(0.0);
}
pub fn poison_rows(pipeline: &str, row: &str, action: &'static str, n: u64) {
if n == 0 {
return;
}
counter!(
"faucet_resilience_poison_rows_total",
"pipeline" => pipeline.to_string(),
"row" => row.to_string(),
"action" => action,
)
.increment(n);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn describe_is_callable_and_idempotent() {
describe();
describe();
}
#[test]
fn emit_helpers_do_not_panic_without_recorder() {
retry("p", "r", "sink_write", "http_5xx");
retry_sleep("p", "r", "flush", 0.25);
giveup("p", "r", "state_put");
circuit_opened("p", "r");
poison_rows("p", "r", "dlq", 3);
poison_rows("p", "r", "drop", 0);
}
#[test]
fn circuit_state_goes_back_to_closed() {
use metrics_util::debugging::DebugValue;
let _g = crate::observability::decorator::source_tests::LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let snap = crate::observability::decorator::source_tests::snapshotter();
let state = || {
snap.snapshot()
.into_vec()
.into_iter()
.find_map(|(k, _, _, v)| {
let k = k.key();
(k.name() == "faucet_resilience_circuit_state"
&& k.labels()
.any(|l| l.key() == "row" && l.value() == "cb-row"))
.then_some(v)
})
};
circuit_opened("p", "cb-row");
assert_eq!(state(), Some(DebugValue::Gauge(1.0.into())));
circuit_closed("p", "cb-row");
assert_eq!(state(), Some(DebugValue::Gauge(0.0.into())));
}
}