1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
6use std::time::{Duration, SystemTime};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub enum TrendDirection {
11 Improving,
13 Degrading,
15 Stable,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct RuleMetrics {
22 pub rule_name: String,
24 pub total_evaluations: u64,
26 pub total_fires: u64,
28 pub total_successes: u64,
30 pub total_failures: u64,
32 pub total_execution_time: Duration,
34 pub min_execution_time: Duration,
36 pub max_execution_time: Duration,
38 pub estimated_memory_usage: usize,
40 pub last_executed: Option<SystemTime>,
42 pub recent_execution_times: Vec<Duration>,
44}
45
46impl RuleMetrics {
47 pub fn new(rule_name: String) -> Self {
49 Self {
50 rule_name,
51 total_evaluations: 0,
52 total_fires: 0,
53 total_successes: 0,
54 total_failures: 0,
55 total_execution_time: Duration::ZERO,
56 min_execution_time: Duration::MAX,
57 max_execution_time: Duration::ZERO,
58 estimated_memory_usage: 0,
59 last_executed: None,
60 recent_execution_times: Vec::new(),
61 }
62 }
63
64 pub fn record_execution(&mut self, duration: Duration, fired: bool, memory_usage: usize) {
66 self.total_evaluations += 1;
67 if fired {
68 self.total_fires += 1;
69 }
70 self.total_successes += 1;
71 self.total_execution_time += duration;
72
73 if duration < self.min_execution_time {
75 self.min_execution_time = duration;
76 }
77 if duration > self.max_execution_time {
78 self.max_execution_time = duration;
79 }
80
81 self.estimated_memory_usage = memory_usage;
82 self.last_executed = Some(SystemTime::now());
83
84 self.recent_execution_times.push(duration);
86 if self.recent_execution_times.len() > 100 {
87 self.recent_execution_times.remove(0);
88 }
89 }
90
91 pub fn record_failure(&mut self, duration: Duration) {
93 self.total_evaluations += 1;
94 self.total_failures += 1;
95 self.total_execution_time += duration;
96 self.last_executed = Some(SystemTime::now());
97 }
98
99 pub fn avg_execution_time(&self) -> Duration {
101 if self.total_evaluations > 0 {
102 self.total_execution_time / self.total_evaluations as u32
103 } else {
104 Duration::ZERO
105 }
106 }
107
108 pub fn success_rate(&self) -> f64 {
110 if self.total_evaluations > 0 {
111 (self.total_successes as f64 / self.total_evaluations as f64) * 100.0
112 } else {
113 0.0
114 }
115 }
116
117 pub fn fire_rate(&self) -> f64 {
119 if self.total_evaluations > 0 {
120 (self.total_fires as f64 / self.total_evaluations as f64) * 100.0
121 } else {
122 0.0
123 }
124 }
125
126 pub fn is_problematic(&self) -> bool {
128 self.success_rate() < 95.0
129 || self.avg_execution_time() > Duration::from_millis(50)
130 || self.total_failures > 10
131 }
132}
133
134#[derive(Debug, Clone)]
136pub struct AnalyticsConfig {
137 pub track_execution_time: bool,
139 pub track_memory_usage: bool,
141 pub track_success_rate: bool,
143 pub sampling_rate: f64,
145 pub retention_period: Duration,
147 pub max_recent_samples: usize,
149}
150
151impl Default for AnalyticsConfig {
152 fn default() -> Self {
153 Self {
154 track_execution_time: true,
155 track_memory_usage: true,
156 track_success_rate: true,
157 sampling_rate: 1.0,
158 retention_period: Duration::from_secs(7 * 24 * 60 * 60), max_recent_samples: 100,
160 }
161 }
162}
163
164impl AnalyticsConfig {
165 pub fn production() -> Self {
167 Self {
168 track_execution_time: true,
169 track_memory_usage: false, track_success_rate: true,
171 sampling_rate: 0.1, retention_period: Duration::from_secs(24 * 60 * 60), max_recent_samples: 50,
174 }
175 }
176
177 pub fn development() -> Self {
179 Self {
180 track_execution_time: true,
181 track_memory_usage: true,
182 track_success_rate: true,
183 sampling_rate: 1.0, retention_period: Duration::from_secs(60 * 60), max_recent_samples: 100,
186 }
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ExecutionEvent {
193 pub timestamp: SystemTime,
195 pub rule_name: String,
197 pub fired: bool,
199 pub duration: Duration,
201 pub success: bool,
203 pub error: Option<String>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct PerformanceTrend {
210 pub rule_name: String,
212 pub trend: TrendDirection,
214 pub change_percentage: f64,
216 pub period: Duration,
218}
219
220#[derive(Debug)]
222pub struct RuleAnalytics {
223 config: AnalyticsConfig,
225 rule_metrics: HashMap<String, RuleMetrics>,
227 execution_timeline: Vec<ExecutionEvent>,
229 start_time: SystemTime,
231 total_executions: u64,
233}
234
235impl RuleAnalytics {
236 pub fn new(config: AnalyticsConfig) -> Self {
238 Self {
239 config,
240 rule_metrics: HashMap::new(),
241 execution_timeline: Vec::new(),
242 start_time: SystemTime::now(),
243 total_executions: 0,
244 }
245 }
246
247 pub fn record_execution(
249 &mut self,
250 rule_name: &str,
251 duration: Duration,
252 fired: bool,
253 success: bool,
254 error: Option<String>,
255 memory_usage: usize,
256 ) {
257 if !self.should_sample() {
259 return;
260 }
261
262 self.total_executions += 1;
263
264 let metrics = self
266 .rule_metrics
267 .entry(rule_name.to_string())
268 .or_insert_with(|| RuleMetrics::new(rule_name.to_string()));
269
270 if success {
271 metrics.record_execution(duration, fired, memory_usage);
272 } else {
273 metrics.record_failure(duration);
274 }
275
276 self.execution_timeline.push(ExecutionEvent {
278 timestamp: SystemTime::now(),
279 rule_name: rule_name.to_string(),
280 fired,
281 duration,
282 success,
283 error,
284 });
285
286 self.cleanup_old_data();
288 }
289
290 pub fn get_rule_metrics(&self, rule_name: &str) -> Option<&RuleMetrics> {
292 self.rule_metrics.get(rule_name)
293 }
294
295 pub fn get_all_metrics(&self) -> &HashMap<String, RuleMetrics> {
297 &self.rule_metrics
298 }
299
300 pub fn slowest_rules(&self, limit: usize) -> Vec<&RuleMetrics> {
302 let mut rules: Vec<&RuleMetrics> = self.rule_metrics.values().collect();
303 rules.sort_by_key(|b| std::cmp::Reverse(b.avg_execution_time()));
304 rules.into_iter().take(limit).collect()
305 }
306
307 pub fn most_fired_rules(&self, limit: usize) -> Vec<&RuleMetrics> {
309 let mut rules: Vec<&RuleMetrics> = self.rule_metrics.values().collect();
310 rules.sort_by(|a, b| b.total_fires.cmp(&a.total_fires));
311 rules.into_iter().take(limit).collect()
312 }
313
314 pub fn problematic_rules(&self) -> Vec<&RuleMetrics> {
316 self.rule_metrics
317 .values()
318 .filter(|metrics| metrics.is_problematic())
319 .collect()
320 }
321
322 pub fn overall_stats(&self) -> OverallStats {
324 let total_time: Duration = self
325 .rule_metrics
326 .values()
327 .map(|m| m.total_execution_time)
328 .sum();
329
330 let total_evaluations: u64 = self
331 .rule_metrics
332 .values()
333 .map(|m| m.total_evaluations)
334 .sum();
335
336 let total_fires: u64 = self.rule_metrics.values().map(|m| m.total_fires).sum();
337
338 let total_successes: u64 = self.rule_metrics.values().map(|m| m.total_successes).sum();
339
340 let avg_execution_time = if total_evaluations > 0 {
341 total_time / total_evaluations as u32
342 } else {
343 Duration::ZERO
344 };
345
346 let rules_per_second = if total_time.as_secs_f64() > 0.0 {
347 total_evaluations as f64 / total_time.as_secs_f64()
348 } else {
349 0.0
350 };
351
352 let success_rate = if total_evaluations > 0 {
353 (total_successes as f64 / total_evaluations as f64) * 100.0
354 } else {
355 0.0
356 };
357
358 OverallStats {
359 total_rules: self.rule_metrics.len(),
360 total_evaluations,
361 total_fires,
362 total_successes,
363 avg_execution_time,
364 rules_per_second,
365 success_rate,
366 uptime: self.start_time.elapsed().unwrap_or(Duration::ZERO),
367 }
368 }
369
370 fn should_sample(&self) -> bool {
372 if self.config.sampling_rate >= 1.0 {
373 return true;
374 }
375
376 use std::collections::hash_map::DefaultHasher;
377 use std::hash::{Hash, Hasher};
378
379 let mut hasher = DefaultHasher::new();
380 self.total_executions.hash(&mut hasher);
381 let hash = hasher.finish();
382
383 (hash as f64 / u64::MAX as f64) < self.config.sampling_rate
384 }
385
386 fn cleanup_old_data(&mut self) {
388 let cutoff = SystemTime::now()
389 .checked_sub(self.config.retention_period)
390 .unwrap_or(SystemTime::UNIX_EPOCH);
391
392 self.execution_timeline
394 .retain(|event| event.timestamp >= cutoff);
395 }
396
397 pub fn config(&self) -> &AnalyticsConfig {
399 &self.config
400 }
401
402 pub fn get_all_rule_metrics(&self) -> &HashMap<String, RuleMetrics> {
404 &self.rule_metrics
405 }
406
407 pub fn generate_recommendations(&self) -> Vec<String> {
409 let mut recommendations = Vec::new();
410
411 for (rule_name, metrics) in &self.rule_metrics {
412 if metrics.avg_execution_time().as_millis() > 100 {
414 recommendations.push(format!(
415 "Consider optimizing '{}' - average execution time is {:.2}ms",
416 rule_name,
417 metrics.avg_execution_time().as_secs_f64() * 1000.0
418 ));
419 }
420
421 if metrics.success_rate() < 50.0 && metrics.total_evaluations > 10 {
423 recommendations.push(format!(
424 "Rule '{}' has low success rate ({:.1}%) - review conditions",
425 rule_name,
426 metrics.success_rate()
427 ));
428 }
429
430 if metrics.total_fires == 0 && metrics.total_evaluations > 20 {
432 recommendations.push(format!(
433 "Rule '{}' never fires despite {} evaluations - review logic",
434 rule_name, metrics.total_evaluations
435 ));
436 }
437 }
438
439 recommendations
440 }
441
442 pub fn get_recent_events(&self, limit: usize) -> Vec<&ExecutionEvent> {
444 self.execution_timeline.iter().rev().take(limit).collect()
445 }
446
447 pub fn get_overall_stats(&self) -> OverallStats {
449 self.overall_stats()
450 }
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct OverallStats {
456 pub total_rules: usize,
458 pub total_evaluations: u64,
460 pub total_fires: u64,
462 pub total_successes: u64,
464 pub avg_execution_time: Duration,
466 pub rules_per_second: f64,
468 pub success_rate: f64,
470 pub uptime: Duration,
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_rule_metrics_creation() {
480 let metrics = RuleMetrics::new("TestRule".to_string());
481 assert_eq!(metrics.rule_name, "TestRule");
482 assert_eq!(metrics.total_evaluations, 0);
483 assert_eq!(metrics.success_rate(), 0.0);
484 }
485
486 #[test]
487 fn test_rule_metrics_recording() {
488 let mut metrics = RuleMetrics::new("TestRule".to_string());
489
490 metrics.record_execution(Duration::from_millis(10), true, 1024);
492
493 assert_eq!(metrics.total_evaluations, 1);
494 assert_eq!(metrics.total_fires, 1);
495 assert_eq!(metrics.total_successes, 1);
496 assert_eq!(metrics.success_rate(), 100.0);
497 assert_eq!(metrics.fire_rate(), 100.0);
498 }
499
500 #[test]
501 fn test_analytics_config() {
502 let config = AnalyticsConfig::production();
503 assert!(config.sampling_rate < 1.0);
504 assert!(!config.track_memory_usage);
505
506 let dev_config = AnalyticsConfig::development();
507 assert_eq!(dev_config.sampling_rate, 1.0);
508 assert!(dev_config.track_memory_usage);
509 }
510
511 #[test]
512 fn test_analytics_recording() {
513 let config = AnalyticsConfig::development();
514 let mut analytics = RuleAnalytics::new(config);
515
516 analytics.record_execution("TestRule", Duration::from_millis(5), true, true, None, 1024);
517
518 assert_eq!(analytics.total_executions, 1);
519 assert!(analytics.get_rule_metrics("TestRule").is_some());
520 }
521}