1use std::collections::VecDeque;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::time::{Duration, Instant};
8
9use dashmap::DashMap;
10use parking_lot::RwLock;
11
12use super::config::PatternConfig;
13use super::fingerprinter::QueryFingerprint;
14use super::statistics::QueryExecution;
15
16#[derive(Debug, Clone)]
18pub enum PatternAlert {
19 NplusOne(NplusOnePattern),
21 Burst(QueryBurst),
23}
24
25impl PatternAlert {
26 pub fn severity(&self) -> u8 {
28 match self {
29 PatternAlert::NplusOne(p) => {
30 if p.repeat_count > 100 {
31 5
32 } else if p.repeat_count > 50 {
33 4
34 } else if p.repeat_count > 20 {
35 3
36 } else if p.repeat_count > 10 {
37 2
38 } else {
39 1
40 }
41 }
42 PatternAlert::Burst(b) => {
43 if b.query_count > 500 {
44 5
45 } else if b.query_count > 200 {
46 4
47 } else if b.query_count > 100 {
48 3
49 } else if b.query_count > 50 {
50 2
51 } else {
52 1
53 }
54 }
55 }
56 }
57
58 pub fn description(&self) -> String {
60 match self {
61 PatternAlert::NplusOne(p) => {
62 format!(
63 "N+1 query pattern: {} repeated {} times in session {}",
64 truncate(&p.fingerprint, 50),
65 p.repeat_count,
66 p.session_id
67 )
68 }
69 PatternAlert::Burst(b) => {
70 format!(
71 "Query burst: {} queries in {:?} from session {}",
72 b.query_count, b.window, b.session_id
73 )
74 }
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct NplusOnePattern {
82 pub session_id: String,
84
85 pub fingerprint: String,
87
88 pub fingerprint_hash: u64,
90
91 pub repeat_count: usize,
93
94 pub window: Duration,
96
97 pub first_seen_nanos: u64,
99
100 pub last_seen_nanos: u64,
102
103 pub tables: Vec<String>,
105}
106
107#[derive(Debug, Clone)]
109pub struct QueryBurst {
110 pub session_id: String,
112
113 pub query_count: usize,
115
116 pub window: Duration,
118
119 pub start_nanos: u64,
121
122 pub end_nanos: u64,
124
125 pub top_fingerprints: Vec<(u64, usize)>,
127}
128
129struct SessionHistory {
131 query_times: VecDeque<Instant>,
133
134 recent_fingerprints: VecDeque<(u64, Instant, String, Vec<String>)>,
136
137 last_activity: Instant,
139
140 session_id: String,
142}
143
144impl SessionHistory {
145 fn new(session_id: String) -> Self {
146 Self {
147 query_times: VecDeque::new(),
148 recent_fingerprints: VecDeque::new(),
149 last_activity: Instant::now(),
150 session_id,
151 }
152 }
153
154 fn record_query(&mut self, fingerprint: &QueryFingerprint, max_history: usize) {
155 let now = Instant::now();
156 self.last_activity = now;
157
158 self.query_times.push_back(now);
160 while self.query_times.len() > max_history {
161 self.query_times.pop_front();
162 }
163
164 self.recent_fingerprints.push_back((
166 fingerprint.hash,
167 now,
168 fingerprint.normalized.clone(),
169 fingerprint.tables.clone(),
170 ));
171 while self.recent_fingerprints.len() > max_history {
172 self.recent_fingerprints.pop_front();
173 }
174 }
175
176 fn count_in_window(&self, window: Duration) -> usize {
177 match Instant::now().checked_sub(window) {
181 Some(cutoff) => self.query_times.iter().filter(|t| **t > cutoff).count(),
182 None => self.query_times.len(),
183 }
184 }
185
186 fn count_fingerprint_in_window(&self, hash: u64, window: Duration) -> usize {
187 match Instant::now().checked_sub(window) {
190 Some(cutoff) => self
191 .recent_fingerprints
192 .iter()
193 .filter(|(h, t, _, _)| *h == hash && *t > cutoff)
194 .count(),
195 None => self
196 .recent_fingerprints
197 .iter()
198 .filter(|(h, _, _, _)| *h == hash)
199 .count(),
200 }
201 }
202
203 fn get_repeated_fingerprints(
204 &self,
205 threshold: usize,
206 ) -> Vec<(u64, usize, String, Vec<String>)> {
207 let mut counts: std::collections::HashMap<u64, (usize, String, Vec<String>)> =
208 std::collections::HashMap::new();
209
210 for (hash, _, normalized, tables) in &self.recent_fingerprints {
211 let entry = counts
212 .entry(*hash)
213 .or_insert((0, normalized.clone(), tables.clone()));
214 entry.0 += 1;
215 }
216
217 counts
218 .into_iter()
219 .filter(|(_, (count, _, _))| *count >= threshold)
220 .map(|(hash, (count, normalized, tables))| (hash, count, normalized, tables))
221 .collect()
222 }
223}
224
225pub struct PatternDetector {
227 config: PatternConfig,
229
230 sessions: DashMap<String, SessionHistory>,
232
233 alerts: RwLock<VecDeque<PatternAlert>>,
235
236 alert_count: AtomicU64,
238
239 last_cleanup: RwLock<Instant>,
241}
242
243impl PatternDetector {
244 pub fn new(config: PatternConfig) -> Self {
246 Self {
247 config,
248 sessions: DashMap::new(),
249 alerts: RwLock::new(VecDeque::new()),
250 alert_count: AtomicU64::new(0),
251 last_cleanup: RwLock::new(Instant::now()),
252 }
253 }
254
255 pub fn record_query(
257 &self,
258 session_id: &str,
259 _execution: &QueryExecution,
260 fingerprint: &QueryFingerprint,
261 ) {
262 self.maybe_cleanup();
264
265 let mut session = self
267 .sessions
268 .entry(session_id.to_string())
269 .or_insert_with(|| SessionHistory::new(session_id.to_string()));
270
271 session.record_query(fingerprint, self.config.session_history_size);
273
274 if self.config.n_plus_one_detection {
276 self.check_n_plus_one(&session, fingerprint);
277 }
278
279 if self.config.burst_detection {
281 self.check_burst(&session);
282 }
283 }
284
285 fn check_n_plus_one(&self, session: &SessionHistory, fingerprint: &QueryFingerprint) {
287 let count = session.count_fingerprint_in_window(fingerprint.hash, Duration::from_secs(5));
288
289 if count >= self.config.n_plus_one_threshold {
290 let pattern = NplusOnePattern {
291 session_id: session.session_id.clone(),
292 fingerprint: fingerprint.normalized.clone(),
293 fingerprint_hash: fingerprint.hash,
294 repeat_count: count,
295 window: Duration::from_secs(5),
296 first_seen_nanos: now_nanos(),
297 last_seen_nanos: now_nanos(),
298 tables: fingerprint.tables.clone(),
299 };
300
301 self.add_alert(PatternAlert::NplusOne(pattern));
302 }
303 }
304
305 fn check_burst(&self, session: &SessionHistory) {
307 let count = session.count_in_window(self.config.burst_window);
308
309 if count >= self.config.burst_threshold {
310 let repeated = session.get_repeated_fingerprints(3);
312 let top_fingerprints: Vec<_> = repeated
313 .iter()
314 .take(5)
315 .map(|(hash, count, _, _)| (*hash, *count))
316 .collect();
317
318 let burst = QueryBurst {
319 session_id: session.session_id.clone(),
320 query_count: count,
321 window: self.config.burst_window,
322 start_nanos: now_nanos().saturating_sub(self.config.burst_window.as_nanos() as u64),
326 end_nanos: now_nanos(),
327 top_fingerprints,
328 };
329
330 self.add_alert(PatternAlert::Burst(burst));
331 }
332 }
333
334 fn add_alert(&self, alert: PatternAlert) {
336 self.alert_count.fetch_add(1, Ordering::Relaxed);
337
338 let mut alerts = self.alerts.write();
339 alerts.push_back(alert);
340
341 while alerts.len() > 1000 {
343 alerts.pop_front();
344 }
345 }
346
347 pub fn get_alerts(&self) -> Vec<PatternAlert> {
349 self.alerts.read().iter().cloned().collect()
350 }
351
352 pub fn get_n_plus_one_alerts(&self) -> Vec<NplusOnePattern> {
354 self.alerts
355 .read()
356 .iter()
357 .filter_map(|a| match a {
358 PatternAlert::NplusOne(p) => Some(p.clone()),
359 _ => None,
360 })
361 .collect()
362 }
363
364 pub fn get_burst_alerts(&self) -> Vec<QueryBurst> {
366 self.alerts
367 .read()
368 .iter()
369 .filter_map(|a| match a {
370 PatternAlert::Burst(b) => Some(b.clone()),
371 _ => None,
372 })
373 .collect()
374 }
375
376 pub fn alert_count(&self) -> u64 {
378 self.alert_count.load(Ordering::Relaxed)
379 }
380
381 pub fn clear_alerts(&self) {
383 self.alerts.write().clear();
384 }
385
386 fn maybe_cleanup(&self) {
388 let now = Instant::now();
389 let mut last_cleanup = self.last_cleanup.write();
390
391 if now.duration_since(*last_cleanup) < Duration::from_secs(60) {
393 return;
394 }
395 *last_cleanup = now;
396 drop(last_cleanup);
397
398 let timeout = self.config.session_timeout;
400 self.sessions
401 .retain(|_, session| now.duration_since(session.last_activity) < timeout);
402
403 while self.sessions.len() > self.config.max_sessions {
405 let oldest = self
407 .sessions
408 .iter()
409 .min_by_key(|s| s.last_activity)
410 .map(|s| s.key().clone());
411
412 if let Some(key) = oldest {
413 self.sessions.remove(&key);
414 } else {
415 break;
416 }
417 }
418 }
419
420 pub fn session_count(&self) -> usize {
422 self.sessions.len()
423 }
424
425 pub fn reset(&self) {
427 self.sessions.clear();
428 self.alerts.write().clear();
429 self.alert_count.store(0, Ordering::Relaxed);
430 }
431}
432
433fn now_nanos() -> u64 {
434 std::time::SystemTime::now()
435 .duration_since(std::time::SystemTime::UNIX_EPOCH)
436 .map(|d| d.as_nanos() as u64)
437 .unwrap_or(0)
438}
439
440fn truncate(s: &str, max: usize) -> String {
441 if s.len() > max {
442 format!("{}...", &s[..max])
443 } else {
444 s.to_string()
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use crate::analytics::fingerprinter::QueryFingerprinter;
452
453 #[test]
454 fn test_pattern_detector_new() {
455 let config = PatternConfig::default();
456 let detector = PatternDetector::new(config);
457 assert_eq!(detector.session_count(), 0);
458 assert_eq!(detector.alert_count(), 0);
459 }
460
461 #[test]
462 fn test_n_plus_one_detection() {
463 let config = PatternConfig {
464 n_plus_one_threshold: 3,
465 burst_detection: false,
466 ..Default::default()
467 };
468
469 let detector = PatternDetector::new(config);
470 let fp = QueryFingerprinter::new();
471
472 let session_id = "session-1";
473
474 for i in 0..5 {
476 let query = format!("SELECT * FROM users WHERE id = {}", i);
477 let fingerprint = fp.fingerprint(&query);
478 let execution =
479 super::super::statistics::QueryExecution::new(query, Duration::from_millis(5));
480 detector.record_query(session_id, &execution, &fingerprint);
481 }
482
483 let alerts = detector.get_n_plus_one_alerts();
485 assert!(!alerts.is_empty(), "Should detect N+1 pattern");
486 }
487
488 #[test]
489 fn test_burst_detection() {
490 let config = PatternConfig {
491 burst_threshold: 5,
492 burst_window: Duration::from_secs(1),
493 n_plus_one_detection: false,
494 ..Default::default()
495 };
496
497 let detector = PatternDetector::new(config);
498 let fp = QueryFingerprinter::new();
499
500 let session_id = "session-1";
501
502 for i in 0..10 {
504 let query = format!("SELECT * FROM table_{}", i);
505 let fingerprint = fp.fingerprint(&query);
506 let execution =
507 super::super::statistics::QueryExecution::new(query, Duration::from_millis(1));
508 detector.record_query(session_id, &execution, &fingerprint);
509 }
510
511 let alerts = detector.get_burst_alerts();
513 assert!(!alerts.is_empty(), "Should detect burst pattern");
514 }
515
516 #[test]
517 fn test_alert_severity() {
518 let pattern = NplusOnePattern {
519 session_id: "session-1".to_string(),
520 fingerprint: "select * from users where id = ?".to_string(),
521 fingerprint_hash: 12345,
522 repeat_count: 25,
523 window: Duration::from_secs(5),
524 first_seen_nanos: 0,
525 last_seen_nanos: 0,
526 tables: vec!["users".to_string()],
527 };
528
529 let alert = PatternAlert::NplusOne(pattern);
530 assert_eq!(alert.severity(), 3);
531 }
532
533 #[test]
534 fn test_session_cleanup() {
535 let config = PatternConfig {
536 session_timeout: Duration::from_millis(100),
537 ..Default::default()
538 };
539
540 let detector = PatternDetector::new(config);
541 let fp = QueryFingerprinter::new();
542
543 let fingerprint = fp.fingerprint("SELECT 1");
545 let execution =
546 super::super::statistics::QueryExecution::new("SELECT 1", Duration::from_millis(1));
547 detector.record_query("session-1", &execution, &fingerprint);
548
549 assert_eq!(detector.session_count(), 1);
550
551 std::thread::sleep(Duration::from_millis(150));
553
554 detector.record_query("session-2", &execution, &fingerprint);
556
557 }
560
561 #[test]
562 fn test_reset() {
563 let config = PatternConfig::default();
564 let detector = PatternDetector::new(config);
565 let fp = QueryFingerprinter::new();
566
567 let fingerprint = fp.fingerprint("SELECT 1");
568 let execution =
569 super::super::statistics::QueryExecution::new("SELECT 1", Duration::from_millis(1));
570 detector.record_query("session-1", &execution, &fingerprint);
571
572 detector.reset();
573
574 assert_eq!(detector.session_count(), 0);
575 assert_eq!(detector.alert_count(), 0);
576 }
577
578 #[test]
579 fn test_alert_description() {
580 let pattern = NplusOnePattern {
581 session_id: "sess-123".to_string(),
582 fingerprint: "select * from users where id = ?".to_string(),
583 fingerprint_hash: 12345,
584 repeat_count: 10,
585 window: Duration::from_secs(5),
586 first_seen_nanos: 0,
587 last_seen_nanos: 0,
588 tables: vec!["users".to_string()],
589 };
590
591 let alert = PatternAlert::NplusOne(pattern);
592 let desc = alert.description();
593
594 assert!(desc.contains("N+1"));
595 assert!(desc.contains("10 times"));
596 assert!(desc.contains("sess-123"));
597 }
598
599 #[test]
600 fn window_larger_than_uptime_does_not_panic() {
601 let huge = Duration::from_secs(u64::MAX);
608
609 let fp = QueryFingerprinter::new();
611 let fingerprint = fp.fingerprint("SELECT * FROM users WHERE id = 1");
612 let mut history = SessionHistory::new("s".to_string());
613 for _ in 0..3 {
614 history.record_query(&fingerprint, 100);
615 }
616 assert_eq!(history.count_in_window(huge), 3);
617 assert_eq!(
618 history.count_fingerprint_in_window(fingerprint.hash, huge),
619 3
620 );
621
622 let config = PatternConfig {
625 burst_window: huge,
626 burst_threshold: 3,
627 n_plus_one_detection: false,
628 ..Default::default()
629 };
630 let detector = PatternDetector::new(config);
631 let execution =
632 super::super::statistics::QueryExecution::new("SELECT 1", Duration::from_millis(1));
633 for _ in 0..5 {
634 let f = fp.fingerprint("SELECT 1");
635 detector.record_query("s", &execution, &f);
636 }
637 assert!(!detector.get_burst_alerts().is_empty());
638 }
639}