syncable_cli/analyzer/k8s_optimize/formatter/
output.rs1use crate::analyzer::k8s_optimize::types::{OptimizationResult, Severity};
6use colored::Colorize;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum OutputFormat {
17 #[default]
19 Table,
20 Json,
22 Yaml,
24 Summary,
26}
27
28impl OutputFormat {
29 pub fn parse(s: &str) -> Option<Self> {
31 match s.to_lowercase().as_str() {
32 "table" => Some(Self::Table),
33 "json" => Some(Self::Json),
34 "yaml" => Some(Self::Yaml),
35 "summary" => Some(Self::Summary),
36 _ => None,
37 }
38 }
39}
40
41pub fn format_result_to_string(result: &OptimizationResult, format: OutputFormat) -> String {
47 match format {
48 OutputFormat::Table => format_table(result),
49 OutputFormat::Json => format_json(result),
50 OutputFormat::Yaml => format_yaml(result),
51 OutputFormat::Summary => format_summary(result),
52 }
53}
54
55pub fn format_result(result: &OptimizationResult, format: OutputFormat) {
57 println!("{}", format_result_to_string(result, format));
58}
59
60fn format_table(result: &OptimizationResult) -> String {
65 let mut output = String::new();
66
67 output.push_str(&format!(
69 "\n{}\n",
70 "═══════════════════════════════════════════════════════════════════════════════════════════════════"
71 .bright_blue()
72 ));
73 output.push_str(&format!(
74 "{}\n",
75 "💰 KUBERNETES RESOURCE OPTIMIZATION REPORT"
76 .bright_white()
77 .bold()
78 ));
79 output.push_str(&format!(
80 "{}\n\n",
81 "═══════════════════════════════════════════════════════════════════════════════════════════════════"
82 .bright_blue()
83 ));
84
85 output.push_str(&format_summary_section(result));
87
88 if result.has_recommendations() {
90 output.push_str(&format!(
91 "\n{}\n",
92 "┌─ Recommendations ─────────────────────────────────────────────────────────────────────────────┐"
93 .bright_blue()
94 ));
95
96 for (i, rec) in result.recommendations.iter().enumerate() {
97 let severity_icon = match rec.severity {
98 Severity::Critical => "🔴",
99 Severity::High => "🟠",
100 Severity::Medium => "🟡",
101 Severity::Low => "🟢",
102 Severity::Info => "ℹ️ ",
103 };
104
105 let severity_str = match rec.severity {
106 Severity::Critical => rec.severity.as_str().bright_red(),
107 Severity::High => rec.severity.as_str().red(),
108 Severity::Medium => rec.severity.as_str().yellow(),
109 Severity::Low => rec.severity.as_str().green(),
110 Severity::Info => rec.severity.as_str().blue(),
111 };
112
113 output.push_str(&format!(
114 "│\n│ {} {} {} {}\n",
115 severity_icon,
116 format!("[{}]", rec.rule_code).bright_cyan(),
117 severity_str.bold(),
118 rec.resource_identifier().bright_white()
119 ));
120
121 output.push_str(&format!(
122 "│ {} {} / {}\n",
123 "Resource:".dimmed(),
124 rec.resource_kind.cyan(),
125 rec.container.yellow()
126 ));
127
128 output.push_str(&format!("│ {} {}\n", "Issue:".dimmed(), rec.message));
129
130 if rec.current.has_any() || rec.recommended.has_any() {
132 output.push_str(&format!("│ {}\n", "Current:".dimmed()));
133 if let Some(cpu) = &rec.current.cpu_request {
134 output.push_str(&format!("│ CPU request: {}\n", cpu.red()));
135 }
136 if let Some(mem) = &rec.current.memory_request {
137 output.push_str(&format!("│ Memory request: {}\n", mem.red()));
138 }
139
140 output.push_str(&format!("│ {}\n", "Recommended:".dimmed()));
141 if let Some(cpu) = &rec.recommended.cpu_request {
142 output.push_str(&format!("│ CPU request: {}\n", cpu.green()));
143 }
144 if let Some(mem) = &rec.recommended.memory_request {
145 output.push_str(&format!("│ Memory request: {}\n", mem.green()));
146 }
147 }
148
149 if i < result.recommendations.len() - 1 {
150 output.push_str(&format!(
151 "│{}",
152 "────────────────────────────────────────────────────────────────────────────────────────────\n"
153 .dimmed()
154 ));
155 }
156 }
157
158 output.push_str(&format!(
159 "{}\n",
160 "└────────────────────────────────────────────────────────────────────────────────────────────────┘"
161 .bright_blue()
162 ));
163 } else {
164 output.push_str(&format!(
165 "\n{}\n",
166 "✅ No optimization issues found! Your resources look well-configured.".green()
167 ));
168 }
169
170 output.push_str(&format!(
172 "\n{}\n",
173 "═══════════════════════════════════════════════════════════════════════════════════════════════════"
174 .bright_blue()
175 ));
176
177 output
178}
179
180fn format_summary_section(result: &OptimizationResult) -> String {
181 let mut output = String::new();
182
183 output.push_str(&format!(
184 "{}",
185 "┌─ Summary ─────────────────────────────────────────────────────────────────────────────────────────┐\n"
186 .bright_blue()
187 ));
188
189 output.push_str(&format!(
190 "│ {} {:>6} {} {:>6} {} {:>6}\n",
191 "Resources:".dimmed(),
192 result.summary.resources_analyzed.to_string().bright_white(),
193 "Containers:".dimmed(),
194 result
195 .summary
196 .containers_analyzed
197 .to_string()
198 .bright_white(),
199 "Mode:".dimmed(),
200 result.metadata.mode.to_string().cyan(),
201 ));
202
203 output.push_str(&format!(
204 "│ {} {:>6} {} {:>6} {} {:>6}\n",
205 "Over-provisioned:".dimmed(),
206 if result.summary.over_provisioned > 0 {
207 result.summary.over_provisioned.to_string().red()
208 } else {
209 result.summary.over_provisioned.to_string().green()
210 },
211 "Missing requests:".dimmed(),
212 if result.summary.missing_requests > 0 {
213 result.summary.missing_requests.to_string().yellow()
214 } else {
215 result.summary.missing_requests.to_string().green()
216 },
217 "Optimal:".dimmed(),
218 result.summary.optimal.to_string().green(),
219 ));
220
221 if result.summary.total_waste_percentage > 0.0 {
222 output.push_str(&format!(
223 "│ {} {:.1}%\n",
224 "Estimated waste:".dimmed(),
225 result.summary.total_waste_percentage.to_string().red(),
226 ));
227 }
228
229 if let Some(savings) = result.summary.estimated_monthly_savings_usd {
230 output.push_str(&format!(
231 "│ {} ${:.2}/month\n",
232 "Potential savings:".dimmed(),
233 savings.to_string().green(),
234 ));
235 }
236
237 output.push_str(&format!(
238 "│ {} {}ms {} {}\n",
239 "Duration:".dimmed(),
240 result.metadata.duration_ms.to_string().dimmed(),
241 "Path:".dimmed(),
242 result.metadata.path.display().to_string().dimmed(),
243 ));
244
245 output.push_str(&format!(
246 "{}",
247 "└───────────────────────────────────────────────────────────────────────────────────────────────────┘\n"
248 .bright_blue()
249 ));
250
251 output
252}
253
254fn format_json(result: &OptimizationResult) -> String {
259 serde_json::to_string_pretty(result).unwrap_or_else(|_| "{}".to_string())
260}
261
262fn format_yaml(result: &OptimizationResult) -> String {
267 serde_yaml::to_string(result).unwrap_or_else(|_| "".to_string())
268}
269
270fn format_summary(result: &OptimizationResult) -> String {
275 let mut output = String::new();
276
277 output.push_str("▶ RESOURCE OPTIMIZATION SUMMARY\n");
278 output.push_str("──────────────────────────────────────────────────\n");
279 output.push_str(&format!(
280 "│ Resources: {} ({})\n",
281 result.summary.resources_analyzed, result.metadata.mode
282 ));
283 output.push_str(&format!(
284 "│ Containers: {}\n",
285 result.summary.containers_analyzed
286 ));
287 output.push_str(&format!(
288 "│ Issues: {} over-provisioned, {} missing requests\n",
289 result.summary.over_provisioned, result.summary.missing_requests
290 ));
291 output.push_str(&format!("│ Optimal: {}\n", result.summary.optimal));
292 output.push_str(&format!(
293 "│ Analysis Time: {}ms\n",
294 result.metadata.duration_ms
295 ));
296 output.push_str("──────────────────────────────────────────────────\n");
297
298 output
299}
300
301#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::analyzer::k8s_optimize::types::AnalysisMode;
309 use std::path::PathBuf;
310
311 #[test]
312 fn test_output_format_parse() {
313 assert_eq!(OutputFormat::parse("table"), Some(OutputFormat::Table));
314 assert_eq!(OutputFormat::parse("JSON"), Some(OutputFormat::Json));
315 assert_eq!(OutputFormat::parse("yaml"), Some(OutputFormat::Yaml));
316 assert_eq!(OutputFormat::parse("summary"), Some(OutputFormat::Summary));
317 assert_eq!(OutputFormat::parse("invalid"), None);
318 }
319
320 #[test]
321 fn test_format_json() {
322 let result = OptimizationResult::new(PathBuf::from("."), AnalysisMode::Static);
323 let json = format_json(&result);
324 assert!(json.contains("\"summary\""));
325 assert!(json.contains("\"recommendations\""));
326 }
327
328 #[test]
329 fn test_format_summary() {
330 let result = OptimizationResult::new(PathBuf::from("."), AnalysisMode::Static);
331 let summary = format_summary(&result);
332 assert!(summary.contains("RESOURCE OPTIMIZATION SUMMARY"));
333 assert!(summary.contains("Resources:"));
334 }
335
336 #[test]
337 fn test_format_table() {
338 let result = OptimizationResult::new(PathBuf::from("."), AnalysisMode::Static);
339 let table = format_table(&result);
340 assert!(table.contains("KUBERNETES RESOURCE OPTIMIZATION REPORT"));
341 assert!(table.contains("Summary"));
342 }
343}