vault_audit_tools/commands/
system_overview.rs1use crate::audit::types::AuditEntry;
40use crate::utils::progress::ProgressBar;
41use crate::utils::reader::open_file;
42use anyhow::Result;
43use std::collections::{HashMap, HashSet};
44use std::io::{BufRead, BufReader};
45
46#[derive(Debug)]
48struct PathData {
49 count: usize,
50 operations: HashMap<String, usize>,
51 entities: HashSet<String>,
52}
53
54impl PathData {
55 fn new() -> Self {
56 Self {
57 count: 0,
58 operations: HashMap::new(),
59 entities: HashSet::new(),
60 }
61 }
62}
63
64fn format_number(n: usize) -> String {
65 let s = n.to_string();
66 let mut result = String::new();
67 for (i, c) in s.chars().rev().enumerate() {
68 if i > 0 && i % 3 == 0 {
69 result.push(',');
70 }
71 result.push(c);
72 }
73 result.chars().rev().collect()
74}
75
76pub fn run(log_files: &[String], top: usize, min_operations: usize) -> Result<()> {
77 let mut path_operations: HashMap<String, PathData> = HashMap::new();
78 let mut operation_types: HashMap<String, usize> = HashMap::new();
79 let mut path_prefixes: HashMap<String, usize> = HashMap::new();
80 let mut entity_paths: HashMap<String, HashMap<String, usize>> = HashMap::new();
81 let mut entity_names: HashMap<String, String> = HashMap::new();
82 let mut total_lines = 0;
83
84 for (file_idx, log_file) in log_files.iter().enumerate() {
86 eprintln!(
87 "[{}/{}] Processing: {}",
88 file_idx + 1,
89 log_files.len(),
90 log_file
91 );
92
93 let file_size = std::fs::metadata(log_file).ok().map(|m| m.len() as usize);
95 let mut progress = if let Some(size) = file_size {
96 ProgressBar::new(size, "Processing")
97 } else {
98 ProgressBar::new_spinner("Processing")
99 };
100
101 let file = open_file(log_file)?;
102 let reader = BufReader::new(file);
103
104 let mut file_lines = 0;
105 let mut bytes_read = 0;
106
107 for line in reader.lines() {
108 file_lines += 1;
109 total_lines += 1;
110 let line = line?;
111 bytes_read += line.len() + 1; if file_lines % 10_000 == 0 {
115 if let Some(size) = file_size {
116 progress.update(bytes_read.min(size)); } else {
118 progress.update(file_lines);
119 }
120 }
121
122 let entry: AuditEntry = match serde_json::from_str(&line) {
123 Ok(e) => e,
124 Err(_) => continue,
125 };
126
127 let request = match &entry.request {
128 Some(r) => r,
129 None => continue,
130 };
131
132 let path = match &request.path {
133 Some(p) => p.as_str(),
134 None => continue,
135 };
136
137 let operation = match &request.operation {
138 Some(o) => o.as_str(),
139 None => continue,
140 };
141
142 let entity_id = entry
143 .auth
144 .as_ref()
145 .and_then(|a| a.entity_id.as_deref())
146 .unwrap_or("no-entity");
147
148 let display_name = entry
149 .auth
150 .as_ref()
151 .and_then(|a| a.display_name.as_deref())
152 .unwrap_or("N/A");
153
154 if path.is_empty() || operation.is_empty() {
155 continue;
156 }
157
158 let path_data = path_operations
160 .entry(path.to_string())
161 .or_insert_with(PathData::new);
162 path_data.count += 1;
163 *path_data
164 .operations
165 .entry(operation.to_string())
166 .or_insert(0) += 1;
167 path_data.entities.insert(entity_id.to_string());
169
170 *operation_types.entry(operation.to_string()).or_insert(0) += 1;
172
173 let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
175 let prefix = if parts.len() >= 2 {
176 format!("{}/{}", parts[0], parts[1])
177 } else if !parts.is_empty() {
178 parts[0].to_string()
179 } else {
180 "root".to_string()
181 };
182 *path_prefixes.entry(prefix).or_insert(0) += 1;
183
184 let entity_map = entity_paths.entry(entity_id.to_string()).or_default();
186 *entity_map.entry(path.to_string()).or_insert(0) += 1;
187 entity_names
188 .entry(entity_id.to_string())
189 .or_insert_with(|| display_name.to_string());
190 }
191
192 if let Some(size) = file_size {
194 progress.update(size);
195 }
196
197 progress.finish_with_message(&format!(
198 "Processed {} lines from this file",
199 format_number(file_lines)
200 ));
201 }
202
203 eprintln!("\nTotal: Processed {} lines", format_number(total_lines));
204
205 let total_operations: usize = operation_types.values().sum();
206
207 println!("\n{}", "=".repeat(100));
209 println!("High-Volume Vault Operations Analysis");
210 println!("{}", "=".repeat(100));
211
212 println!("\n1. Operation Types (Overall)");
214 println!("{}", "-".repeat(100));
215 println!("{:<20} {:>15} {:>12}", "Operation", "Count", "Percentage");
216 println!("{}", "-".repeat(100));
217
218 let mut sorted_ops: Vec<_> = operation_types.iter().collect();
219 sorted_ops.sort_by(|a, b| b.1.cmp(a.1));
220
221 for (op, count) in sorted_ops {
222 let pct = if total_operations > 0 {
223 (*count as f64 / total_operations as f64) * 100.0
224 } else {
225 0.0
226 };
227 println!("{:<20} {:>15} {:>11.2}%", op, format_number(*count), pct);
228 }
229
230 println!("{}", "-".repeat(100));
231 println!(
232 "{:<20} {:>15} {:>11.2}%",
233 "TOTAL",
234 format_number(total_operations),
235 100.0
236 );
237
238 println!("\n2. Top Path Prefixes (First 2 components)");
240 println!("{}", "-".repeat(100));
241 println!(
242 "{:<40} {:>15} {:>12}",
243 "Path Prefix", "Operations", "Percentage"
244 );
245 println!("{}", "-".repeat(100));
246
247 let mut sorted_prefixes: Vec<_> = path_prefixes.iter().collect();
248 sorted_prefixes.sort_by(|a, b| b.1.cmp(a.1));
249
250 for (prefix, count) in sorted_prefixes.iter().take(top) {
251 let pct = if total_operations > 0 {
252 (**count as f64 / total_operations as f64) * 100.0
253 } else {
254 0.0
255 };
256 println!(
257 "{:<40} {:>15} {:>11.2}%",
258 prefix,
259 format_number(**count),
260 pct
261 );
262 }
263
264 println!("\n3. Top {} Individual Paths (Highest Volume)", top);
266 println!("{}", "-".repeat(100));
267 println!(
268 "{:<60} {:>10} {:>10} {:>15}",
269 "Path", "Ops", "Entities", "Top Op"
270 );
271 println!("{}", "-".repeat(100));
272
273 let mut sorted_paths: Vec<_> = path_operations.iter().collect();
274 sorted_paths.sort_by(|a, b| b.1.count.cmp(&a.1.count));
275
276 for (path, data) in sorted_paths.iter().take(top) {
277 if data.count < min_operations {
278 break;
279 }
280 let top_op = data
281 .operations
282 .iter()
283 .max_by_key(|x| x.1)
284 .map(|x| x.0.as_str())
285 .unwrap_or("N/A");
286 let path_display = if path.len() > 60 {
287 format!("{}...", &path[..58])
288 } else {
289 path.to_string()
290 };
291 println!(
292 "{:<60} {:>10} {:>10} {:>15}",
293 path_display,
294 format_number(data.count),
295 format_number(data.entities.len()),
296 top_op
297 );
298 }
299
300 println!("\n4. Top {} Entities by Total Operations", top);
302 println!("{}", "-".repeat(100));
303 println!(
304 "{:<50} {:<38} {:>10}",
305 "Display Name", "Entity ID", "Total Ops"
306 );
307 println!("{}", "-".repeat(100));
308
309 let mut entity_totals: HashMap<String, usize> = HashMap::new();
310 for (entity_id, paths) in &entity_paths {
311 let total: usize = paths.values().sum();
312 entity_totals.insert(entity_id.clone(), total);
313 }
314
315 let mut sorted_entities: Vec<_> = entity_totals.iter().collect();
316 sorted_entities.sort_by(|a, b| b.1.cmp(a.1));
317
318 for (entity_id, total) in sorted_entities.iter().take(top) {
319 let name = entity_names
320 .get(*entity_id)
321 .map(|s| s.as_str())
322 .unwrap_or("N/A");
323 let name_display = if name.len() > 48 { &name[..48] } else { name };
324 let entity_short = if entity_id.len() > 36 {
325 &entity_id[..36]
326 } else {
327 entity_id
328 };
329 println!(
330 "{:<50} {:<38} {:>10}",
331 name_display,
332 entity_short,
333 format_number(**total)
334 );
335 }
336
337 println!("\n5. Potential System Stress Points");
339 println!("{}", "-".repeat(100));
340
341 #[derive(Debug)]
342 struct StressPoint {
343 path: String,
344 entity_name: String,
345 operations: usize,
346 }
347
348 let mut stress_points = Vec::new();
349
350 for (path, data) in &path_operations {
351 if data.count >= min_operations {
352 for entity_id in &data.entities {
353 if let Some(entity_ops_map) = entity_paths.get(entity_id) {
354 if let Some(&entity_ops) = entity_ops_map.get(path) {
355 if entity_ops >= min_operations {
356 stress_points.push(StressPoint {
357 path: path.clone(),
358 entity_name: entity_names
359 .get(entity_id)
360 .cloned()
361 .unwrap_or_else(|| "N/A".to_string()),
362 operations: entity_ops,
363 });
364 }
365 }
366 }
367 }
368 }
369 }
370
371 stress_points.sort_by(|a, b| b.operations.cmp(&a.operations));
372
373 println!("{:<40} {:<40} {:>10}", "Entity", "Path", "Ops");
374 println!("{}", "-".repeat(100));
375
376 for sp in stress_points.iter().take(top) {
377 let entity_display = if sp.entity_name.len() > 38 {
378 &sp.entity_name[..38]
379 } else {
380 &sp.entity_name
381 };
382 let path_display = if sp.path.len() > 38 {
383 &sp.path[..38]
384 } else {
385 &sp.path
386 };
387 println!(
388 "{:<40} {:<40} {:>10}",
389 entity_display,
390 path_display,
391 format_number(sp.operations)
392 );
393 }
394
395 println!("{}", "=".repeat(100));
396 println!("\nTotal Lines Processed: {}", format_number(total_lines));
397 println!("Total Operations: {}", format_number(total_operations));
398 println!("{}", "=".repeat(100));
399
400 Ok(())
401}