backup_suite/smart/anomaly/
pattern.rs1use crate::core::history::{BackupHistory, BackupStatus};
6use crate::smart::error::{SmartError, SmartResult};
7use crate::smart::types::FailureRate;
8use chrono::Timelike;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
33pub struct PatternAnalyzer {
34 min_occurrences: usize,
35}
36
37impl PatternAnalyzer {
38 #[must_use]
44 pub const fn new() -> Self {
45 Self { min_occurrences: 3 }
46 }
47
48 pub fn calculate_failure_rate(&self, histories: &[BackupHistory]) -> SmartResult<FailureRate> {
54 if histories.is_empty() {
55 return Err(SmartError::InsufficientData {
56 required: 1,
57 actual: 0,
58 });
59 }
60
61 let total = histories.len();
62 let failed = histories
63 .iter()
64 .filter(|h| matches!(h.status, BackupStatus::Failed | BackupStatus::Partial))
65 .count();
66
67 let rate = failed as f64 / total as f64;
68 FailureRate::new(rate).map_err(SmartError::InvalidParameter)
69 }
70
71 pub fn detect_failure_patterns(
77 &self,
78 histories: &[BackupHistory],
79 ) -> SmartResult<Vec<FailurePattern>> {
80 let failed_histories: Vec<_> = histories
82 .iter()
83 .filter(|h| matches!(h.status, BackupStatus::Failed))
84 .collect();
85
86 if failed_histories.is_empty() {
87 return Ok(Vec::new());
88 }
89
90 let mut error_counts: HashMap<String, usize> = HashMap::new();
92
93 for history in &failed_histories {
94 if let Some(error_msg) = &history.error_message {
95 *error_counts.entry(error_msg.clone()).or_insert(0) += 1;
96 }
97 }
98
99 let mut patterns: Vec<FailurePattern> = error_counts
101 .into_iter()
102 .filter(|(_, count)| *count >= self.min_occurrences)
103 .map(|(error_message, count)| {
104 let frequency = count as f64 / failed_histories.len() as f64;
105 FailurePattern::new(error_message, count, frequency)
106 })
107 .collect();
108
109 patterns.sort_by(|a, b| b.count.cmp(&a.count));
111
112 Ok(patterns)
113 }
114
115 pub fn calculate_failure_rate_by_category(
121 &self,
122 histories: &[BackupHistory],
123 ) -> SmartResult<HashMap<String, FailureRate>> {
124 if histories.is_empty() {
125 return Ok(HashMap::new());
126 }
127
128 let mut category_stats: HashMap<String, (usize, usize)> = HashMap::new();
130
131 for history in histories {
132 let category = history
133 .category
134 .clone()
135 .unwrap_or_else(|| "未分類".to_string());
136
137 let (total, failed) = category_stats.entry(category).or_insert((0, 0));
138 *total += 1;
139 if matches!(history.status, BackupStatus::Failed | BackupStatus::Partial) {
140 *failed += 1;
141 }
142 }
143
144 let mut result = HashMap::new();
146 for (category, (total, failed)) in category_stats {
147 let rate = failed as f64 / total as f64;
148 let failure_rate = FailureRate::new(rate).map_err(SmartError::InvalidParameter)?;
149 result.insert(category, failure_rate);
150 }
151
152 Ok(result)
153 }
154
155 pub fn analyze_failure_by_hour(
161 &self,
162 histories: &[BackupHistory],
163 ) -> SmartResult<HashMap<u32, FailureRate>> {
164 if histories.is_empty() {
165 return Ok(HashMap::new());
166 }
167
168 let mut hour_stats: HashMap<u32, (usize, usize)> = HashMap::new();
170
171 for history in histories {
172 let hour = history.timestamp.hour();
173 let (total, failed) = hour_stats.entry(hour).or_insert((0, 0));
174 *total += 1;
175 if matches!(history.status, BackupStatus::Failed | BackupStatus::Partial) {
176 *failed += 1;
177 }
178 }
179
180 let mut result = HashMap::new();
182 for (hour, (total, failed)) in hour_stats {
183 let rate = failed as f64 / total as f64;
184 let failure_rate = FailureRate::new(rate).map_err(SmartError::InvalidParameter)?;
185 result.insert(hour, failure_rate);
186 }
187
188 Ok(result)
189 }
190}
191
192impl Default for PatternAnalyzer {
193 fn default() -> Self {
194 Self::new()
195 }
196}
197
198#[derive(Debug, Clone)]
200pub struct FailurePattern {
201 error_message: String,
202 count: usize,
203 frequency: f64,
204}
205
206impl FailurePattern {
207 #[must_use]
209 pub const fn new(error_message: String, count: usize, frequency: f64) -> Self {
210 Self {
211 error_message,
212 count,
213 frequency,
214 }
215 }
216
217 #[must_use]
219 pub fn error_message(&self) -> &str {
220 &self.error_message
221 }
222
223 #[must_use]
225 pub const fn count(&self) -> usize {
226 self.count
227 }
228
229 #[must_use]
231 pub const fn frequency(&self) -> f64 {
232 self.frequency
233 }
234
235 #[must_use]
237 pub fn frequency_percentage(&self) -> f64 {
238 self.frequency * 100.0
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use chrono::Utc;
246 use std::path::PathBuf;
247
248 fn create_failed_history(error_msg: &str) -> BackupHistory {
249 let mut history =
250 BackupHistory::new(PathBuf::from("/tmp/backup"), 100, 1000, false, false, false);
251 history.error_message = Some(error_msg.to_string());
252 history
253 }
254
255 fn create_successful_history() -> BackupHistory {
256 BackupHistory::new(PathBuf::from("/tmp/backup"), 100, 1000, true, false, false)
257 }
258
259 #[test]
260 fn test_calculate_failure_rate() {
261 let analyzer = PatternAnalyzer::new();
262 let histories = vec![
263 create_successful_history(),
264 create_successful_history(),
265 create_failed_history("Error 1"),
266 create_successful_history(),
267 ];
268
269 let rate = analyzer.calculate_failure_rate(&histories).unwrap();
270 assert_eq!(rate.as_percentage(), 25.0);
271 }
272
273 #[test]
274 fn test_calculate_failure_rate_empty() {
275 let analyzer = PatternAnalyzer::new();
276 let histories = vec![];
277
278 assert!(analyzer.calculate_failure_rate(&histories).is_err());
279 }
280
281 #[test]
282 fn test_detect_failure_patterns() {
283 let analyzer = PatternAnalyzer::new();
284 let histories = vec![
285 create_failed_history("Permission denied"),
286 create_failed_history("Permission denied"),
287 create_failed_history("Permission denied"),
288 create_failed_history("Disk full"),
289 create_successful_history(),
290 ];
291
292 let patterns = analyzer.detect_failure_patterns(&histories).unwrap();
293 assert_eq!(patterns.len(), 1); assert_eq!(patterns[0].error_message(), "Permission denied");
295 assert_eq!(patterns[0].count(), 3);
296 }
297
298 #[test]
299 fn test_detect_failure_patterns_no_failures() {
300 let analyzer = PatternAnalyzer::new();
301 let histories = vec![create_successful_history(), create_successful_history()];
302
303 let patterns = analyzer.detect_failure_patterns(&histories).unwrap();
304 assert!(patterns.is_empty());
305 }
306
307 #[test]
308 fn test_calculate_failure_rate_by_category() {
309 let analyzer = PatternAnalyzer::new();
310
311 let mut h1 = create_successful_history();
312 h1.category = Some("documents".to_string());
313
314 let mut h2 = create_failed_history("Error");
315 h2.category = Some("documents".to_string());
316
317 let mut h3 = create_successful_history();
318 h3.category = Some("photos".to_string());
319
320 let histories = vec![h1, h2, h3];
321
322 let rates = analyzer
323 .calculate_failure_rate_by_category(&histories)
324 .unwrap();
325 assert_eq!(rates.get("documents").unwrap().as_percentage(), 50.0);
326 assert_eq!(rates.get("photos").unwrap().as_percentage(), 0.0);
327 }
328
329 #[test]
330 fn test_analyze_failure_by_hour() {
331 let analyzer = PatternAnalyzer::new();
332
333 let mut h1 = create_successful_history();
334 h1.timestamp = Utc::now().with_hour(10).unwrap();
335
336 let mut h2 = create_failed_history("Error");
337 h2.timestamp = Utc::now().with_hour(10).unwrap();
338
339 let mut h3 = create_successful_history();
340 h3.timestamp = Utc::now().with_hour(15).unwrap();
341
342 let histories = vec![h1, h2, h3];
343
344 let rates = analyzer.analyze_failure_by_hour(&histories).unwrap();
345 assert_eq!(rates.get(&10).unwrap().as_percentage(), 50.0);
346 assert_eq!(rates.get(&15).unwrap().as_percentage(), 0.0);
347 }
348}