use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
pub const SAGA_LATENCY_BUCKETS: &[f64] = &[
0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0,
];
#[derive(Clone, Copy)]
pub(crate) enum LatencyStage {
Handler,
ParticipantTransaction,
StartTransaction,
TransitionTransaction,
TimerLateness,
}
const STAGES: [&str; 5] = [
"participant_handler",
"participant_transaction",
"start_transaction",
"transition_transaction",
"timer_lateness",
];
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SagaLatencySnapshot {
pub stage: &'static str,
pub count: u64,
pub micros_sum: u64,
pub buckets: Vec<u64>,
}
fn metrics() -> &'static Mutex<Vec<SagaLatencySnapshot>> {
static METRICS: OnceLock<Mutex<Vec<SagaLatencySnapshot>>> = OnceLock::new();
METRICS.get_or_init(|| {
Mutex::new(
STAGES
.iter()
.map(|stage| SagaLatencySnapshot {
stage,
buckets: vec![0; SAGA_LATENCY_BUCKETS.len() + 1],
..Default::default()
})
.collect(),
)
})
}
pub fn saga_latency_snapshot() -> Vec<SagaLatencySnapshot> {
metrics()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn observe(stage: LatencyStage, duration: Duration) {
let mut metrics = metrics()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let metric = &mut metrics[stage as usize];
let bucket = SAGA_LATENCY_BUCKETS.partition_point(|bound| *bound < duration.as_secs_f64());
metric.count = metric.count.saturating_add(1);
metric.micros_sum = metric
.micros_sum
.saturating_add(duration.as_micros().min(u64::MAX as u128) as u64);
metric.buckets[bucket] = metric.buckets[bucket].saturating_add(1);
}
pub(crate) struct LatencyGuard {
stage: LatencyStage,
started: Instant,
}
impl LatencyGuard {
pub(crate) fn new(stage: LatencyStage) -> Self {
Self {
stage,
started: Instant::now(),
}
}
}
impl Drop for LatencyGuard {
fn drop(&mut self) {
observe(self.stage, self.started.elapsed());
}
}
pub(crate) async fn measure_handler<T>(body: impl std::future::Future<Output = T>) -> T {
let _latency = LatencyGuard::new(LatencyStage::Handler);
body.await
}
pub fn render_saga_latency_metrics() -> String {
use std::fmt::Write as _;
let name = "nasaga_stage_duration_seconds";
let mut output = format!("# TYPE {name} histogram\n");
for sample in saga_latency_snapshot() {
let mut cumulative = 0u64;
for (index, bucket) in sample.buckets.iter().enumerate() {
cumulative = cumulative.saturating_add(*bucket);
let bound = SAGA_LATENCY_BUCKETS
.get(index)
.map(f64::to_string)
.unwrap_or_else(|| "+Inf".to_owned());
let _ = writeln!(
output,
"{name}_bucket{{stage=\"{}\",le=\"{bound}\"}} {cumulative}",
sample.stage
);
}
let _ = writeln!(
output,
"{name}_count{{stage=\"{}\"}} {}",
sample.stage, sample.count
);
let _ = writeln!(
output,
"{name}_sum{{stage=\"{}\"}} {}",
sample.stage,
sample.micros_sum as f64 / 1_000_000.0
);
}
output
}