1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::time::Duration;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct PatternDetectionConfig {
14 pub enable_failure_patterns: bool,
16 pub enable_performance_patterns: bool,
18 pub enable_data_quality_patterns: bool,
20 pub min_pattern_threshold: usize,
22 pub detection_window: Duration,
24 pub enable_anomaly_detection: bool,
26}
27
28impl Default for PatternDetectionConfig {
29 fn default() -> Self {
30 Self {
31 enable_failure_patterns: true,
32 enable_performance_patterns: true,
33 enable_data_quality_patterns: true,
34 min_pattern_threshold: 3,
35 detection_window: Duration::from_secs(3600), enable_anomaly_detection: true,
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum PatternType {
44 ValidationFailure,
46 PerformanceBottleneck,
48 DataQuality,
50 ConstraintUsage,
52 TargetSelection,
54 Anomaly,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct DetectedPattern {
61 pub pattern_type: PatternType,
63 pub pattern_id: String,
65 pub description: String,
67 pub severity: f64,
69 pub occurrence_count: usize,
71 pub first_detected: DateTime<Utc>,
73 pub last_detected: DateTime<Utc>,
75 pub metadata: HashMap<String, String>,
77 pub suggested_actions: Vec<String>,
79}
80
81#[derive(Debug)]
83pub struct PatternDetector {
84 config: PatternDetectionConfig,
85 detected_patterns: HashMap<String, DetectedPattern>,
86 failure_patterns: HashMap<String, usize>,
87 performance_patterns: HashMap<String, Vec<Duration>>,
88 data_quality_patterns: HashMap<String, usize>,
89 last_cleanup: DateTime<Utc>,
90}
91
92impl PatternDetector {
93 pub fn new(config: PatternDetectionConfig) -> Self {
95 Self {
96 config,
97 detected_patterns: HashMap::new(),
98 failure_patterns: HashMap::new(),
99 performance_patterns: HashMap::new(),
100 data_quality_patterns: HashMap::new(),
101 last_cleanup: Utc::now(),
102 }
103 }
104
105 pub fn with_default_config() -> Self {
107 Self::new(PatternDetectionConfig::default())
108 }
109
110 pub fn record_validation_failure(
112 &mut self,
113 shape_iri: &str,
114 constraint_type: &str,
115 failure_reason: &str,
116 ) {
117 if !self.config.enable_failure_patterns {
118 return;
119 }
120
121 let pattern_key = format!("{shape_iri}:{constraint_type}:{failure_reason}");
122 let new_count = {
123 let count = self
124 .failure_patterns
125 .entry(pattern_key.clone())
126 .or_insert(0);
127 *count += 1;
128 *count
129 };
130
131 if new_count >= self.config.min_pattern_threshold {
132 self.create_or_update_pattern(
133 PatternType::ValidationFailure,
134 pattern_key,
135 format!("Recurring validation failure in shape {shape_iri} for constraint {constraint_type} with reason: {failure_reason}"),
136 0.7, new_count,
138 vec![
139 "Review shape definition for potential issues".to_string(),
140 "Check data quality for this constraint".to_string(),
141 "Consider relaxing constraint if appropriate".to_string(),
142 ],
143 );
144 }
145 }
146
147 pub fn record_performance_data(&mut self, operation: &str, duration: Duration) {
149 if !self.config.enable_performance_patterns {
150 return;
151 }
152
153 let (durations_len, avg_duration) = {
154 let durations = self
155 .performance_patterns
156 .entry(operation.to_string())
157 .or_default();
158 durations.push(duration);
159
160 durations.retain(|d| d.as_millis() > 0);
162
163 let len = durations.len();
164 let avg_duration = if len > 0 {
165 Duration::from_millis(
166 durations.iter().map(|d| d.as_millis() as u64).sum::<u64>() / len as u64,
167 )
168 } else {
169 Duration::from_millis(0)
170 };
171 (len, avg_duration)
172 };
173
174 if durations_len >= self.config.min_pattern_threshold
176 && avg_duration > Duration::from_millis(1000)
177 {
178 self.create_or_update_pattern(
179 PatternType::PerformanceBottleneck,
180 format!("slow_{operation}"),
181 format!("Performance bottleneck detected in operation: {operation} (avg: {avg_duration:?})"),
182 0.8, durations_len,
184 vec![
185 "Optimize the operation implementation".to_string(),
186 "Consider caching strategies".to_string(),
187 "Review data structures and algorithms".to_string(),
188 ],
189 );
190 }
191 }
192
193 pub fn record_data_quality_issue(&mut self, issue_type: &str, resource_iri: &str) {
195 if !self.config.enable_data_quality_patterns {
196 return;
197 }
198
199 let pattern_key = format!("{issue_type}:{resource_iri}");
200 let new_count = {
201 let count = self
202 .data_quality_patterns
203 .entry(pattern_key.clone())
204 .or_insert(0);
205 *count += 1;
206 *count
207 };
208
209 if new_count >= self.config.min_pattern_threshold {
210 self.create_or_update_pattern(
211 PatternType::DataQuality,
212 pattern_key,
213 format!("Data quality issue detected: {issue_type} for resource {resource_iri}"),
214 0.6, new_count,
216 vec![
217 "Review data source quality".to_string(),
218 "Implement data validation rules".to_string(),
219 "Consider data cleansing processes".to_string(),
220 ],
221 );
222 }
223 }
224
225 pub fn detect_anomalies(&mut self) -> Vec<DetectedPattern> {
227 if !self.config.enable_anomaly_detection {
228 return Vec::new();
229 }
230
231 let mut anomalies = Vec::new();
232
233 for (pattern_key, count) in &self.failure_patterns {
235 if *count > self.config.min_pattern_threshold * 5 {
236 let anomaly = DetectedPattern {
238 pattern_type: PatternType::Anomaly,
239 pattern_id: format!("anomaly_spike_{pattern_key}"),
240 description: format!("Anomalous spike in failures: {count} occurrences"),
241 severity: 0.9,
242 occurrence_count: *count,
243 first_detected: Utc::now(),
244 last_detected: Utc::now(),
245 metadata: HashMap::new(),
246 suggested_actions: vec![
247 "Investigate root cause immediately".to_string(),
248 "Check for data source changes".to_string(),
249 "Review recent system changes".to_string(),
250 ],
251 };
252 anomalies.push(anomaly);
253 }
254 }
255
256 anomalies
257 }
258
259 pub fn get_detected_patterns(&self) -> Vec<&DetectedPattern> {
261 self.detected_patterns.values().collect()
262 }
263
264 pub fn get_patterns_by_type(&self, pattern_type: PatternType) -> Vec<&DetectedPattern> {
266 self.detected_patterns
267 .values()
268 .filter(|p| p.pattern_type == pattern_type)
269 .collect()
270 }
271
272 pub fn cleanup_old_patterns(&mut self) {
274 let cutoff = Utc::now()
275 - chrono::Duration::from_std(self.config.detection_window).unwrap_or_default();
276
277 self.detected_patterns
278 .retain(|_, pattern| pattern.last_detected > cutoff);
279
280 if Utc::now()
282 .signed_duration_since(self.last_cleanup)
283 .num_minutes()
284 > 60
285 {
286 self.failure_patterns.clear();
287 self.performance_patterns.clear();
288 self.data_quality_patterns.clear();
289 self.last_cleanup = Utc::now();
290 }
291 }
292
293 pub fn generate_report(&self) -> PatternAnalysisReport {
295 let patterns_by_type: HashMap<PatternType, Vec<&DetectedPattern>> = self
296 .detected_patterns
297 .values()
298 .fold(HashMap::new(), |mut acc, pattern| {
299 acc.entry(pattern.pattern_type.clone())
300 .or_default()
301 .push(pattern);
302 acc
303 });
304
305 let high_severity_patterns = self
306 .detected_patterns
307 .values()
308 .filter(|p| p.severity >= 0.8)
309 .cloned()
310 .collect();
311
312 PatternAnalysisReport {
313 total_patterns: self.detected_patterns.len(),
314 patterns_by_type: patterns_by_type
315 .into_iter()
316 .map(|(k, v)| (k, v.len()))
317 .collect(),
318 high_severity_patterns,
319 analysis_timestamp: Utc::now(),
320 summary: self.generate_summary(),
321 }
322 }
323
324 fn create_or_update_pattern(
326 &mut self,
327 pattern_type: PatternType,
328 pattern_id: String,
329 description: String,
330 severity: f64,
331 occurrence_count: usize,
332 suggested_actions: Vec<String>,
333 ) {
334 let now = Utc::now();
335
336 match self.detected_patterns.get_mut(&pattern_id) {
337 Some(existing_pattern) => {
338 existing_pattern.occurrence_count = occurrence_count;
339 existing_pattern.last_detected = now;
340 existing_pattern.severity = severity;
341 }
342 None => {
343 let pattern = DetectedPattern {
344 pattern_type,
345 pattern_id: pattern_id.clone(),
346 description,
347 severity,
348 occurrence_count,
349 first_detected: now,
350 last_detected: now,
351 metadata: HashMap::new(),
352 suggested_actions,
353 };
354 self.detected_patterns.insert(pattern_id, pattern);
355 }
356 }
357 }
358
359 fn generate_summary(&self) -> String {
361 let total = self.detected_patterns.len();
362 let high_severity = self
363 .detected_patterns
364 .values()
365 .filter(|p| p.severity >= 0.8)
366 .count();
367 let medium_severity = self
368 .detected_patterns
369 .values()
370 .filter(|p| p.severity >= 0.5 && p.severity < 0.8)
371 .count();
372 let low_severity = total - high_severity - medium_severity;
373
374 format!(
375 "Pattern Analysis Summary: {total} total patterns detected ({high_severity} high severity, {medium_severity} medium severity, {low_severity} low severity)"
376 )
377 }
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct PatternAnalysisReport {
383 pub total_patterns: usize,
385 pub patterns_by_type: HashMap<PatternType, usize>,
387 pub high_severity_patterns: Vec<DetectedPattern>,
389 pub analysis_timestamp: DateTime<Utc>,
391 pub summary: String,
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_pattern_detector_creation() {
401 let detector = PatternDetector::with_default_config();
402 assert_eq!(detector.detected_patterns.len(), 0);
403 }
404
405 #[test]
406 fn test_validation_failure_pattern_detection() {
407 let mut detector = PatternDetector::with_default_config();
408
409 for _ in 0..5 {
411 detector.record_validation_failure(
412 "http://example.org/shape1",
413 "minCount",
414 "insufficient_values",
415 );
416 }
417
418 let patterns = detector.get_detected_patterns();
419 assert!(!patterns.is_empty());
420 assert!(patterns
421 .iter()
422 .any(|p| p.pattern_type == PatternType::ValidationFailure));
423 }
424
425 #[test]
426 fn test_performance_pattern_detection() {
427 let mut detector = PatternDetector::with_default_config();
428
429 for _ in 0..5 {
431 detector.record_performance_data("shape_validation", Duration::from_millis(2000));
432 }
433
434 let patterns = detector.get_detected_patterns();
435 assert!(patterns
436 .iter()
437 .any(|p| p.pattern_type == PatternType::PerformanceBottleneck));
438 }
439
440 #[test]
441 fn test_data_quality_pattern_detection() {
442 let mut detector = PatternDetector::with_default_config();
443
444 for _ in 0..5 {
446 detector.record_data_quality_issue(
447 "missing_required_property",
448 "http://example.org/resource1",
449 );
450 }
451
452 let patterns = detector.get_detected_patterns();
453 assert!(patterns
454 .iter()
455 .any(|p| p.pattern_type == PatternType::DataQuality));
456 }
457
458 #[test]
459 fn test_anomaly_detection() {
460 let mut detector = PatternDetector::with_default_config();
461
462 for _ in 0..20 {
464 detector.record_validation_failure(
466 "http://example.org/shape1",
467 "minCount",
468 "insufficient_values",
469 );
470 }
471
472 let anomalies = detector.detect_anomalies();
473 assert!(!anomalies.is_empty());
474 assert!(anomalies
475 .iter()
476 .any(|a| a.pattern_type == PatternType::Anomaly));
477 }
478
479 #[test]
480 fn test_pattern_cleanup() {
481 let mut detector = PatternDetector::with_default_config();
482
483 detector.record_validation_failure("shape1", "constraint1", "reason1");
485 assert!(!detector.failure_patterns.is_empty());
486
487 detector.cleanup_old_patterns();
489 assert!(!detector.failure_patterns.is_empty());
491 }
492
493 #[test]
494 fn test_report_generation() {
495 let mut detector = PatternDetector::with_default_config();
496
497 for _ in 0..5 {
499 detector.record_validation_failure("shape1", "constraint1", "reason1");
500 detector.record_performance_data("operation1", Duration::from_millis(2000));
501 detector.record_data_quality_issue("issue1", "resource1");
502 }
503
504 let report = detector.generate_report();
505 assert!(report.total_patterns > 0);
506 assert!(!report.patterns_by_type.is_empty());
507 assert!(!report.summary.is_empty());
508 }
509
510 #[test]
511 fn test_patterns_by_type_filtering() {
512 let mut detector = PatternDetector::with_default_config();
513
514 for _ in 0..5 {
516 detector.record_validation_failure("shape1", "constraint1", "reason1");
517 detector.record_performance_data("operation1", Duration::from_millis(2000));
518 }
519
520 let validation_patterns = detector.get_patterns_by_type(PatternType::ValidationFailure);
521 let performance_patterns =
522 detector.get_patterns_by_type(PatternType::PerformanceBottleneck);
523
524 assert!(!validation_patterns.is_empty());
525 assert!(!performance_patterns.is_empty());
526 }
527}