syncable_cli/analyzer/k8s_optimize/
cost_calculator.rs1use super::live_analyzer::LiveRecommendation;
7use super::types::{
8 CloudProvider, CostBreakdown, CostEstimation, ResourceRecommendation, WorkloadCost,
9};
10
11const AWS_CPU_HOURLY: f64 = 0.0416; const GCP_CPU_HOURLY: f64 = 0.0335; const AZURE_CPU_HOURLY: f64 = 0.0400; const ONPREM_CPU_HOURLY: f64 = 0.0250; const AWS_MEM_HOURLY: f64 = 0.0052; const GCP_MEM_HOURLY: f64 = 0.0045; const AZURE_MEM_HOURLY: f64 = 0.0050; const ONPREM_MEM_HOURLY: f64 = 0.0030; const HOURS_PER_MONTH: f64 = 730.0;
25
26pub fn calculate_from_live(
28 recommendations: &[LiveRecommendation],
29 provider: CloudProvider,
30 region: &str,
31) -> CostEstimation {
32 let (cpu_hourly, mem_hourly) = get_pricing(&provider);
33
34 let mut total_cpu_waste_millicores: u64 = 0;
35 let mut total_memory_waste_bytes: u64 = 0;
36 let mut workload_costs: Vec<WorkloadCost> = Vec::new();
37
38 for rec in recommendations {
39 let cpu_waste = if rec.cpu_waste_pct > 0.0 {
41 let current = rec.current_cpu_millicores.unwrap_or(0);
43 current.saturating_sub(rec.recommended_cpu_millicores)
44 } else {
45 0
46 };
47
48 let memory_waste = if rec.memory_waste_pct > 0.0 {
49 let current = rec.current_memory_bytes.unwrap_or(0);
50 current.saturating_sub(rec.recommended_memory_bytes)
51 } else {
52 0
53 };
54
55 total_cpu_waste_millicores += cpu_waste;
56 total_memory_waste_bytes += memory_waste;
57
58 let cpu_cores = cpu_waste as f64 / 1000.0;
60 let memory_gb = memory_waste as f64 / (1024.0 * 1024.0 * 1024.0);
61
62 let monthly_cost =
63 (cpu_cores * cpu_hourly * HOURS_PER_MONTH) + (memory_gb * mem_hourly * HOURS_PER_MONTH);
64
65 if monthly_cost > 0.01 {
66 workload_costs.push(WorkloadCost {
67 namespace: rec.namespace.clone(),
68 workload_name: rec.workload_name.clone(),
69 monthly_cost: round_cost(monthly_cost),
70 monthly_savings: round_cost(monthly_cost),
71 });
72 }
73 }
74
75 let cpu_cores = total_cpu_waste_millicores as f64 / 1000.0;
77 let memory_gb = total_memory_waste_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
78
79 let cpu_monthly = cpu_cores * cpu_hourly * HOURS_PER_MONTH;
80 let mem_monthly = memory_gb * mem_hourly * HOURS_PER_MONTH;
81 let monthly_waste = cpu_monthly + mem_monthly;
82
83 workload_costs.sort_by(|a, b| {
85 b.monthly_cost
86 .partial_cmp(&a.monthly_cost)
87 .unwrap_or(std::cmp::Ordering::Equal)
88 });
89
90 CostEstimation {
91 provider,
92 region: region.to_string(),
93 monthly_waste_cost: round_cost(monthly_waste),
94 annual_waste_cost: round_cost(monthly_waste * 12.0),
95 monthly_savings: round_cost(monthly_waste),
96 annual_savings: round_cost(monthly_waste * 12.0),
97 currency: "USD".to_string(),
98 breakdown: CostBreakdown {
99 cpu_cost: round_cost(cpu_monthly),
100 memory_cost: round_cost(mem_monthly),
101 },
102 workload_costs,
103 }
104}
105
106pub fn calculate_from_static(
108 recommendations: &[ResourceRecommendation],
109 provider: CloudProvider,
110 region: &str,
111) -> CostEstimation {
112 let (cpu_hourly, mem_hourly) = get_pricing(&provider);
113
114 let mut total_cpu_waste_millicores: u64 = 0;
115 let mut total_memory_waste_bytes: u64 = 0;
116 let mut workload_costs: Vec<WorkloadCost> = Vec::new();
117
118 for rec in recommendations {
119 let cpu_waste = parse_cpu_to_millicores(&rec.current.cpu_request)
121 .saturating_sub(parse_cpu_to_millicores(&rec.recommended.cpu_request));
122
123 let memory_waste = parse_memory_to_bytes(&rec.current.memory_request)
124 .saturating_sub(parse_memory_to_bytes(&rec.recommended.memory_request));
125
126 total_cpu_waste_millicores += cpu_waste;
127 total_memory_waste_bytes += memory_waste;
128
129 let cpu_cores = cpu_waste as f64 / 1000.0;
130 let memory_gb = memory_waste as f64 / (1024.0 * 1024.0 * 1024.0);
131
132 let monthly_cost =
133 (cpu_cores * cpu_hourly * HOURS_PER_MONTH) + (memory_gb * mem_hourly * HOURS_PER_MONTH);
134
135 if monthly_cost > 0.01 {
136 workload_costs.push(WorkloadCost {
137 namespace: rec
138 .namespace
139 .clone()
140 .unwrap_or_else(|| "default".to_string()),
141 workload_name: rec.resource_name.clone(),
142 monthly_cost: round_cost(monthly_cost),
143 monthly_savings: round_cost(monthly_cost),
144 });
145 }
146 }
147
148 let cpu_cores = total_cpu_waste_millicores as f64 / 1000.0;
149 let memory_gb = total_memory_waste_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
150
151 let cpu_monthly = cpu_cores * cpu_hourly * HOURS_PER_MONTH;
152 let mem_monthly = memory_gb * mem_hourly * HOURS_PER_MONTH;
153 let monthly_waste = cpu_monthly + mem_monthly;
154
155 workload_costs.sort_by(|a, b| {
156 b.monthly_cost
157 .partial_cmp(&a.monthly_cost)
158 .unwrap_or(std::cmp::Ordering::Equal)
159 });
160
161 CostEstimation {
162 provider,
163 region: region.to_string(),
164 monthly_waste_cost: round_cost(monthly_waste),
165 annual_waste_cost: round_cost(monthly_waste * 12.0),
166 monthly_savings: round_cost(monthly_waste),
167 annual_savings: round_cost(monthly_waste * 12.0),
168 currency: "USD".to_string(),
169 breakdown: CostBreakdown {
170 cpu_cost: round_cost(cpu_monthly),
171 memory_cost: round_cost(mem_monthly),
172 },
173 workload_costs,
174 }
175}
176
177fn get_pricing(provider: &CloudProvider) -> (f64, f64) {
179 match provider {
180 CloudProvider::Aws => (AWS_CPU_HOURLY, AWS_MEM_HOURLY),
181 CloudProvider::Gcp => (GCP_CPU_HOURLY, GCP_MEM_HOURLY),
182 CloudProvider::Azure => (AZURE_CPU_HOURLY, AZURE_MEM_HOURLY),
183 CloudProvider::OnPrem => (ONPREM_CPU_HOURLY, ONPREM_MEM_HOURLY),
184 CloudProvider::Unknown => (AWS_CPU_HOURLY, AWS_MEM_HOURLY), }
186}
187
188fn parse_cpu_to_millicores(cpu: &Option<String>) -> u64 {
190 let cpu_str = match cpu {
191 Some(s) => s,
192 None => return 0,
193 };
194
195 if cpu_str.ends_with('m') {
196 cpu_str.trim_end_matches('m').parse().unwrap_or(0)
197 } else {
198 let cores: f64 = cpu_str.parse().unwrap_or(0.0);
200 (cores * 1000.0) as u64
201 }
202}
203
204fn parse_memory_to_bytes(memory: &Option<String>) -> u64 {
206 let mem_str = match memory {
207 Some(s) => s,
208 None => return 0,
209 };
210
211 let mem_str = mem_str.trim();
212
213 if mem_str.ends_with("Gi") {
214 let val: f64 = mem_str.trim_end_matches("Gi").parse().unwrap_or(0.0);
215 (val * 1024.0 * 1024.0 * 1024.0) as u64
216 } else if mem_str.ends_with("Mi") {
217 let val: f64 = mem_str.trim_end_matches("Mi").parse().unwrap_or(0.0);
218 (val * 1024.0 * 1024.0) as u64
219 } else if mem_str.ends_with("Ki") {
220 let val: f64 = mem_str.trim_end_matches("Ki").parse().unwrap_or(0.0);
221 (val * 1024.0) as u64
222 } else {
223 mem_str.parse().unwrap_or(0)
225 }
226}
227
228fn round_cost(cost: f64) -> f64 {
230 (cost * 100.0).round() / 100.0
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_parse_cpu() {
239 assert_eq!(parse_cpu_to_millicores(&Some("100m".to_string())), 100);
240 assert_eq!(parse_cpu_to_millicores(&Some("1".to_string())), 1000);
241 assert_eq!(parse_cpu_to_millicores(&Some("1.5".to_string())), 1500);
242 assert_eq!(parse_cpu_to_millicores(&None), 0);
243 }
244
245 #[test]
246 fn test_parse_memory() {
247 assert_eq!(
248 parse_memory_to_bytes(&Some("128Mi".to_string())),
249 128 * 1024 * 1024
250 );
251 assert_eq!(
252 parse_memory_to_bytes(&Some("1Gi".to_string())),
253 1024 * 1024 * 1024
254 );
255 assert_eq!(parse_memory_to_bytes(&None), 0);
256 }
257
258 #[test]
259 fn test_round_cost() {
260 assert_eq!(round_cost(10.1234), 10.12);
261 assert_eq!(round_cost(10.125), 10.13);
262 }
263}