1use std::io::Write;
7use std::time::Instant;
8
9use crate::cli::wprintln;
10use crate::innodb::health;
11use crate::innodb::schema::SdiEnvelope;
12use crate::innodb::sdi;
13use crate::util::prometheus as prom;
14use crate::IdbError;
15
16pub struct HealthOptions {
18 pub file: String,
20 pub verbose: bool,
22 pub json: bool,
24 pub csv: bool,
26 pub prometheus: bool,
28 pub page_size: Option<u32>,
30 pub keyring: Option<String>,
32 pub mmap: bool,
34}
35
36pub fn execute(opts: &HealthOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
38 if opts.prometheus && (opts.json || opts.csv) {
39 return Err(IdbError::Argument(
40 "--prometheus cannot be combined with JSON or CSV output".to_string(),
41 ));
42 }
43
44 let start = Instant::now();
45
46 let mut ts = crate::cli::open_tablespace(&opts.file, opts.page_size, opts.mmap)?;
47
48 if let Some(ref keyring_path) = opts.keyring {
49 crate::cli::setup_decryption(&mut ts, keyring_path)?;
50 }
51
52 let page_size = ts.page_size();
53 let total_file_pages = ts.page_count();
54
55 let mut snapshots = Vec::new();
57 let mut empty_pages = 0u64;
58 let mut rtree_pages = 0u64;
59 let mut lob_pages = 0u64;
60 let mut undo_pages = 0u64;
61
62 ts.for_each_page(|page_num, data| {
63 if data.iter().all(|&b| b == 0) {
64 empty_pages += 1;
65 } else if let Some(snap) = health::extract_index_page_snapshot(data, page_num) {
66 snapshots.push(snap);
67 }
68
69 if let Some(fil) = crate::innodb::page::FilHeader::parse(data) {
71 use crate::innodb::page_types::PageType;
72 match fil.page_type {
73 PageType::Rtree | PageType::EncryptedRtree => rtree_pages += 1,
74 PageType::Blob
75 | PageType::ZBlob
76 | PageType::ZBlob2
77 | PageType::LobFirst
78 | PageType::LobData
79 | PageType::LobIndex
80 | PageType::ZlobFirst
81 | PageType::ZlobData
82 | PageType::ZlobFrag
83 | PageType::ZlobFragEntry
84 | PageType::ZlobIndex => lob_pages += 1,
85 PageType::UndoLog => undo_pages += 1,
86 _ => {}
87 }
88 }
89 Ok(())
90 })?;
91
92 let mut report = health::analyze_health(
93 snapshots,
94 page_size,
95 total_file_pages,
96 empty_pages,
97 &opts.file,
98 );
99 report.summary.rtree_pages = rtree_pages;
100 report.summary.lob_pages = lob_pages;
101 report.summary.undo_pages = undo_pages;
102
103 resolve_index_names(
105 &opts.file,
106 opts.page_size,
107 opts.mmap,
108 &opts.keyring,
109 &mut report,
110 );
111
112 let duration_secs = start.elapsed().as_secs_f64();
113
114 if opts.prometheus {
115 print_prometheus(writer, &report, duration_secs)?;
116 return Ok(());
117 }
118
119 if opts.json {
120 wprintln!(
121 writer,
122 "{}",
123 serde_json::to_string_pretty(&report).map_err(|e| IdbError::Parse(e.to_string()))?
124 )?;
125 } else if opts.csv {
126 wprintln!(
127 writer,
128 "index_id,index_name,tree_depth,total_pages,leaf_pages,avg_fill_factor,garbage_ratio,fragmentation"
129 )?;
130 for idx in &report.indexes {
131 wprintln!(
132 writer,
133 "{},{},{},{},{},{},{},{}",
134 idx.index_id,
135 crate::cli::csv_escape(idx.index_name.as_deref().unwrap_or("")),
136 idx.tree_depth,
137 idx.total_pages,
138 idx.leaf_pages,
139 idx.avg_fill_factor,
140 idx.avg_garbage_ratio,
141 idx.fragmentation
142 )?;
143 }
144 } else {
145 print_text(writer, &report, opts.verbose)?;
146 }
147
148 Ok(())
149}
150
151fn resolve_index_names(
153 file: &str,
154 page_size: Option<u32>,
155 mmap: bool,
156 keyring: &Option<String>,
157 report: &mut health::HealthReport,
158) {
159 let resolve = || -> Result<std::collections::HashMap<u64, String>, IdbError> {
160 let mut ts = crate::cli::open_tablespace(file, page_size, mmap)?;
161 if let Some(ref kp) = keyring {
162 crate::cli::setup_decryption(&mut ts, kp)?;
163 }
164 let sdi_pages = sdi::find_sdi_pages(&mut ts)?;
165 if sdi_pages.is_empty() {
166 return Ok(std::collections::HashMap::new());
167 }
168 let records = sdi::extract_sdi_from_pages(&mut ts, &sdi_pages)?;
169 let mut name_map = std::collections::HashMap::new();
170 for rec in &records {
171 if rec.sdi_type == 1 {
172 if let Ok(envelope) = serde_json::from_str::<SdiEnvelope>(&rec.data) {
173 for dd_idx in &envelope.dd_object.indexes {
174 if let Some(id) = parse_se_private_id(&rec.data, &dd_idx.name) {
181 name_map.insert(id, dd_idx.name.clone());
182 }
183 }
184 }
185 }
186 }
187 Ok(name_map)
188 };
189
190 if let Ok(name_map) = resolve() {
191 for idx in &mut report.indexes {
192 if let Some(name) = name_map.get(&idx.index_id) {
193 idx.index_name = Some(name.clone());
194 }
195 }
196 }
197}
198
199fn parse_se_private_id(sdi_json: &str, name: &str) -> Option<u64> {
204 let val: serde_json::Value = serde_json::from_str(sdi_json).ok()?;
206 let indexes = val.get("dd_object")?.get("indexes")?.as_array()?;
207 for idx in indexes {
208 let idx_name = idx.get("name")?.as_str()?;
209 if idx_name == name {
210 let se_data = idx.get("se_private_data")?.as_str()?;
211 for part in se_data.split(';') {
212 if let Some(id_str) = part.strip_prefix("id=") {
213 return id_str.parse::<u64>().ok();
214 }
215 }
216 }
217 }
218 None
219}
220
221fn print_text(
223 writer: &mut dyn Write,
224 report: &health::HealthReport,
225 verbose: bool,
226) -> Result<(), IdbError> {
227 wprintln!(writer, "Tablespace Health Report: {}", report.file)?;
228 wprintln!(writer)?;
229
230 if report.indexes.is_empty() {
231 wprintln!(writer, "No INDEX pages found.")?;
232 wprintln!(writer)?;
233 }
234
235 for idx in &report.indexes {
236 let name = idx.index_name.as_deref().unwrap_or("(unknown)");
237 wprintln!(writer, "Index {} ({}):", idx.index_id, name)?;
238 wprintln!(writer, " Tree depth: {}", idx.tree_depth)?;
239 wprintln!(
240 writer,
241 " Pages: {} total ({} leaf, {} non-leaf)",
242 idx.total_pages,
243 idx.leaf_pages,
244 idx.non_leaf_pages
245 )?;
246 wprintln!(
247 writer,
248 " Fill factor: avg {:.0}%, min {:.0}%, max {:.0}%",
249 idx.avg_fill_factor * 100.0,
250 idx.min_fill_factor * 100.0,
251 idx.max_fill_factor * 100.0
252 )?;
253 wprintln!(
254 writer,
255 " Garbage: {:.1}% ({} bytes)",
256 idx.avg_garbage_ratio * 100.0,
257 idx.total_garbage_bytes
258 )?;
259 wprintln!(
260 writer,
261 " Fragmentation: {:.1}%",
262 idx.fragmentation * 100.0
263 )?;
264
265 if verbose {
266 wprintln!(writer, " Total records: {}", idx.total_records)?;
267 if idx.empty_leaf_pages > 0 {
268 wprintln!(writer, " Empty leaves: {}", idx.empty_leaf_pages)?;
269 }
270 }
271
272 wprintln!(writer)?;
273 }
274
275 wprintln!(writer, "Summary:")?;
277 wprintln!(
278 writer,
279 " Total pages: {} ({} INDEX, {} non-INDEX, {} empty)",
280 report.summary.total_pages,
281 report.summary.index_pages,
282 report.summary.non_index_pages,
283 report.summary.empty_pages
284 )?;
285 wprintln!(
286 writer,
287 " Page size: {} bytes",
288 report.summary.page_size
289 )?;
290 wprintln!(writer, " Indexes: {}", report.summary.index_count)?;
291 if report.summary.rtree_pages > 0 {
292 wprintln!(writer, " RTREE pages: {}", report.summary.rtree_pages)?;
293 }
294 if report.summary.lob_pages > 0 {
295 wprintln!(writer, " LOB/BLOB pages: {}", report.summary.lob_pages)?;
296 }
297 if report.summary.undo_pages > 0 {
298 wprintln!(writer, " UNDO pages: {}", report.summary.undo_pages)?;
299 }
300 wprintln!(
301 writer,
302 " Avg fill factor: {:.0}%",
303 report.summary.avg_fill_factor * 100.0
304 )?;
305 wprintln!(
306 writer,
307 " Avg garbage: {:.1}%",
308 report.summary.avg_garbage_ratio * 100.0
309 )?;
310 wprintln!(
311 writer,
312 " Avg fragmentation: {:.1}%",
313 report.summary.avg_fragmentation * 100.0
314 )?;
315
316 Ok(())
317}
318
319fn print_prometheus(
321 writer: &mut dyn Write,
322 report: &health::HealthReport,
323 duration_secs: f64,
324) -> Result<(), IdbError> {
325 let file = &report.file;
326
327 wprintln!(
329 writer,
330 "{}",
331 prom::help_line("innodb_pages", "Total pages in the tablespace by type")
332 )?;
333 wprintln!(writer, "{}", prom::type_line("innodb_pages", "gauge"))?;
334 wprintln!(
335 writer,
336 "{}",
337 prom::format_gauge_int(
338 "innodb_pages",
339 &[("file", file), ("type", "index")],
340 report.summary.index_pages
341 )
342 )?;
343 wprintln!(
344 writer,
345 "{}",
346 prom::format_gauge_int(
347 "innodb_pages",
348 &[("file", file), ("type", "non_index")],
349 report.summary.non_index_pages
350 )
351 )?;
352 wprintln!(
353 writer,
354 "{}",
355 prom::format_gauge_int(
356 "innodb_pages",
357 &[("file", file), ("type", "empty")],
358 report.summary.empty_pages
359 )
360 )?;
361
362 wprintln!(
364 writer,
365 "{}",
366 prom::help_line("innodb_fill_factor", "Average B+Tree fill factor per index")
367 )?;
368 wprintln!(writer, "{}", prom::type_line("innodb_fill_factor", "gauge"))?;
369 for idx in &report.indexes {
370 let id_str = idx.index_id.to_string();
371 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
372 wprintln!(
373 writer,
374 "{}",
375 prom::format_gauge(
376 "innodb_fill_factor",
377 &[("file", file), ("index", index_label)],
378 idx.avg_fill_factor
379 )
380 )?;
381 }
382
383 wprintln!(
385 writer,
386 "{}",
387 prom::help_line(
388 "innodb_fragmentation_ratio",
389 "Leaf-level fragmentation ratio per index"
390 )
391 )?;
392 wprintln!(
393 writer,
394 "{}",
395 prom::type_line("innodb_fragmentation_ratio", "gauge")
396 )?;
397 for idx in &report.indexes {
398 let id_str = idx.index_id.to_string();
399 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
400 wprintln!(
401 writer,
402 "{}",
403 prom::format_gauge(
404 "innodb_fragmentation_ratio",
405 &[("file", file), ("index", index_label)],
406 idx.fragmentation
407 )
408 )?;
409 }
410
411 wprintln!(
413 writer,
414 "{}",
415 prom::help_line("innodb_garbage_ratio", "Average garbage ratio per index")
416 )?;
417 wprintln!(
418 writer,
419 "{}",
420 prom::type_line("innodb_garbage_ratio", "gauge")
421 )?;
422 for idx in &report.indexes {
423 let id_str = idx.index_id.to_string();
424 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
425 wprintln!(
426 writer,
427 "{}",
428 prom::format_gauge(
429 "innodb_garbage_ratio",
430 &[("file", file), ("index", index_label)],
431 idx.avg_garbage_ratio
432 )
433 )?;
434 }
435
436 wprintln!(
438 writer,
439 "{}",
440 prom::help_line("innodb_index_pages", "Total pages per index")
441 )?;
442 wprintln!(writer, "{}", prom::type_line("innodb_index_pages", "gauge"))?;
443 for idx in &report.indexes {
444 let id_str = idx.index_id.to_string();
445 let index_label = idx.index_name.as_deref().unwrap_or(&id_str);
446 wprintln!(
447 writer,
448 "{}",
449 prom::format_gauge_int(
450 "innodb_index_pages",
451 &[("file", file), ("index", index_label)],
452 idx.total_pages
453 )
454 )?;
455 }
456
457 wprintln!(
459 writer,
460 "{}",
461 prom::help_line(
462 "innodb_scan_duration_seconds",
463 "Time spent scanning the tablespace"
464 )
465 )?;
466 wprintln!(
467 writer,
468 "{}",
469 prom::type_line("innodb_scan_duration_seconds", "gauge")
470 )?;
471 wprintln!(
472 writer,
473 "{}",
474 prom::format_gauge(
475 "innodb_scan_duration_seconds",
476 &[("file", file)],
477 duration_secs
478 )
479 )?;
480
481 Ok(())
482}