1use std::{
2 collections::{HashMap, VecDeque},
3 sync::{
4 atomic::{AtomicU64, Ordering},
5 Arc, Mutex, MutexGuard,
6 },
7 time::Duration,
8};
9
10use serde_json::{json, Value};
11use tracing::debug;
12
13use crate::registry::ConnectionId;
14
15const ROUTE_OPEN_REFUSAL_COUNTER_CODES: &[&str] = &[
23 "module_warming",
24 ROUTE_OPEN_REFUSED_DECLARED_NOT_READY,
25 "target_unavailable",
26 "module_removed",
27 "module_no_protocol",
28 "unknown_module",
29 "module_reloading",
30 "op_not_allowed",
31 "bad_consumer_identity",
32 "capability_forbidden",
33 "admission_facts_not_permitted",
34 "admission_facts_target_not_allowed",
35 "route_limit",
36 "forwarding_error",
37 "module_timeout",
38 ROUTE_OPEN_REFUSED_BREAKER_OPEN,
39 "module_rejected",
40];
41
42pub(crate) const ROUTE_OPEN_REFUSED_BREAKER_OPEN: &str = "module_timeout_breaker_open";
54
55pub(crate) const ROUTE_OPEN_REFUSED_DECLARED_NOT_READY: &str = "module_warming_declared_not_ready";
61
62#[derive(Debug, Clone, Default)]
64pub struct ConnectedClients {
65 count: Arc<AtomicU64>,
66}
67
68impl ConnectedClients {
69 pub fn new() -> Self {
70 Self::default()
71 }
72
73 pub fn count(&self) -> u64 {
74 self.count.load(Ordering::SeqCst)
75 }
76
77 pub(crate) fn open(&self, connection_id: ConnectionId) -> ConnectedClientGuard {
78 let previous = self.count.fetch_add(1, Ordering::SeqCst);
79 let current = previous + 1;
80 debug!(
88 connection_id = connection_id.get(),
89 connected_clients = current,
90 previous_connected_clients = previous,
91 "authenticated connection count changed"
92 );
93 ConnectedClientGuard {
94 clients: self.clone(),
95 connection_id,
96 }
97 }
98}
99
100pub(crate) struct ConnectedClientGuard {
101 clients: ConnectedClients,
102 connection_id: ConnectionId,
103}
104
105impl Drop for ConnectedClientGuard {
106 fn drop(&mut self) {
107 let previous = self.clients.count.fetch_sub(1, Ordering::SeqCst);
108 let current = previous.saturating_sub(1);
109 debug!(
111 connection_id = self.connection_id.get(),
112 connected_clients = current,
113 previous_connected_clients = previous,
114 "authenticated connection count changed"
115 );
116 }
117}
118
119#[derive(Debug, Clone, Default)]
121pub struct DaemonCounters {
122 module_frames_dropped_no_route: Arc<AtomicU64>,
123 module_frames_dropped_no_route_by_module: Arc<Mutex<HashMap<String, u64>>>,
126 route_open_refused_by_code: Arc<Mutex<HashMap<String, u64>>>,
127 route_open_accepted_by_principal: Arc<Mutex<HashMap<String, u64>>>,
128 module_frames_dropped_no_route_window: Arc<Mutex<DropWindow>>,
129 module_requests_dropped_stale_route: Arc<AtomicU64>,
130 client_frames_dropped_stale_route: Arc<AtomicU64>,
131 client_egress_close_delivery_failed: Arc<AtomicU64>,
132 goodbye_relay_client_failed: Arc<AtomicU64>,
133 goodbye_relay_module_dropped: Arc<AtomicU64>,
134 goodbye_relay_module_dropped_by_module: Arc<Mutex<HashMap<String, u64>>>,
135 route_released_epoch_fenced: Arc<AtomicU64>,
136 route_release_stale_skipped: Arc<AtomicU64>,
137 drains_with_undeclared_gauge: Arc<AtomicU64>,
138}
139
140#[derive(Debug)]
143struct DropWindow {
144 started_at: tokio::time::Instant,
145 buckets: VecDeque<DropBucket>,
146}
147
148#[derive(Debug)]
149struct DropBucket {
150 minute: u64,
151 count: u64,
152}
153
154impl Default for DropWindow {
155 fn default() -> Self {
156 Self {
157 started_at: tokio::time::Instant::now(),
158 buckets: VecDeque::new(),
159 }
160 }
161}
162
163impl DropWindow {
164 const MINUTE: Duration = Duration::from_secs(60);
165 const BUCKETS: u64 = 10;
166
167 fn record(&mut self, now: tokio::time::Instant) {
168 let minute = self.minute_at(now);
169 self.prune_before(minute);
170 match self.buckets.back_mut() {
171 Some(bucket) if bucket.minute == minute => bucket.count += 1,
172 _ => self.buckets.push_back(DropBucket { minute, count: 1 }),
173 }
174 }
175
176 fn count_last_10m(&mut self, now: tokio::time::Instant) -> u64 {
177 let minute = self.minute_at(now);
178 self.prune_before(minute);
179 self.buckets.iter().map(|bucket| bucket.count).sum()
180 }
181
182 fn nonzero_minutes_last_10m(&mut self, now: tokio::time::Instant) -> u64 {
183 let minute = self.minute_at(now);
184 self.prune_before(minute);
185 self.buckets.len() as u64
186 }
187
188 fn minute_at(&self, now: tokio::time::Instant) -> u64 {
189 now.saturating_duration_since(self.started_at).as_secs() / Self::MINUTE.as_secs()
190 }
191
192 fn prune_before(&mut self, current_minute: u64) {
193 while self
194 .buckets
195 .front()
196 .is_some_and(|bucket| current_minute.saturating_sub(bucket.minute) >= Self::BUCKETS)
197 {
198 self.buckets.pop_front();
199 }
200 }
201}
202
203impl DaemonCounters {
204 pub fn new() -> Self {
205 Self::default()
206 }
207
208 pub fn snapshot(&self) -> Value {
211 let mut snapshot = serde_json::Map::new();
212 snapshot.insert(
213 "module_frames_dropped_no_route".into(),
214 self.module_frames_dropped_no_route
215 .load(Ordering::Relaxed)
216 .into(),
217 );
218 let mut drop_window = self
219 .module_frames_dropped_no_route_window
220 .lock()
221 .expect("drop-rate window mutex poisoned");
222 let now = tokio::time::Instant::now();
223 snapshot.insert(
224 "module_frames_dropped_no_route_last_10m".into(),
225 drop_window.count_last_10m(now).into(),
226 );
227 snapshot.insert(
228 "module_frames_dropped_no_route_nonzero_minutes_last_10m".into(),
229 drop_window.nonzero_minutes_last_10m(now).into(),
230 );
231 insert_nonempty_counts(
232 &mut snapshot,
233 "module_frames_dropped_no_route_by_module",
234 &self.module_frames_dropped_no_route_by_module,
235 );
236 insert_nonempty_counts(
237 &mut snapshot,
238 "route_open_refused_by_code",
239 &self.route_open_refused_by_code,
240 );
241 insert_nonempty_counts(
242 &mut snapshot,
243 "route_open_accepted_by_principal",
244 &self.route_open_accepted_by_principal,
245 );
246 snapshot.insert(
247 "module_requests_dropped_stale_route".into(),
248 self.module_requests_dropped_stale_route
249 .load(Ordering::Relaxed)
250 .into(),
251 );
252 snapshot.insert(
253 "client_frames_dropped_stale_route".into(),
254 self.client_frames_dropped_stale_route
255 .load(Ordering::Relaxed)
256 .into(),
257 );
258 snapshot.insert(
259 "client_egress_close_delivery_failed".into(),
260 self.client_egress_close_delivery_failed
261 .load(Ordering::Relaxed)
262 .into(),
263 );
264 snapshot.insert(
265 "goodbye_relay_client_failed".into(),
266 self.goodbye_relay_client_failed
267 .load(Ordering::Relaxed)
268 .into(),
269 );
270 snapshot.insert(
271 "goodbye_relay_module_dropped".into(),
272 self.goodbye_relay_module_dropped
273 .load(Ordering::Relaxed)
274 .into(),
275 );
276 insert_nonempty_counts(
277 &mut snapshot,
278 "goodbye_relay_module_dropped_by_module",
279 &self.goodbye_relay_module_dropped_by_module,
280 );
281 snapshot.insert(
282 "route_released_epoch_fenced".into(),
283 self.route_released_epoch_fenced
284 .load(Ordering::Relaxed)
285 .into(),
286 );
287 snapshot.insert(
288 "route_release_stale_skipped".into(),
289 self.route_release_stale_skipped
290 .load(Ordering::Relaxed)
291 .into(),
292 );
293 snapshot.insert(
294 "drains_with_undeclared_gauge".into(),
295 self.drains_with_undeclared_gauge
296 .load(Ordering::Relaxed)
297 .into(),
298 );
299 Value::Object(snapshot)
300 }
301
302 pub(crate) fn increment_module_frames_dropped_no_route(&self, module_id: Option<&str>) {
303 self.module_frames_dropped_no_route
304 .fetch_add(1, Ordering::Relaxed);
305 if let Some(module_id) = module_id {
306 increment_keyed_count(&self.module_frames_dropped_no_route_by_module, module_id);
307 }
308 self.module_frames_dropped_no_route_window
309 .lock()
310 .expect("drop-rate window mutex poisoned")
311 .record(tokio::time::Instant::now());
312 }
313
314 pub(crate) fn increment_route_open_refused(&self, code: &'static str) {
315 debug_assert!(ROUTE_OPEN_REFUSAL_COUNTER_CODES.contains(&code));
316 increment_keyed_count(&self.route_open_refused_by_code, code);
317 }
318
319 pub(crate) fn increment_route_open_accepted(&self, principal: &str) {
329 increment_keyed_count(&self.route_open_accepted_by_principal, principal);
330 }
331
332 pub(crate) fn increment_module_requests_dropped_stale_route(&self) {
333 self.module_requests_dropped_stale_route
334 .fetch_add(1, Ordering::Relaxed);
335 }
336
337 pub(crate) fn increment_client_frames_dropped_stale_route(&self) {
338 self.client_frames_dropped_stale_route
339 .fetch_add(1, Ordering::Relaxed);
340 }
341
342 pub(crate) fn increment_client_egress_close_delivery_failed(&self) {
343 self.client_egress_close_delivery_failed
344 .fetch_add(1, Ordering::Relaxed);
345 }
346
347 pub(crate) fn increment_goodbye_relay_client_failed(&self) {
348 self.goodbye_relay_client_failed
349 .fetch_add(1, Ordering::Relaxed);
350 }
351
352 pub(crate) fn increment_goodbye_relay_module_dropped(&self, module_id: Option<&str>) {
353 self.goodbye_relay_module_dropped
354 .fetch_add(1, Ordering::Relaxed);
355 if let Some(module_id) = module_id {
356 increment_keyed_count(&self.goodbye_relay_module_dropped_by_module, module_id);
357 }
358 }
359
360 pub(crate) fn increment_route_released_epoch_fenced(&self) {
361 self.route_released_epoch_fenced
362 .fetch_add(1, Ordering::Relaxed);
363 }
364
365 pub(crate) fn increment_route_release_stale_skipped(&self) {
366 self.route_release_stale_skipped
367 .fetch_add(1, Ordering::Relaxed);
368 }
369
370 pub(crate) fn increment_drains_with_undeclared_gauge(&self) {
371 self.drains_with_undeclared_gauge
372 .fetch_add(1, Ordering::Relaxed);
373 }
374}
375
376fn increment_keyed_count(counts: &Mutex<HashMap<String, u64>>, key: &str) {
377 *counts
378 .lock()
379 .expect("keyed counter mutex poisoned")
380 .entry(key.to_string())
381 .or_default() += 1;
382}
383
384fn insert_nonempty_counts(
385 snapshot: &mut serde_json::Map<String, Value>,
386 key: &str,
387 counts: &Mutex<HashMap<String, u64>>,
388) {
389 let counts: MutexGuard<'_, HashMap<String, u64>> =
390 counts.lock().expect("keyed counter mutex poisoned");
391 if !counts.is_empty() {
392 snapshot.insert(key.to_string(), json!(&*counts));
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn counter_snapshot_includes_zero_rate_and_omits_empty_module_maps() {
402 let counters = DaemonCounters::new();
403 let snapshot = counters.snapshot();
404
405 assert_eq!(snapshot["module_frames_dropped_no_route_last_10m"], 0);
406 assert_eq!(
407 snapshot["module_frames_dropped_no_route_nonzero_minutes_last_10m"],
408 0
409 );
410 assert!(snapshot
411 .get("module_frames_dropped_no_route_by_module")
412 .is_none());
413 assert!(snapshot
414 .get("goodbye_relay_module_dropped_by_module")
415 .is_none());
416 }
417
418 #[tokio::test(start_paused = true)]
419 async fn module_frame_drops_are_attributed_to_the_emitting_module() {
420 let counters = DaemonCounters::new();
421 counters.increment_module_frames_dropped_no_route(Some("alpha"));
422 counters.increment_module_frames_dropped_no_route(Some("alpha"));
423
424 let snapshot = counters.snapshot();
425 assert_eq!(snapshot["module_frames_dropped_no_route"], 2);
426 assert_eq!(
427 snapshot["module_frames_dropped_no_route_by_module"],
428 json!({ "alpha": 2 })
429 );
430 assert_eq!(snapshot["module_frames_dropped_no_route_last_10m"], 2);
431 }
432
433 #[tokio::test(start_paused = true)]
434 async fn frame_drop_rate_ages_out_after_ten_minute_buckets() {
435 let counters = DaemonCounters::new();
436 counters.increment_module_frames_dropped_no_route(Some("alpha"));
437
438 tokio::time::advance(Duration::from_secs(9 * 60)).await;
439 assert_eq!(
440 counters.snapshot()["module_frames_dropped_no_route_last_10m"],
441 1
442 );
443
444 tokio::time::advance(Duration::from_secs(60)).await;
445 assert_eq!(
446 counters.snapshot()["module_frames_dropped_no_route_last_10m"],
447 0
448 );
449 }
450
451 #[tokio::test(start_paused = true)]
452 async fn frame_drop_window_counts_only_nonzero_minutes() {
453 let counters = DaemonCounters::new();
454 for minute in 0..10 {
455 if minute > 0 {
456 tokio::time::advance(Duration::from_secs(60)).await;
457 }
458 if minute != 4 {
459 counters.increment_module_frames_dropped_no_route(Some("alpha"));
460 }
461 }
462
463 assert_eq!(
464 counters.snapshot()["module_frames_dropped_no_route_nonzero_minutes_last_10m"],
465 9
466 );
467 }
468
469 #[test]
470 fn goodbye_relay_drops_are_attributed_to_the_target_module() {
471 let counters = DaemonCounters::new();
472 counters.increment_goodbye_relay_module_dropped(Some("alpha"));
473
474 assert_eq!(
475 counters.snapshot()["goodbye_relay_module_dropped_by_module"],
476 json!({ "alpha": 1 })
477 );
478 }
479}