1use parking_lot::RwLock;
4use siphon_protocol::TunnelType;
5use std::collections::VecDeque;
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8
9const HISTORY_SIZE: usize = 60;
11
12const MAX_RECENT_REQUESTS: usize = 100;
14
15#[derive(Clone)]
17pub struct MetricsCollector {
18 inner: Arc<RwLock<MetricsState>>,
19}
20
21pub struct MetricsState {
23 pub tunnel_info: Option<TunnelInfo>,
25 pub connected_at: Option<Instant>,
26
27 pub total_requests: u64,
29 pub requests_in_progress: u64,
30 pub status_codes: StatusCodeDistribution,
31
32 response_times: VecDeque<Duration>,
34
35 pub active_tcp_connections: u64,
37 pub total_tcp_connections: u64,
38 pub bytes_in: u64,
39 pub bytes_out: u64,
40
41 pub error_count: u64,
43 pub last_error: Option<String>,
44
45 pub recent_requests: VecDeque<RequestLogEntry>,
47
48 pub request_rate_history: VecDeque<u64>,
50 pub response_time_p50_history: VecDeque<u64>,
51 pub response_time_p99_history: VecDeque<u64>,
52 pub bytes_in_rate_history: VecDeque<u64>,
53 pub bytes_out_rate_history: VecDeque<u64>,
54
55 requests_this_second: u64,
57 bytes_in_this_second: u64,
58 bytes_out_this_second: u64,
59 last_tick: Instant,
60}
61
62#[derive(Debug, Clone)]
64pub struct TunnelInfo {
65 pub subdomain: String,
66 pub url: String,
67 pub port: Option<u16>,
68 pub tunnel_type: TunnelType,
69}
70
71#[derive(Debug, Clone, Default)]
73pub struct StatusCodeDistribution {
74 pub code_2xx: u64,
75 pub code_3xx: u64,
76 pub code_4xx: u64,
77 pub code_5xx: u64,
78}
79
80#[derive(Debug, Clone, Default)]
82pub struct ResponseTimeStats {
83 pub min: Option<Duration>,
84 pub max: Option<Duration>,
85 pub avg: Option<Duration>,
86 pub p50: Option<Duration>,
87 pub p95: Option<Duration>,
88 pub p99: Option<Duration>,
89}
90
91#[derive(Debug, Clone)]
93pub struct RequestLogEntry {
94 pub timestamp: chrono::DateTime<chrono::Local>,
95 pub method: String,
96 pub uri: String,
97 pub status: u16,
98 pub duration: Duration,
99 pub bytes: usize,
100}
101
102#[derive(Debug, Clone)]
104pub struct MetricsSnapshot {
105 pub tunnel_info: Option<TunnelInfo>,
106 pub uptime: Option<Duration>,
107 pub total_requests: u64,
108 pub requests_per_second: f64,
109 pub status_distribution: StatusCodeDistribution,
110 pub response_times: ResponseTimeStats,
111 pub active_connections: u64,
112 pub total_connections: u64,
113 pub bytes_in: u64,
114 pub bytes_out: u64,
115 pub error_count: u64,
116 pub last_error: Option<String>,
117 pub recent_requests: Vec<RequestLogEntry>,
118
119 pub request_rate_history: Vec<u64>,
121 pub response_time_p50_history: Vec<u64>,
122 pub response_time_p99_history: Vec<u64>,
123 pub bytes_in_rate_history: Vec<u64>,
124 pub bytes_out_rate_history: Vec<u64>,
125}
126
127impl Default for MetricsState {
128 fn default() -> Self {
129 Self {
130 tunnel_info: None,
131 connected_at: None,
132 total_requests: 0,
133 requests_in_progress: 0,
134 status_codes: StatusCodeDistribution::default(),
135 response_times: VecDeque::with_capacity(1000),
136 active_tcp_connections: 0,
137 total_tcp_connections: 0,
138 bytes_in: 0,
139 bytes_out: 0,
140 error_count: 0,
141 last_error: None,
142 recent_requests: VecDeque::with_capacity(MAX_RECENT_REQUESTS),
143 request_rate_history: VecDeque::with_capacity(HISTORY_SIZE),
144 response_time_p50_history: VecDeque::with_capacity(HISTORY_SIZE),
145 response_time_p99_history: VecDeque::with_capacity(HISTORY_SIZE),
146 bytes_in_rate_history: VecDeque::with_capacity(HISTORY_SIZE),
147 bytes_out_rate_history: VecDeque::with_capacity(HISTORY_SIZE),
148 requests_this_second: 0,
149 bytes_in_this_second: 0,
150 bytes_out_this_second: 0,
151 last_tick: Instant::now(),
152 }
153 }
154}
155
156impl MetricsCollector {
157 pub fn new() -> Self {
159 Self {
160 inner: Arc::new(RwLock::new(MetricsState::default())),
161 }
162 }
163
164 pub fn set_tunnel_info(&self, info: TunnelInfo) {
166 let mut state = self.inner.write();
167 state.tunnel_info = Some(info);
168 state.connected_at = Some(Instant::now());
169 }
170
171 pub fn record_request_start(&self) {
173 let mut state = self.inner.write();
174 state.requests_in_progress += 1;
175 }
176
177 pub fn record_request_complete(
179 &self,
180 status: u16,
181 duration: Duration,
182 bytes: usize,
183 method: String,
184 uri: String,
185 ) {
186 let mut state = self.inner.write();
187
188 state.total_requests += 1;
190 state.requests_in_progress = state.requests_in_progress.saturating_sub(1);
191 state.requests_this_second += 1;
192
193 match status {
195 200..=299 => state.status_codes.code_2xx += 1,
196 300..=399 => state.status_codes.code_3xx += 1,
197 400..=499 => state.status_codes.code_4xx += 1,
198 _ => state.status_codes.code_5xx += 1,
199 }
200
201 state.response_times.push_back(duration);
203 if state.response_times.len() > 1000 {
204 state.response_times.pop_front();
205 }
206
207 state.recent_requests.push_back(RequestLogEntry {
209 timestamp: chrono::Local::now(),
210 method,
211 uri,
212 status,
213 duration,
214 bytes,
215 });
216 if state.recent_requests.len() > MAX_RECENT_REQUESTS {
217 state.recent_requests.pop_front();
218 }
219 }
220
221 pub fn record_tcp_connect(&self) {
223 let mut state = self.inner.write();
224 state.active_tcp_connections += 1;
225 state.total_tcp_connections += 1;
226 }
227
228 pub fn record_tcp_disconnect(&self) {
230 let mut state = self.inner.write();
231 state.active_tcp_connections = state.active_tcp_connections.saturating_sub(1);
232 }
233
234 pub fn record_bytes_in(&self, bytes: u64) {
236 let mut state = self.inner.write();
237 state.bytes_in += bytes;
238 state.bytes_in_this_second += bytes;
239 }
240
241 pub fn record_bytes_out(&self, bytes: u64) {
243 let mut state = self.inner.write();
244 state.bytes_out += bytes;
245 state.bytes_out_this_second += bytes;
246 }
247
248 pub fn record_error(&self, error: String) {
250 let mut state = self.inner.write();
251 state.error_count += 1;
252 state.last_error = Some(error);
253 }
254
255 pub fn tick(&self) {
257 let mut state = self.inner.write();
258
259 let elapsed = state.last_tick.elapsed();
261 if elapsed < Duration::from_millis(900) {
262 return; }
264
265 let requests_this_sec = state.requests_this_second;
267 state.request_rate_history.push_back(requests_this_sec);
268 if state.request_rate_history.len() > HISTORY_SIZE {
269 state.request_rate_history.pop_front();
270 }
271
272 let bytes_in_this_sec = state.bytes_in_this_second;
274 state.bytes_in_rate_history.push_back(bytes_in_this_sec);
275 if state.bytes_in_rate_history.len() > HISTORY_SIZE {
276 state.bytes_in_rate_history.pop_front();
277 }
278
279 let bytes_out_this_sec = state.bytes_out_this_second;
280 state.bytes_out_rate_history.push_back(bytes_out_this_sec);
281 if state.bytes_out_rate_history.len() > HISTORY_SIZE {
282 state.bytes_out_rate_history.pop_front();
283 }
284
285 let (p50, p99) = calculate_percentiles(&state.response_times);
287 state
288 .response_time_p50_history
289 .push_back(p50.map(|d| d.as_millis() as u64).unwrap_or(0));
290 if state.response_time_p50_history.len() > HISTORY_SIZE {
291 state.response_time_p50_history.pop_front();
292 }
293
294 state
295 .response_time_p99_history
296 .push_back(p99.map(|d| d.as_millis() as u64).unwrap_or(0));
297 if state.response_time_p99_history.len() > HISTORY_SIZE {
298 state.response_time_p99_history.pop_front();
299 }
300
301 state.requests_this_second = 0;
303 state.bytes_in_this_second = 0;
304 state.bytes_out_this_second = 0;
305 state.last_tick = Instant::now();
306 }
307
308 pub fn snapshot(&self) -> MetricsSnapshot {
310 let state = self.inner.read();
311
312 let uptime = state.connected_at.map(|t| t.elapsed());
313
314 let recent_requests: u64 = state.request_rate_history.iter().rev().take(10).sum();
316 let sample_count = state.request_rate_history.len().min(10) as f64;
317 let requests_per_second = if sample_count > 0.0 {
318 recent_requests as f64 / sample_count
319 } else {
320 0.0
321 };
322
323 let response_times = calculate_response_time_stats(&state.response_times);
325
326 MetricsSnapshot {
327 tunnel_info: state.tunnel_info.clone(),
328 uptime,
329 total_requests: state.total_requests,
330 requests_per_second,
331 status_distribution: state.status_codes.clone(),
332 response_times,
333 active_connections: state.active_tcp_connections,
334 total_connections: state.total_tcp_connections,
335 bytes_in: state.bytes_in,
336 bytes_out: state.bytes_out,
337 error_count: state.error_count,
338 last_error: state.last_error.clone(),
339 recent_requests: state.recent_requests.iter().cloned().collect(),
340
341 request_rate_history: pad_history(&state.request_rate_history, HISTORY_SIZE),
343 response_time_p50_history: pad_history(&state.response_time_p50_history, HISTORY_SIZE),
344 response_time_p99_history: pad_history(&state.response_time_p99_history, HISTORY_SIZE),
345 bytes_in_rate_history: pad_history(&state.bytes_in_rate_history, HISTORY_SIZE),
346 bytes_out_rate_history: pad_history(&state.bytes_out_rate_history, HISTORY_SIZE),
347 }
348 }
349}
350
351fn pad_history(data: &VecDeque<u64>, size: usize) -> Vec<u64> {
353 let current_len = data.len();
354 if current_len >= size {
355 data.iter().copied().collect()
357 } else {
358 let mut result = vec![0u64; size - current_len];
360 result.extend(data.iter().copied());
361 result
362 }
363}
364
365impl Default for MetricsCollector {
366 fn default() -> Self {
367 Self::new()
368 }
369}
370
371fn calculate_response_time_stats(samples: &VecDeque<Duration>) -> ResponseTimeStats {
373 if samples.is_empty() {
374 return ResponseTimeStats::default();
375 }
376
377 let mut sorted: Vec<Duration> = samples.iter().copied().collect();
378 sorted.sort();
379
380 let min = sorted.first().copied();
381 let max = sorted.last().copied();
382
383 let sum: Duration = sorted.iter().sum();
384 let avg = Some(sum / sorted.len() as u32);
385
386 let p50 = percentile(&sorted, 50);
387 let p95 = percentile(&sorted, 95);
388 let p99 = percentile(&sorted, 99);
389
390 ResponseTimeStats {
391 min,
392 max,
393 avg,
394 p50,
395 p95,
396 p99,
397 }
398}
399
400fn percentile(sorted: &[Duration], p: usize) -> Option<Duration> {
402 if sorted.is_empty() {
403 return None;
404 }
405 let idx = (sorted.len() * p / 100).min(sorted.len() - 1);
406 Some(sorted[idx])
407}
408
409fn calculate_percentiles(samples: &VecDeque<Duration>) -> (Option<Duration>, Option<Duration>) {
411 if samples.is_empty() {
412 return (None, None);
413 }
414
415 let mut sorted: Vec<Duration> = samples.iter().copied().collect();
416 sorted.sort();
417
418 (percentile(&sorted, 50), percentile(&sorted, 99))
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn test_metrics_collector() {
427 let metrics = MetricsCollector::new();
428
429 metrics.record_request_start();
430 metrics.record_request_complete(
431 200,
432 Duration::from_millis(50),
433 1024,
434 "GET".into(),
435 "/api/test".into(),
436 );
437
438 let snapshot = metrics.snapshot();
439 assert_eq!(snapshot.total_requests, 1);
440 assert_eq!(snapshot.status_distribution.code_2xx, 1);
441 }
442
443 #[test]
444 fn test_status_code_distribution() {
445 let metrics = MetricsCollector::new();
446
447 for status in [200, 201, 301, 404, 500] {
448 metrics.record_request_complete(
449 status,
450 Duration::from_millis(10),
451 100,
452 "GET".into(),
453 "/".into(),
454 );
455 }
456
457 let snapshot = metrics.snapshot();
458 assert_eq!(snapshot.status_distribution.code_2xx, 2);
459 assert_eq!(snapshot.status_distribution.code_3xx, 1);
460 assert_eq!(snapshot.status_distribution.code_4xx, 1);
461 assert_eq!(snapshot.status_distribution.code_5xx, 1);
462 }
463
464 #[test]
465 fn test_response_time_percentiles() {
466 let metrics = MetricsCollector::new();
467
468 for ms in [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] {
470 metrics.record_request_complete(
471 200,
472 Duration::from_millis(ms),
473 100,
474 "GET".into(),
475 "/".into(),
476 );
477 }
478
479 let snapshot = metrics.snapshot();
480 assert!(snapshot.response_times.p50.is_some());
481 assert!(snapshot.response_times.p95.is_some());
482 assert!(snapshot.response_times.p99.is_some());
483 }
484
485 #[test]
486 fn test_tcp_connection_tracking() {
487 let metrics = MetricsCollector::new();
488
489 metrics.record_tcp_connect();
490 metrics.record_tcp_connect();
491 assert_eq!(metrics.snapshot().active_connections, 2);
492
493 metrics.record_tcp_disconnect();
494 assert_eq!(metrics.snapshot().active_connections, 1);
495 }
496}