backup_suite/smart/anomaly/
pattern.rs

1//! 失敗パターン分析エンジン
2//!
3//! バックアップ失敗の頻発パターンを検出します。
4
5use 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/// パターン分析器
12///
13/// バックアップ失敗のパターンを分析します。
14///
15/// # 使用例
16///
17/// ```rust,no_run
18/// use backup_suite::smart::anomaly::PatternAnalyzer;
19/// use backup_suite::BackupHistory;
20///
21/// let analyzer = PatternAnalyzer::new();
22/// let histories = BackupHistory::load_all().unwrap();
23///
24/// let failure_rate = analyzer.calculate_failure_rate(&histories).unwrap();
25/// println!("失敗率: {:.1}%", failure_rate.as_percentage());
26///
27/// let patterns = analyzer.detect_failure_patterns(&histories).unwrap();
28/// for pattern in patterns {
29///     println!("頻発エラー: {} ({}回)", pattern.error_message(), pattern.count());
30/// }
31/// ```
32#[derive(Debug, Clone)]
33pub struct PatternAnalyzer {
34    min_occurrences: usize,
35}
36
37impl PatternAnalyzer {
38    /// 新しいパターン分析器を作成
39    ///
40    /// # 引数
41    ///
42    /// * `min_occurrences` - パターンとして認識する最低発生回数
43    #[must_use]
44    pub const fn new() -> Self {
45        Self { min_occurrences: 3 }
46    }
47
48    /// 失敗率を計算
49    ///
50    /// # Errors
51    ///
52    /// データが不足している場合はエラーを返します。
53    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    /// 失敗パターンを検出
72    ///
73    /// # Errors
74    ///
75    /// データ処理に失敗した場合はエラーを返します。
76    pub fn detect_failure_patterns(
77        &self,
78        histories: &[BackupHistory],
79    ) -> SmartResult<Vec<FailurePattern>> {
80        // 失敗したバックアップのみを対象
81        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        // エラーメッセージごとにカウント
91        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        // 頻発するパターンを抽出
100        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        // 発生回数順にソート
110        patterns.sort_by(|a, b| b.count.cmp(&a.count));
111
112        Ok(patterns)
113    }
114
115    /// カテゴリ別の失敗率を計算
116    ///
117    /// # Errors
118    ///
119    /// データ処理に失敗した場合はエラーを返します。
120    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        // カテゴリごとにグループ化
129        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        // 失敗率を計算
145        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    /// 時間帯別の失敗率を分析
156    ///
157    /// # Errors
158    ///
159    /// データ処理に失敗した場合はエラーを返します。
160    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        // 時間帯ごとにグループ化(0-23時)
169        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        // 失敗率を計算
181        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/// 失敗パターン
199#[derive(Debug, Clone)]
200pub struct FailurePattern {
201    error_message: String,
202    count: usize,
203    frequency: f64,
204}
205
206impl FailurePattern {
207    /// 新しい失敗パターンを作成
208    #[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    /// エラーメッセージを取得
218    #[must_use]
219    pub fn error_message(&self) -> &str {
220        &self.error_message
221    }
222
223    /// 発生回数を取得
224    #[must_use]
225    pub const fn count(&self) -> usize {
226        self.count
227    }
228
229    /// 発生頻度を取得(0.0-1.0)
230    #[must_use]
231    pub const fn frequency(&self) -> f64 {
232        self.frequency
233    }
234
235    /// 頻度をパーセンテージで取得
236    #[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); // "Permission denied" のみが3回以上
294        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}