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 ROUTE_OPEN_REFUSED_REQUIRED_CAPABILITY_UNPROVIDED,
26 "target_unavailable",
27 "module_removed",
28 "module_no_protocol",
29 "unknown_module",
30 "module_reloading",
31 "op_not_allowed",
32 "bad_consumer_identity",
33 "capability_forbidden",
34 "admission_facts_not_permitted",
35 "admission_facts_target_not_allowed",
36 "route_limit",
37 "forwarding_error",
38 "module_timeout",
39 ROUTE_OPEN_REFUSED_BREAKER_OPEN,
40 "module_rejected",
41];
42
43pub(crate) const ROUTE_OPEN_REFUSED_BREAKER_OPEN: &str = "module_timeout_breaker_open";
55
56pub(crate) const ROUTE_OPEN_REFUSED_DECLARED_NOT_READY: &str = "module_warming_declared_not_ready";
62
63pub(crate) const ROUTE_OPEN_REFUSED_REQUIRED_CAPABILITY_UNPROVIDED: &str =
70 "module_warming_required_capability_unprovided";
71
72#[derive(Debug, Clone, Default)]
74pub struct ConnectedClients {
75 count: Arc<AtomicU64>,
76}
77
78impl ConnectedClients {
79 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn count(&self) -> u64 {
84 self.count.load(Ordering::SeqCst)
85 }
86
87 pub(crate) fn open(&self, connection_id: ConnectionId) -> ConnectedClientGuard {
88 let previous = self.count.fetch_add(1, Ordering::SeqCst);
89 let current = previous + 1;
90 debug!(
98 connection_id = connection_id.get(),
99 connected_clients = current,
100 previous_connected_clients = previous,
101 "authenticated connection count changed"
102 );
103 ConnectedClientGuard {
104 clients: self.clone(),
105 connection_id,
106 }
107 }
108}
109
110pub(crate) struct ConnectedClientGuard {
111 clients: ConnectedClients,
112 connection_id: ConnectionId,
113}
114
115impl Drop for ConnectedClientGuard {
116 fn drop(&mut self) {
117 let previous = self.clients.count.fetch_sub(1, Ordering::SeqCst);
118 let current = previous.saturating_sub(1);
119 debug!(
121 connection_id = self.connection_id.get(),
122 connected_clients = current,
123 previous_connected_clients = previous,
124 "authenticated connection count changed"
125 );
126 }
127}
128
129#[derive(Debug, Clone, Default)]
131pub struct DaemonCounters {
132 module_frames_dropped_no_route: Arc<AtomicU64>,
137 module_frames_dropped_released_route: Arc<AtomicU64>,
143 module_frames_dropped_released_route_by_module: Arc<Mutex<HashMap<String, u64>>>,
144 module_orphan_route_goodbyes_sent: Arc<AtomicU64>,
149 module_frames_dropped_no_route_by_module: Arc<Mutex<HashMap<String, u64>>>,
152 route_open_refused_by_code: Arc<Mutex<HashMap<String, u64>>>,
153 route_open_accepted_by_principal: Arc<Mutex<HashMap<String, u64>>>,
154 module_frames_dropped_no_route_window: Arc<Mutex<DropWindow>>,
155 module_requests_dropped_stale_route: Arc<AtomicU64>,
156 client_frames_dropped_stale_route: Arc<AtomicU64>,
157 client_egress_close_delivery_failed: Arc<AtomicU64>,
158 goodbye_relay_client_failed: Arc<AtomicU64>,
159 goodbye_relay_module_dropped: Arc<AtomicU64>,
160 goodbye_relay_module_dropped_by_module: Arc<Mutex<HashMap<String, u64>>>,
161 route_released_epoch_fenced: Arc<AtomicU64>,
162 route_release_stale_skipped: Arc<AtomicU64>,
163 drains_with_undeclared_gauge: Arc<AtomicU64>,
164}
165
166#[derive(Debug)]
169struct DropWindow {
170 started_at: tokio::time::Instant,
171 buckets: VecDeque<DropBucket>,
172}
173
174#[derive(Debug)]
175struct DropBucket {
176 minute: u64,
177 count: u64,
178}
179
180impl Default for DropWindow {
181 fn default() -> Self {
182 Self {
183 started_at: tokio::time::Instant::now(),
184 buckets: VecDeque::new(),
185 }
186 }
187}
188
189impl DropWindow {
190 const MINUTE: Duration = Duration::from_secs(60);
191 const BUCKETS: u64 = 10;
192
193 fn record(&mut self, now: tokio::time::Instant) {
194 let minute = self.minute_at(now);
195 self.prune_before(minute);
196 match self.buckets.back_mut() {
197 Some(bucket) if bucket.minute == minute => bucket.count += 1,
198 _ => self.buckets.push_back(DropBucket { minute, count: 1 }),
199 }
200 }
201
202 fn count_last_10m(&mut self, now: tokio::time::Instant) -> u64 {
203 let minute = self.minute_at(now);
204 self.prune_before(minute);
205 self.buckets.iter().map(|bucket| bucket.count).sum()
206 }
207
208 fn nonzero_minutes_last_10m(&mut self, now: tokio::time::Instant) -> u64 {
209 let minute = self.minute_at(now);
210 self.prune_before(minute);
211 self.buckets.len() as u64
212 }
213
214 fn minute_at(&self, now: tokio::time::Instant) -> u64 {
215 now.saturating_duration_since(self.started_at).as_secs() / Self::MINUTE.as_secs()
216 }
217
218 fn prune_before(&mut self, current_minute: u64) {
219 while self
220 .buckets
221 .front()
222 .is_some_and(|bucket| current_minute.saturating_sub(bucket.minute) >= Self::BUCKETS)
223 {
224 self.buckets.pop_front();
225 }
226 }
227}
228
229impl DaemonCounters {
230 pub fn new() -> Self {
231 Self::default()
232 }
233
234 pub fn snapshot(&self) -> Value {
237 let mut snapshot = serde_json::Map::new();
238 snapshot.insert(
239 "module_frames_dropped_no_route".into(),
240 self.module_frames_dropped_no_route
241 .load(Ordering::Relaxed)
242 .into(),
243 );
244 let mut drop_window = self
245 .module_frames_dropped_no_route_window
246 .lock()
247 .expect("drop-rate window mutex poisoned");
248 let now = tokio::time::Instant::now();
249 snapshot.insert(
250 "module_frames_dropped_no_route_last_10m".into(),
251 drop_window.count_last_10m(now).into(),
252 );
253 snapshot.insert(
254 "module_frames_dropped_no_route_nonzero_minutes_last_10m".into(),
255 drop_window.nonzero_minutes_last_10m(now).into(),
256 );
257 insert_nonempty_counts(
258 &mut snapshot,
259 "module_frames_dropped_no_route_by_module",
260 &self.module_frames_dropped_no_route_by_module,
261 );
262 snapshot.insert(
263 "module_frames_dropped_released_route".into(),
264 self.module_frames_dropped_released_route
265 .load(Ordering::Relaxed)
266 .into(),
267 );
268 insert_nonempty_counts(
269 &mut snapshot,
270 "module_frames_dropped_released_route_by_module",
271 &self.module_frames_dropped_released_route_by_module,
272 );
273 snapshot.insert(
274 "module_orphan_route_goodbyes_sent".into(),
275 self.module_orphan_route_goodbyes_sent
276 .load(Ordering::Relaxed)
277 .into(),
278 );
279 insert_nonempty_counts(
280 &mut snapshot,
281 "route_open_refused_by_code",
282 &self.route_open_refused_by_code,
283 );
284 insert_nonempty_counts(
285 &mut snapshot,
286 "route_open_accepted_by_principal",
287 &self.route_open_accepted_by_principal,
288 );
289 snapshot.insert(
290 "module_requests_dropped_stale_route".into(),
291 self.module_requests_dropped_stale_route
292 .load(Ordering::Relaxed)
293 .into(),
294 );
295 snapshot.insert(
296 "client_frames_dropped_stale_route".into(),
297 self.client_frames_dropped_stale_route
298 .load(Ordering::Relaxed)
299 .into(),
300 );
301 snapshot.insert(
302 "client_egress_close_delivery_failed".into(),
303 self.client_egress_close_delivery_failed
304 .load(Ordering::Relaxed)
305 .into(),
306 );
307 snapshot.insert(
308 "goodbye_relay_client_failed".into(),
309 self.goodbye_relay_client_failed
310 .load(Ordering::Relaxed)
311 .into(),
312 );
313 snapshot.insert(
314 "goodbye_relay_module_dropped".into(),
315 self.goodbye_relay_module_dropped
316 .load(Ordering::Relaxed)
317 .into(),
318 );
319 insert_nonempty_counts(
320 &mut snapshot,
321 "goodbye_relay_module_dropped_by_module",
322 &self.goodbye_relay_module_dropped_by_module,
323 );
324 snapshot.insert(
325 "route_released_epoch_fenced".into(),
326 self.route_released_epoch_fenced
327 .load(Ordering::Relaxed)
328 .into(),
329 );
330 snapshot.insert(
331 "route_release_stale_skipped".into(),
332 self.route_release_stale_skipped
333 .load(Ordering::Relaxed)
334 .into(),
335 );
336 snapshot.insert(
337 "drains_with_undeclared_gauge".into(),
338 self.drains_with_undeclared_gauge
339 .load(Ordering::Relaxed)
340 .into(),
341 );
342 Value::Object(snapshot)
343 }
344
345 pub(crate) fn increment_module_frames_dropped_no_route(&self, module_id: Option<&str>) {
346 self.module_frames_dropped_no_route
347 .fetch_add(1, Ordering::Relaxed);
348 if let Some(module_id) = module_id {
349 increment_keyed_count(&self.module_frames_dropped_no_route_by_module, module_id);
350 }
351 self.module_frames_dropped_no_route_window
352 .lock()
353 .expect("drop-rate window mutex poisoned")
354 .record(tokio::time::Instant::now());
355 }
356
357 pub(crate) fn increment_module_frames_dropped_released_route(&self, module_id: Option<&str>) {
361 self.module_frames_dropped_released_route
362 .fetch_add(1, Ordering::Relaxed);
363 if let Some(module_id) = module_id {
364 increment_keyed_count(
365 &self.module_frames_dropped_released_route_by_module,
366 module_id,
367 );
368 }
369 }
370
371 pub(crate) fn increment_module_orphan_route_goodbyes_sent(&self) {
372 self.module_orphan_route_goodbyes_sent
373 .fetch_add(1, Ordering::Relaxed);
374 }
375
376 pub(crate) fn increment_route_open_refused(&self, code: &'static str) {
377 debug_assert!(ROUTE_OPEN_REFUSAL_COUNTER_CODES.contains(&code));
378 increment_keyed_count(&self.route_open_refused_by_code, code);
379 }
380
381 pub(crate) fn increment_route_open_accepted(&self, principal: &str) {
391 increment_keyed_count(&self.route_open_accepted_by_principal, principal);
392 }
393
394 pub(crate) fn increment_module_requests_dropped_stale_route(&self) {
395 self.module_requests_dropped_stale_route
396 .fetch_add(1, Ordering::Relaxed);
397 }
398
399 pub(crate) fn increment_client_frames_dropped_stale_route(&self) {
400 self.client_frames_dropped_stale_route
401 .fetch_add(1, Ordering::Relaxed);
402 }
403
404 pub(crate) fn increment_client_egress_close_delivery_failed(&self) {
405 self.client_egress_close_delivery_failed
406 .fetch_add(1, Ordering::Relaxed);
407 }
408
409 pub(crate) fn increment_goodbye_relay_client_failed(&self) {
410 self.goodbye_relay_client_failed
411 .fetch_add(1, Ordering::Relaxed);
412 }
413
414 pub(crate) fn increment_goodbye_relay_module_dropped(&self, module_id: Option<&str>) {
415 self.goodbye_relay_module_dropped
416 .fetch_add(1, Ordering::Relaxed);
417 if let Some(module_id) = module_id {
418 increment_keyed_count(&self.goodbye_relay_module_dropped_by_module, module_id);
419 }
420 }
421
422 pub(crate) fn increment_route_released_epoch_fenced(&self) {
423 self.route_released_epoch_fenced
424 .fetch_add(1, Ordering::Relaxed);
425 }
426
427 pub(crate) fn increment_route_release_stale_skipped(&self) {
428 self.route_release_stale_skipped
429 .fetch_add(1, Ordering::Relaxed);
430 }
431
432 pub(crate) fn increment_drains_with_undeclared_gauge(&self) {
433 self.drains_with_undeclared_gauge
434 .fetch_add(1, Ordering::Relaxed);
435 }
436}
437
438fn increment_keyed_count(counts: &Mutex<HashMap<String, u64>>, key: &str) {
439 *counts
440 .lock()
441 .expect("keyed counter mutex poisoned")
442 .entry(key.to_string())
443 .or_default() += 1;
444}
445
446fn insert_nonempty_counts(
447 snapshot: &mut serde_json::Map<String, Value>,
448 key: &str,
449 counts: &Mutex<HashMap<String, u64>>,
450) {
451 let counts: MutexGuard<'_, HashMap<String, u64>> =
452 counts.lock().expect("keyed counter mutex poisoned");
453 if !counts.is_empty() {
454 snapshot.insert(key.to_string(), json!(&*counts));
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
463 fn counter_snapshot_includes_zero_rate_and_omits_empty_module_maps() {
464 let counters = DaemonCounters::new();
465 let snapshot = counters.snapshot();
466
467 assert_eq!(snapshot["module_frames_dropped_no_route_last_10m"], 0);
468 assert_eq!(
469 snapshot["module_frames_dropped_no_route_nonzero_minutes_last_10m"],
470 0
471 );
472 assert!(snapshot
473 .get("module_frames_dropped_no_route_by_module")
474 .is_none());
475 assert!(snapshot
476 .get("goodbye_relay_module_dropped_by_module")
477 .is_none());
478 }
479
480 #[tokio::test(start_paused = true)]
481 async fn module_frame_drops_are_attributed_to_the_emitting_module() {
482 let counters = DaemonCounters::new();
483 counters.increment_module_frames_dropped_no_route(Some("alpha"));
484 counters.increment_module_frames_dropped_no_route(Some("alpha"));
485
486 let snapshot = counters.snapshot();
487 assert_eq!(snapshot["module_frames_dropped_no_route"], 2);
488 assert_eq!(
489 snapshot["module_frames_dropped_no_route_by_module"],
490 json!({ "alpha": 2 })
491 );
492 assert_eq!(snapshot["module_frames_dropped_no_route_last_10m"], 2);
493 }
494
495 #[tokio::test(start_paused = true)]
496 async fn frame_drop_rate_ages_out_after_ten_minute_buckets() {
497 let counters = DaemonCounters::new();
498 counters.increment_module_frames_dropped_no_route(Some("alpha"));
499
500 tokio::time::advance(Duration::from_secs(9 * 60)).await;
501 assert_eq!(
502 counters.snapshot()["module_frames_dropped_no_route_last_10m"],
503 1
504 );
505
506 tokio::time::advance(Duration::from_secs(60)).await;
507 assert_eq!(
508 counters.snapshot()["module_frames_dropped_no_route_last_10m"],
509 0
510 );
511 }
512
513 #[tokio::test(start_paused = true)]
514 async fn frame_drop_window_counts_only_nonzero_minutes() {
515 let counters = DaemonCounters::new();
516 for minute in 0..10 {
517 if minute > 0 {
518 tokio::time::advance(Duration::from_secs(60)).await;
519 }
520 if minute != 4 {
521 counters.increment_module_frames_dropped_no_route(Some("alpha"));
522 }
523 }
524
525 assert_eq!(
526 counters.snapshot()["module_frames_dropped_no_route_nonzero_minutes_last_10m"],
527 9
528 );
529 }
530
531 #[test]
532 fn goodbye_relay_drops_are_attributed_to_the_target_module() {
533 let counters = DaemonCounters::new();
534 counters.increment_goodbye_relay_module_dropped(Some("alpha"));
535
536 assert_eq!(
537 counters.snapshot()["goodbye_relay_module_dropped_by_module"],
538 json!({ "alpha": 1 })
539 );
540 }
541}