syncable_cli/analyzer/k8s_optimize/
trend_analyzer.rs

1//! Trend Analyzer for Kubernetes Resource Waste
2//!
3//! Compares current waste metrics against historical data to identify trends.
4
5use super::live_analyzer::LiveRecommendation;
6use super::types::{TrendAnalysis, TrendDirection, WasteMetrics, WorkloadTrend};
7
8/// Analyze trends from live recommendations.
9/// Since we don't store historical data, this calculates current state
10/// and marks as "unknown" trend direction (no historical comparison available).
11pub fn analyze_trends_from_live(current_recs: &[LiveRecommendation]) -> TrendAnalysis {
12    // Calculate current waste metrics
13    let current = calculate_current_waste(current_recs);
14
15    // Calculate per-workload trends (current state)
16    let workload_trends = calculate_workload_trends(current_recs);
17
18    TrendAnalysis {
19        period: "current".to_string(),
20        current: current.clone(),
21        historical: WasteMetrics {
22            cpu_waste_millicores: 0,
23            memory_waste_bytes: 0,
24            waste_percentage: 0.0,
25            over_provisioned_count: 0,
26        },
27        trend: TrendDirection {
28            // Without historical data, we report current snapshot
29            direction: if current.over_provisioned_count > 5 {
30                "needs_attention"
31            } else if current.waste_percentage > 50.0 {
32                "high_waste"
33            } else if current.waste_percentage > 20.0 {
34                "moderate_waste"
35            } else {
36                "acceptable"
37            }
38            .to_string(),
39            change_percent: 0.0,
40        },
41        workload_trends,
42    }
43}
44
45/// Calculate waste metrics from current recommendations.
46fn calculate_current_waste(recs: &[LiveRecommendation]) -> WasteMetrics {
47    let mut total_cpu_waste: u64 = 0;
48    let mut total_mem_waste: u64 = 0;
49    let mut over_provisioned = 0;
50    let mut total_waste_pct = 0.0;
51
52    for rec in recs {
53        if rec.cpu_waste_pct > 0.0 || rec.memory_waste_pct > 0.0 {
54            over_provisioned += 1;
55
56            let cpu_waste = if rec.cpu_waste_pct > 0.0 {
57                let current = rec.current_cpu_millicores.unwrap_or(0);
58                current.saturating_sub(rec.recommended_cpu_millicores)
59            } else {
60                0
61            };
62
63            let mem_waste = if rec.memory_waste_pct > 0.0 {
64                let current = rec.current_memory_bytes.unwrap_or(0);
65                current.saturating_sub(rec.recommended_memory_bytes)
66            } else {
67                0
68            };
69
70            total_cpu_waste += cpu_waste;
71            total_mem_waste += mem_waste;
72            total_waste_pct += rec.cpu_waste_pct.max(rec.memory_waste_pct);
73        }
74    }
75
76    let avg_waste_pct = if over_provisioned > 0 {
77        total_waste_pct / over_provisioned as f32
78    } else {
79        0.0
80    };
81
82    WasteMetrics {
83        cpu_waste_millicores: total_cpu_waste,
84        memory_waste_bytes: total_mem_waste,
85        waste_percentage: avg_waste_pct,
86        over_provisioned_count: over_provisioned,
87    }
88}
89
90/// Calculate per-workload trends.
91fn calculate_workload_trends(recs: &[LiveRecommendation]) -> Vec<WorkloadTrend> {
92    recs.iter()
93        .filter(|rec| rec.cpu_waste_pct > 10.0 || rec.memory_waste_pct > 10.0)
94        .map(|rec| {
95            let cpu_change = if rec.cpu_waste_pct > 0.0 {
96                let current = rec.current_cpu_millicores.unwrap_or(0) as i64;
97                let recommended = rec.recommended_cpu_millicores as i64;
98                current - recommended
99            } else {
100                0
101            };
102
103            let mem_change = if rec.memory_waste_pct > 0.0 {
104                let current = rec.current_memory_bytes.unwrap_or(0) as i64;
105                let recommended = rec.recommended_memory_bytes as i64;
106                current - recommended
107            } else {
108                0
109            };
110
111            let direction = if cpu_change > 0 || mem_change > 0 {
112                "over-provisioned"
113            } else if cpu_change < 0 || mem_change < 0 {
114                "under-provisioned"
115            } else {
116                "optimal"
117            };
118
119            WorkloadTrend {
120                namespace: rec.namespace.clone(),
121                workload_name: rec.workload_name.clone(),
122                cpu_change_millicores: cpu_change,
123                memory_change_bytes: mem_change,
124                direction: direction.to_string(),
125            }
126        })
127        .collect()
128}
129
130/// Analyze trends from static recommendations (no Prometheus required).
131pub fn analyze_trends_static(
132    current_waste_pct: f32,
133    over_provisioned_count: usize,
134) -> TrendAnalysis {
135    // Without historical data, we can only report current state
136    TrendAnalysis {
137        period: "current".to_string(),
138        current: WasteMetrics {
139            cpu_waste_millicores: 0,
140            memory_waste_bytes: 0,
141            waste_percentage: current_waste_pct,
142            over_provisioned_count,
143        },
144        historical: WasteMetrics {
145            cpu_waste_millicores: 0,
146            memory_waste_bytes: 0,
147            waste_percentage: 0.0,
148            over_provisioned_count: 0,
149        },
150        trend: TrendDirection {
151            direction: if over_provisioned_count > 5 {
152                "needs_attention"
153            } else if current_waste_pct > 50.0 {
154                "high_waste"
155            } else if current_waste_pct > 20.0 {
156                "moderate_waste"
157            } else {
158                "acceptable"
159            }
160            .to_string(),
161            change_percent: 0.0,
162        },
163        workload_trends: vec![],
164    }
165}