use super::monitor::{MetricTrend, MonitorEngine};
use super::types::{Finding, FindingKind};
use std::collections::HashMap;
pub struct AnalyzeEngine {
findings: Vec<Finding>,
slo_config: SLOConfig,
}
#[derive(Debug, Clone)]
pub struct SLOConfig {
pub max_ticks_p99: f64,
pub max_guard_failure_rate: f64,
pub max_avg_latency_ms: f64,
pub slo_increase_threshold: f64,
}
impl Default for SLOConfig {
fn default() -> Self {
Self {
max_ticks_p99: 8.0, max_guard_failure_rate: 1.0,
max_avg_latency_ms: 100.0,
slo_increase_threshold: 10.0,
}
}
}
impl AnalyzeEngine {
pub fn new(slo_config: SLOConfig) -> Self {
Self {
findings: Vec::new(),
slo_config,
}
}
pub fn analyze(&mut self, monitor: &MonitorEngine) -> Vec<Finding> {
self.findings.clear();
self.detect_tick_budget_violations(monitor);
self.detect_guard_failures(monitor);
self.detect_slo_breaches(monitor);
self.detect_drift(monitor);
self.detect_optimization_opportunities(monitor);
self.findings.clone()
}
fn detect_tick_budget_violations(&mut self, monitor: &MonitorEngine) {
for (metric_name, agg) in monitor.aggregations() {
if metric_name.contains("pattern")
&& metric_name.contains("ticks")
&& agg.p99 > self.slo_config.max_ticks_p99
{
let pattern_name = metric_name
.strip_prefix("pattern.")
.and_then(|s| s.strip_suffix(".ticks"))
.unwrap_or(metric_name)
.to_string();
let finding = Finding {
id: format!("finding-tick-{}", pattern_name),
kind: FindingKind::TickBudgetViolation,
severity: "High".to_string(),
description: format!(
"Pattern '{}' exceeds tick budget: P99 = {} ticks (max = {})",
pattern_name, agg.p99, self.slo_config.max_ticks_p99
),
component: pattern_name,
evidence: vec![metric_name.clone()],
suggested_action: Some(
"Consider breaking pattern into smaller units or optimizing hot path"
.to_string(),
),
timestamp: get_timestamp(),
metadata: HashMap::new(),
};
self.findings.push(finding);
}
}
}
fn detect_guard_failures(&mut self, monitor: &MonitorEngine) {
for (metric_name, agg) in monitor.aggregations() {
if metric_name.contains("guard")
&& metric_name.contains("failure_rate")
&& agg.avg > self.slo_config.max_guard_failure_rate
{
let guard_id = metric_name
.strip_prefix("guard.")
.and_then(|s| s.strip_suffix(".failure_rate"))
.unwrap_or(metric_name)
.to_string();
let finding = Finding {
id: format!("finding-guard-{}", guard_id),
kind: FindingKind::GuardFailureRate,
severity: if agg.avg > 5.0 { "Critical" } else { "High" }.to_string(),
description: format!(
"Guard '{}' has high failure rate: {:.2}% (threshold = {}%)",
guard_id, agg.avg, self.slo_config.max_guard_failure_rate
),
component: guard_id,
evidence: vec![metric_name.clone()],
suggested_action: Some(
"Analyze failures, consider relaxing guard or fixing root cause"
.to_string(),
),
timestamp: get_timestamp(),
metadata: HashMap::new(),
};
self.findings.push(finding);
}
}
}
fn detect_slo_breaches(&mut self, monitor: &MonitorEngine) {
for (metric_name, agg) in monitor.aggregations() {
if metric_name.contains("latency") && agg.avg > self.slo_config.max_avg_latency_ms {
let finding = Finding {
id: format!("finding-slo-{}", metric_name),
kind: FindingKind::SLOBreach,
severity: "High".to_string(),
description: format!(
"Latency SLO breach: {} (avg={:.1}ms, threshold={}ms)",
metric_name, agg.avg, self.slo_config.max_avg_latency_ms
),
component: metric_name.clone(),
evidence: vec![metric_name.clone()],
suggested_action: Some(
"Investigate latency causes, may require query optimization or caching"
.to_string(),
),
timestamp: get_timestamp(),
metadata: HashMap::new(),
};
self.findings.push(finding);
}
}
}
fn detect_drift(&mut self, monitor: &MonitorEngine) {
for (metric_name, agg) in monitor.aggregations() {
if agg.trend == MetricTrend::Increasing {
if agg.p99 > agg.min * (1.0 + self.slo_config.slo_increase_threshold / 100.0) {
let finding = Finding {
id: format!("finding-drift-{}", metric_name),
kind: FindingKind::DriftDetected,
severity: "Medium".to_string(),
description: format!(
"Metric '{}' shows increasing trend: min={:.1}, p99={:.1}",
metric_name, agg.min, agg.p99
),
component: metric_name.clone(),
evidence: vec![metric_name.clone()],
suggested_action: Some(
"Investigate why metric is increasing over time; may indicate resource leak or workload growth"
.to_string(),
),
timestamp: get_timestamp(),
metadata: HashMap::new(),
};
self.findings.push(finding);
}
}
}
}
fn detect_optimization_opportunities(&mut self, monitor: &MonitorEngine) {
for (metric_name, agg) in monitor.aggregations() {
if metric_name.contains("pattern")
&& metric_name.contains("ticks")
&& agg.p99 < 2.0
&& agg.count >= 10
{
let pattern_name = metric_name
.strip_prefix("pattern.")
.and_then(|s| s.strip_suffix(".ticks"))
.unwrap_or(metric_name)
.to_string();
let finding = Finding {
id: format!("finding-opt-{}", pattern_name),
kind: FindingKind::OptimizationOpportunity,
severity: "Low".to_string(),
description: format!(
"Pattern '{}' consistently uses few ticks (P99={:.1}): opportunity to merge with hot path",
pattern_name, agg.p99
),
component: pattern_name,
evidence: vec![metric_name.clone()],
suggested_action: Some(
"Consider inlining or merging this pattern into hot path"
.to_string(),
),
timestamp: get_timestamp(),
metadata: HashMap::new(),
};
self.findings.push(finding);
}
}
}
pub fn findings(&self) -> &[Finding] {
&self.findings
}
pub fn findings_by_kind(&self, kind: FindingKind) -> Vec<&Finding> {
self.findings.iter().filter(|f| f.kind == kind).collect()
}
pub fn critical_findings(&self) -> Vec<&Finding> {
self.findings
.iter()
.filter(|f| f.severity == "Critical" || f.severity == "High")
.collect()
}
}
fn get_timestamp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mape_k::monitor::MonitorEngine;
#[test]
fn test_tick_budget_finding() {
let mut monitor = MonitorEngine::new();
let obs = crate::mape_k::types::Observation {
id: "obs-1".to_string(),
obs_type: crate::mape_k::types::ObservationType::Event,
timestamp: 1000,
data: serde_json::json!({
"pattern": "expensive_pattern",
"ticks": 12.0
}),
source: "test".to_string(),
};
monitor.ingest_observation(obs);
monitor.run_aggregations();
let mut analyzer = AnalyzeEngine::new(SLOConfig::default());
let findings = analyzer.analyze(&monitor);
let violations = findings
.iter()
.filter(|f| f.kind == FindingKind::TickBudgetViolation)
.collect::<Vec<_>>();
assert!(!violations.is_empty());
}
#[test]
fn test_finding_severity() {
let mut analyzer = AnalyzeEngine::new(SLOConfig::default());
let finding = Finding {
id: "test".to_string(),
kind: FindingKind::SLOBreach,
severity: "Critical".to_string(),
description: "Test".to_string(),
component: "test".to_string(),
evidence: vec![],
suggested_action: None,
timestamp: 0,
metadata: HashMap::new(),
};
analyzer.findings.push(finding);
let critical = analyzer.critical_findings();
assert_eq!(critical.len(), 1);
}
}