rightkit_daemon/
metrics.rs1use std::collections::{BTreeMap, VecDeque};
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::Mutex;
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13
14pub const LATENCY_BOUNDS_MS: [u64; 10] = [1, 5, 10, 25, 50, 100, 250, 500, 1_000, 5_000];
17pub const MAX_METHODS: usize = 256;
19pub const OTHER_METHOD: &str = "(other)";
21const MINUTES_KEPT: usize = 60;
23
24#[derive(Debug, Clone, PartialEq, Eq, Default)]
26pub struct MethodMetrics {
27 pub requests: u64,
29 pub errors: BTreeMap<String, u64>,
31 pub latency_buckets: [u64; LATENCY_BOUNDS_MS.len() + 1],
34 pub total_micros: u64,
36 pub max_micros: u64,
38 pub per_minute: Vec<(u64, u64)>,
40}
41
42impl MethodMetrics {
43 pub fn error_count(&self) -> u64 {
45 self.errors.values().sum()
46 }
47
48 pub fn requests_in_minute_of(&self, now: SystemTime) -> u64 {
50 let minute = minute_of(now);
51 self.per_minute
52 .iter()
53 .find(|(m, _)| *m == minute)
54 .map_or(0, |(_, n)| *n)
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Default)]
60pub struct HostMetrics {
61 pub methods: BTreeMap<String, MethodMetrics>,
63 pub inflight: usize,
65 pub inflight_max: usize,
67 pub connections: usize,
69 pub connections_peak: usize,
71 pub connection_limit: usize,
74 pub connections_refused: u64,
76}
77
78impl HostMetrics {
79 pub fn total_requests(&self) -> u64 {
81 self.methods.values().map(|m| m.requests).sum()
82 }
83}
84
85#[derive(Default)]
86struct PerMethod {
87 stats: MethodMetrics,
88 minutes: VecDeque<(u64, u64)>,
89}
90
91pub(crate) struct Metrics {
93 methods: Mutex<BTreeMap<String, PerMethod>>,
94 inflight_max: AtomicUsize,
95 connections_peak: AtomicUsize,
96 refused: std::sync::atomic::AtomicU64,
97 connection_limit: usize,
98}
99
100fn minute_of(now: SystemTime) -> u64 {
101 now.duration_since(UNIX_EPOCH)
102 .map(|d| d.as_secs() / 60)
103 .unwrap_or(0)
104}
105
106impl Metrics {
107 pub(crate) fn new(connection_limit: usize) -> Self {
108 Self {
109 methods: Mutex::new(BTreeMap::new()),
110 inflight_max: AtomicUsize::new(0),
111 connections_peak: AtomicUsize::new(0),
112 refused: std::sync::atomic::AtomicU64::new(0),
113 connection_limit,
114 }
115 }
116
117 pub(crate) fn note_inflight(&self, now_inflight: usize) {
119 self.inflight_max.fetch_max(now_inflight, Ordering::SeqCst);
120 }
121
122 pub(crate) fn note_connections(&self, live: usize) {
124 self.connections_peak.fetch_max(live, Ordering::SeqCst);
125 }
126
127 pub(crate) fn note_refused(&self) {
128 self.refused.fetch_add(1, Ordering::SeqCst);
129 }
130
131 pub(crate) fn record(&self, method: &str, took: Duration, error_code: Option<&str>) {
133 let mut methods = self.methods.lock().unwrap_or_else(|e| e.into_inner());
134 let key = if methods.contains_key(method) || methods.len() < MAX_METHODS {
135 method
136 } else {
137 OTHER_METHOD
138 };
139 let entry = methods.entry(key.to_string()).or_default();
140 entry.stats.requests += 1;
141 if let Some(code) = error_code {
142 *entry.stats.errors.entry(code.to_string()).or_insert(0) += 1;
143 }
144 let ms = took.as_millis() as u64;
145 let bucket = LATENCY_BOUNDS_MS
146 .iter()
147 .position(|bound| ms <= *bound)
148 .unwrap_or(LATENCY_BOUNDS_MS.len());
149 entry.stats.latency_buckets[bucket] += 1;
150 let micros = took.as_micros().min(u128::from(u64::MAX)) as u64;
151 entry.stats.total_micros = entry.stats.total_micros.saturating_add(micros);
152 entry.stats.max_micros = entry.stats.max_micros.max(micros);
153 let minute = minute_of(SystemTime::now());
154 match entry.minutes.back_mut() {
155 Some((m, n)) if *m == minute => *n += 1,
156 _ => {
157 entry.minutes.push_back((minute, 1));
158 while entry.minutes.len() > MINUTES_KEPT {
159 entry.minutes.pop_front();
160 }
161 }
162 }
163 }
164
165 pub(crate) fn snapshot(&self, inflight: usize, connections: usize) -> HostMetrics {
166 let methods = self
167 .methods
168 .lock()
169 .unwrap_or_else(|e| e.into_inner())
170 .iter()
171 .map(|(name, m)| {
172 let mut stats = m.stats.clone();
173 stats.per_minute = m.minutes.iter().copied().collect();
174 (name.clone(), stats)
175 })
176 .collect();
177 HostMetrics {
178 methods,
179 inflight,
180 inflight_max: self.inflight_max.load(Ordering::SeqCst),
181 connections,
182 connections_peak: self.connections_peak.load(Ordering::SeqCst),
183 connection_limit: self.connection_limit,
184 connections_refused: self.refused.load(Ordering::SeqCst),
185 }
186 }
187}