1use std::io::Write;
2use std::path::Path;
3use std::time::Instant;
4
5use colored::Colorize;
6use rayon::prelude::*;
7use serde::Serialize;
8
9use crate::cli::{create_progress_bar, csv_escape, wprintln};
10use crate::innodb::checksum::{validate_checksum, validate_lsn};
11use crate::innodb::health;
12use crate::util::fs::find_tablespace_files;
13use crate::util::prometheus as prom;
14use crate::IdbError;
15
16pub struct AuditOptions {
18 pub datadir: String,
20 pub health: bool,
22 pub checksum_mismatch: bool,
24 pub verbose: bool,
26 pub json: bool,
28 pub csv: bool,
30 pub prometheus: bool,
32 pub page_size: Option<u32>,
34 pub keyring: Option<String>,
36 pub mmap: bool,
38 pub min_fill_factor: Option<f64>,
40 pub max_fragmentation: Option<f64>,
42 pub bloat: bool,
44 pub max_bloat_grade: Option<String>,
46 pub depth: Option<u32>,
48 pub compliance: bool,
50 pub pattern: Option<String>,
52}
53
54#[derive(Serialize)]
59struct AuditReport {
60 datadir: String,
61 files: Vec<FileIntegrityResult>,
62 summary: AuditSummary,
63}
64
65#[derive(Serialize, Clone)]
66struct FileIntegrityResult {
67 file: String,
68 status: String,
69 page_size: u32,
70 total_pages: u64,
71 empty_pages: u64,
72 valid_pages: u64,
73 invalid_pages: u64,
74 lsn_mismatches: u64,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 error: Option<String>,
77 #[serde(skip_serializing_if = "Vec::is_empty")]
78 corrupt_pages: Vec<u64>,
79}
80
81#[derive(Serialize)]
82struct AuditSummary {
83 total_files: usize,
84 files_passed: usize,
85 files_failed: usize,
86 files_error: usize,
87 total_pages: u64,
88 corrupt_pages: u64,
89 integrity_pct: f64,
90}
91
92#[derive(Serialize, Clone)]
99struct FileComplianceResult {
100 file: String,
101 match_count: usize,
102 pages_with_matches: usize,
103 capped: bool,
104 #[serde(skip_serializing_if = "Vec::is_empty")]
105 sample_matches: Vec<crate::innodb::compliance::ResidueMatch>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 error: Option<String>,
108}
109
110#[derive(Serialize)]
111struct ComplianceReport {
112 datadir: String,
113 pattern: String,
114 files: Vec<FileComplianceResult>,
115 summary: ComplianceSummary,
116}
117
118#[derive(Serialize)]
119struct ComplianceSummary {
120 total_files: usize,
121 files_with_residue: usize,
122 total_matches: usize,
123}
124
125#[derive(Serialize, Clone)]
130struct FileHealthResult {
131 file: String,
132 avg_fill_factor: f64,
133 avg_fragmentation: f64,
134 avg_garbage_ratio: f64,
135 index_count: u64,
136 total_index_pages: u64,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 worst_bloat_grade: Option<String>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 worst_bloat_score: Option<f64>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 error: Option<String>,
143}
144
145#[derive(Serialize)]
146struct HealthAuditReport {
147 datadir: String,
148 tablespaces: Vec<FileHealthResult>,
149 summary: DirectoryHealthSummary,
150}
151
152#[derive(Serialize)]
153struct DirectoryHealthSummary {
154 total_files: usize,
155 total_index_pages: u64,
156 avg_fill_factor: f64,
157 avg_fragmentation: f64,
158 avg_garbage_ratio: f64,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 worst_bloat_grade: Option<String>,
161}
162
163#[derive(Serialize, Clone)]
168struct MismatchEntry {
169 file: String,
170 page_number: u64,
171 stored_checksum: u32,
172 calculated_checksum: u32,
173 algorithm: String,
174}
175
176#[derive(Serialize)]
177struct MismatchReport {
178 datadir: String,
179 mismatches: Vec<MismatchEntry>,
180 total_files_scanned: usize,
181 total_pages_scanned: u64,
182}
183
184fn audit_file(
190 path: &Path,
191 datadir: &Path,
192 page_size_override: Option<u32>,
193 keyring: &Option<String>,
194 use_mmap: bool,
195) -> FileIntegrityResult {
196 let display = path.strip_prefix(datadir).unwrap_or(path);
197 let display_str = display.display().to_string();
198 let path_str = path.to_string_lossy();
199
200 let mut ts = match crate::cli::open_tablespace(&path_str, page_size_override, use_mmap) {
201 Ok(t) => t,
202 Err(e) => {
203 return FileIntegrityResult {
204 file: display_str,
205 status: "error".to_string(),
206 page_size: 0,
207 total_pages: 0,
208 empty_pages: 0,
209 valid_pages: 0,
210 invalid_pages: 0,
211 lsn_mismatches: 0,
212 error: Some(e.to_string()),
213 corrupt_pages: Vec::new(),
214 };
215 }
216 };
217
218 if let Some(ref kp) = keyring {
219 if crate::cli::setup_decryption(&mut ts, kp).is_err() {
220 }
222 }
223
224 let page_size = ts.page_size();
225 let page_count = ts.page_count();
226 let vendor_info = ts.vendor_info().clone();
227
228 let all_data = match ts.read_all_pages() {
230 Ok(d) => d,
231 Err(e) => {
232 return FileIntegrityResult {
233 file: display_str,
234 status: "error".to_string(),
235 page_size,
236 total_pages: page_count,
237 empty_pages: 0,
238 valid_pages: 0,
239 invalid_pages: 0,
240 lsn_mismatches: 0,
241 error: Some(e.to_string()),
242 corrupt_pages: Vec::new(),
243 };
244 }
245 };
246
247 let ps = page_size as usize;
248
249 let results: Vec<(u64, bool, bool, bool, Option<u64>)> = (0..page_count)
251 .into_par_iter()
252 .map(|page_num| {
253 let offset = page_num as usize * ps;
254 if offset + ps > all_data.len() {
255 return (page_num, false, false, false, None);
256 }
257 let page_data = &all_data[offset..offset + ps];
258
259 if page_data.iter().all(|&b| b == 0) {
261 return (page_num, true, true, true, None); }
263
264 let csum = validate_checksum(page_data, page_size, Some(&vendor_info));
265 let lsn_ok = validate_lsn(page_data, page_size);
266
267 let corrupt_page = if !csum.valid { Some(page_num) } else { None };
268 (page_num, csum.valid, false, lsn_ok, corrupt_page)
270 })
271 .collect();
272
273 let mut valid = 0u64;
274 let mut invalid = 0u64;
275 let mut empty = 0u64;
276 let mut lsn_mismatches = 0u64;
277 let mut corrupt_pages = Vec::new();
278
279 for &(_, is_valid, is_empty, lsn_ok, ref corrupt) in &results {
280 if is_empty {
281 empty += 1;
282 } else if is_valid {
283 valid += 1;
284 } else {
285 invalid += 1;
286 }
287 if !lsn_ok && !is_empty && is_valid {
288 lsn_mismatches += 1;
289 }
290 if let Some(pn) = corrupt {
291 corrupt_pages.push(*pn);
292 }
293 }
294
295 let status = if invalid > 0 { "FAIL" } else { "PASS" };
296
297 FileIntegrityResult {
298 file: display_str,
299 status: status.to_string(),
300 page_size,
301 total_pages: page_count,
302 empty_pages: empty,
303 valid_pages: valid,
304 invalid_pages: invalid,
305 lsn_mismatches,
306 error: None,
307 corrupt_pages,
308 }
309}
310
311fn audit_file_health(
313 path: &Path,
314 datadir: &Path,
315 page_size_override: Option<u32>,
316 keyring: &Option<String>,
317 use_mmap: bool,
318 bloat: bool,
319) -> FileHealthResult {
320 let display = path.strip_prefix(datadir).unwrap_or(path);
321 let display_str = display.display().to_string();
322 let path_str = path.to_string_lossy();
323
324 let mut ts = match crate::cli::open_tablespace(&path_str, page_size_override, use_mmap) {
325 Ok(t) => t,
326 Err(e) => {
327 return FileHealthResult {
328 file: display_str,
329 avg_fill_factor: 0.0,
330 avg_fragmentation: 0.0,
331 avg_garbage_ratio: 0.0,
332 index_count: 0,
333 total_index_pages: 0,
334 worst_bloat_grade: None,
335 worst_bloat_score: None,
336 error: Some(e.to_string()),
337 };
338 }
339 };
340
341 if let Some(ref kp) = keyring {
342 let _ = crate::cli::setup_decryption(&mut ts, kp);
343 }
344
345 let page_size = ts.page_size();
346 let total_pages = ts.page_count();
347
348 let mut snapshots = Vec::new();
349 let mut empty_pages = 0u64;
350 let mut delete_counts: std::collections::HashMap<u64, (u64, u64)> =
352 std::collections::HashMap::new();
353
354 let scan_result = ts.for_each_page(|page_num, data| {
355 if data.iter().all(|&b| b == 0) {
356 empty_pages += 1;
357 } else if let Some(snap) = health::extract_index_page_snapshot(data, page_num) {
358 if snap.level == 0 && bloat {
360 let recs = crate::innodb::record::walk_compact_records(data);
361 let total = recs.len() as u64;
362 let deleted = recs.iter().filter(|r| r.header.delete_mark()).count() as u64;
363 let entry = delete_counts.entry(snap.index_id).or_insert((0, 0));
364 entry.0 += deleted;
365 entry.1 += total;
366 }
367 snapshots.push(snap);
368 }
369 Ok(())
370 });
371
372 if let Err(e) = scan_result {
373 return FileHealthResult {
374 file: display_str,
375 avg_fill_factor: 0.0,
376 avg_fragmentation: 0.0,
377 avg_garbage_ratio: 0.0,
378 index_count: 0,
379 total_index_pages: 0,
380 worst_bloat_grade: None,
381 worst_bloat_score: None,
382 error: Some(e.to_string()),
383 };
384 }
385
386 let mut report =
387 health::analyze_health(snapshots, page_size, total_pages, empty_pages, &path_str);
388
389 let (worst_grade, worst_score) = if bloat {
391 let mut worst_g: Option<String> = None;
392 let mut worst_s: f64 = 0.0;
393 for idx in &mut report.indexes {
394 let (deleted, total) = delete_counts.get(&idx.index_id).copied().unwrap_or((0, 0));
395 let delete_mark_ratio = if total > 0 {
396 deleted as f64 / total as f64
397 } else {
398 0.0
399 };
400 let bloat_score = health::score_bloat(idx, delete_mark_ratio);
401 if bloat_score.score > worst_s {
402 worst_s = bloat_score.score;
403 worst_g = Some(format!("{}", bloat_score.grade));
404 }
405 idx.bloat = Some(bloat_score);
406 }
407 let has_score = worst_g.is_some();
408 (worst_g, if has_score { Some(worst_s) } else { None })
409 } else {
410 (None, None)
411 };
412
413 FileHealthResult {
414 file: display_str,
415 avg_fill_factor: report.summary.avg_fill_factor,
416 avg_fragmentation: report.summary.avg_fragmentation,
417 avg_garbage_ratio: report.summary.avg_garbage_ratio,
418 index_count: report.summary.index_count,
419 total_index_pages: report.summary.index_pages,
420 worst_bloat_grade: worst_grade,
421 worst_bloat_score: worst_score,
422 error: None,
423 }
424}
425
426fn audit_file_mismatches(
428 path: &Path,
429 datadir: &Path,
430 page_size_override: Option<u32>,
431 keyring: &Option<String>,
432 use_mmap: bool,
433) -> (Vec<MismatchEntry>, u64) {
434 let display = path.strip_prefix(datadir).unwrap_or(path);
435 let display_str = display.display().to_string();
436 let path_str = path.to_string_lossy();
437
438 let mut ts = match crate::cli::open_tablespace(&path_str, page_size_override, use_mmap) {
439 Ok(t) => t,
440 Err(_) => return (Vec::new(), 0),
441 };
442
443 if let Some(ref kp) = keyring {
444 let _ = crate::cli::setup_decryption(&mut ts, kp);
445 }
446
447 let page_size = ts.page_size();
448 let page_count = ts.page_count();
449 let vendor_info = ts.vendor_info().clone();
450
451 let all_data = match ts.read_all_pages() {
452 Ok(d) => d,
453 Err(_) => return (Vec::new(), page_count),
454 };
455
456 let ps = page_size as usize;
457
458 let mismatches: Vec<MismatchEntry> = (0..page_count)
459 .into_par_iter()
460 .filter_map(|page_num| {
461 let offset = page_num as usize * ps;
462 if offset + ps > all_data.len() {
463 return None;
464 }
465 let page_data = &all_data[offset..offset + ps];
466
467 if page_data.iter().all(|&b| b == 0) {
468 return None;
469 }
470
471 let csum = validate_checksum(page_data, page_size, Some(&vendor_info));
472 if csum.valid {
473 return None;
474 }
475
476 Some(MismatchEntry {
477 file: display_str.clone(),
478 page_number: page_num,
479 stored_checksum: csum.stored_checksum,
480 calculated_checksum: csum.calculated_checksum,
481 algorithm: algorithm_name(csum.algorithm).to_string(),
482 })
483 })
484 .collect();
485
486 (mismatches, page_count)
487}
488
489fn algorithm_name(algo: crate::innodb::checksum::ChecksumAlgorithm) -> &'static str {
490 match algo {
491 crate::innodb::checksum::ChecksumAlgorithm::Crc32c => "crc32c",
492 crate::innodb::checksum::ChecksumAlgorithm::InnoDB => "innodb",
493 crate::innodb::checksum::ChecksumAlgorithm::MariaDbFullCrc32 => "mariadb_full_crc32",
494 crate::innodb::checksum::ChecksumAlgorithm::None => "none",
495 }
496}
497
498pub fn execute(opts: &AuditOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
504 if opts.prometheus && (opts.json || opts.csv) {
505 return Err(IdbError::Argument(
506 "--prometheus cannot be combined with JSON or CSV output".to_string(),
507 ));
508 }
509
510 if opts.health && opts.checksum_mismatch {
512 return Err(IdbError::Argument(
513 "--health and --checksum-mismatch are mutually exclusive".to_string(),
514 ));
515 }
516 if opts.compliance {
517 if opts.health || opts.checksum_mismatch {
518 return Err(IdbError::Argument(
519 "--compliance cannot be combined with --health or --checksum-mismatch".to_string(),
520 ));
521 }
522 if opts.prometheus {
523 return Err(IdbError::Argument(
524 "--compliance does not support --prometheus output".to_string(),
525 ));
526 }
527 if opts.pattern.is_none() {
528 return Err(IdbError::Argument(
529 "--compliance requires --pattern <text|hex:...>".to_string(),
530 ));
531 }
532 }
533
534 let datadir = Path::new(&opts.datadir);
535 if !datadir.is_dir() {
536 return Err(IdbError::Argument(format!(
537 "Data directory does not exist: {}",
538 opts.datadir
539 )));
540 }
541
542 let ibd_files = find_tablespace_files(datadir, &["ibd"], opts.depth)?;
543
544 if ibd_files.is_empty() {
545 if opts.prometheus {
546 return Ok(());
548 } else if opts.json {
549 if opts.compliance {
550 let report = ComplianceReport {
551 datadir: opts.datadir.clone(),
552 pattern: opts.pattern.clone().unwrap_or_default(),
553 files: Vec::new(),
554 summary: ComplianceSummary {
555 total_files: 0,
556 files_with_residue: 0,
557 total_matches: 0,
558 },
559 };
560 let json = serde_json::to_string_pretty(&report)
561 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
562 wprintln!(writer, "{}", json)?;
563 } else if opts.health {
564 let report = HealthAuditReport {
565 datadir: opts.datadir.clone(),
566 tablespaces: Vec::new(),
567 summary: DirectoryHealthSummary {
568 total_files: 0,
569 total_index_pages: 0,
570 avg_fill_factor: 0.0,
571 avg_fragmentation: 0.0,
572 avg_garbage_ratio: 0.0,
573 worst_bloat_grade: None,
574 },
575 };
576 let json = serde_json::to_string_pretty(&report)
577 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
578 wprintln!(writer, "{}", json)?;
579 } else if opts.checksum_mismatch {
580 let report = MismatchReport {
581 datadir: opts.datadir.clone(),
582 mismatches: Vec::new(),
583 total_files_scanned: 0,
584 total_pages_scanned: 0,
585 };
586 let json = serde_json::to_string_pretty(&report)
587 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
588 wprintln!(writer, "{}", json)?;
589 } else {
590 let report = AuditReport {
591 datadir: opts.datadir.clone(),
592 files: Vec::new(),
593 summary: AuditSummary {
594 total_files: 0,
595 files_passed: 0,
596 files_failed: 0,
597 files_error: 0,
598 total_pages: 0,
599 corrupt_pages: 0,
600 integrity_pct: 100.0,
601 },
602 };
603 let json = serde_json::to_string_pretty(&report)
604 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
605 wprintln!(writer, "{}", json)?;
606 }
607 } else if opts.csv {
608 if opts.compliance {
609 wprintln!(writer, "file,match_count,pages_with_matches,capped,error")?;
610 } else if opts.health {
611 let do_bloat = opts.bloat || opts.max_bloat_grade.is_some();
612 if do_bloat {
613 wprintln!(
614 writer,
615 "file,avg_fill_factor,avg_fragmentation,avg_garbage_ratio,index_count,total_index_pages,worst_bloat_grade,worst_bloat_score"
616 )?;
617 } else {
618 wprintln!(
619 writer,
620 "file,avg_fill_factor,avg_fragmentation,avg_garbage_ratio,index_count,total_index_pages"
621 )?;
622 }
623 } else if opts.checksum_mismatch {
624 wprintln!(
625 writer,
626 "file,page_number,stored_checksum,calculated_checksum,algorithm"
627 )?;
628 } else {
629 wprintln!(
630 writer,
631 "file,status,total_pages,empty_pages,valid_pages,invalid_pages,lsn_mismatches"
632 )?;
633 }
634 } else {
635 wprintln!(writer, "No .ibd files found in {}", opts.datadir)?;
636 }
637 return Ok(());
638 }
639
640 if opts.compliance {
641 execute_compliance(opts, &ibd_files, datadir, writer)
642 } else if opts.health {
643 execute_health(opts, &ibd_files, datadir, writer)
644 } else if opts.checksum_mismatch {
645 execute_mismatch(opts, &ibd_files, datadir, writer)
646 } else {
647 execute_integrity(opts, &ibd_files, datadir, writer)
648 }
649}
650
651const COMPLIANCE_MATCH_CAP: usize = 10_000;
657const COMPLIANCE_SAMPLE: usize = 25;
659
660fn compliance_file(
661 path: &Path,
662 datadir: &Path,
663 page_size_override: Option<u32>,
664 keyring: &Option<String>,
665 use_mmap: bool,
666 pattern: &crate::innodb::compliance::Pattern,
667) -> FileComplianceResult {
668 let display = path.strip_prefix(datadir).unwrap_or(path);
669 let display_str = display.display().to_string();
670 let path_str = path.to_string_lossy();
671
672 let mut ts = match crate::cli::open_tablespace(&path_str, page_size_override, use_mmap) {
673 Ok(t) => t,
674 Err(e) => {
675 return FileComplianceResult {
676 file: display_str,
677 match_count: 0,
678 pages_with_matches: 0,
679 capped: false,
680 sample_matches: Vec::new(),
681 error: Some(e.to_string()),
682 }
683 }
684 };
685
686 if let Some(ref kp) = keyring {
687 let _ = crate::cli::setup_decryption(&mut ts, kp);
688 }
689
690 match crate::innodb::compliance::scan_residue(&mut ts, pattern, COMPLIANCE_MATCH_CAP) {
691 Ok(matches) => {
692 let match_count = matches.len();
693 let mut pages: std::collections::HashSet<u64> = std::collections::HashSet::new();
694 for m in &matches {
695 pages.insert(m.page_number);
696 }
697 let sample_matches = matches.into_iter().take(COMPLIANCE_SAMPLE).collect();
698 FileComplianceResult {
699 file: display_str,
700 match_count,
701 pages_with_matches: pages.len(),
702 capped: match_count >= COMPLIANCE_MATCH_CAP,
703 sample_matches,
704 error: None,
705 }
706 }
707 Err(e) => FileComplianceResult {
708 file: display_str,
709 match_count: 0,
710 pages_with_matches: 0,
711 capped: false,
712 sample_matches: Vec::new(),
713 error: Some(e.to_string()),
714 },
715 }
716}
717
718fn execute_compliance(
719 opts: &AuditOptions,
720 ibd_files: &[std::path::PathBuf],
721 datadir: &Path,
722 writer: &mut dyn Write,
723) -> Result<(), IdbError> {
724 let pattern_str = opts.pattern.as_deref().ok_or_else(|| {
725 IdbError::Argument("--compliance requires --pattern <text|hex:...>".to_string())
726 })?;
727 let pattern = crate::innodb::compliance::Pattern::parse(pattern_str)?;
728
729 let pb = if !opts.json && !opts.csv {
730 Some(create_progress_bar(ibd_files.len() as u64, "files"))
731 } else {
732 None
733 };
734
735 let page_size = opts.page_size;
736 let keyring = opts.keyring.clone();
737 let use_mmap = opts.mmap;
738
739 let mut results: Vec<FileComplianceResult> = ibd_files
740 .par_iter()
741 .map(|path| {
742 let r = compliance_file(path, datadir, page_size, &keyring, use_mmap, &pattern);
743 if let Some(ref pb) = pb {
744 pb.inc(1);
745 }
746 r
747 })
748 .collect();
749
750 if let Some(ref pb) = pb {
751 pb.finish_and_clear();
752 }
753
754 results.sort_by(|a, b| a.file.cmp(&b.file));
755
756 let total_matches: usize = results.iter().map(|r| r.match_count).sum();
757 let files_with_residue = results.iter().filter(|r| r.match_count > 0).count();
758 let summary = ComplianceSummary {
759 total_files: results.len(),
760 files_with_residue,
761 total_matches,
762 };
763
764 if opts.json {
765 let report = ComplianceReport {
766 datadir: opts.datadir.clone(),
767 pattern: pattern_str.to_string(),
768 files: results,
769 summary,
770 };
771 let json = serde_json::to_string_pretty(&report)
772 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
773 wprintln!(writer, "{}", json)?;
774 return Ok(());
775 }
776
777 if opts.csv {
778 wprintln!(writer, "file,match_count,pages_with_matches,capped,error")?;
779 for r in &results {
780 wprintln!(
781 writer,
782 "{},{},{},{},{}",
783 csv_escape(&r.file),
784 r.match_count,
785 r.pages_with_matches,
786 if r.capped { "Y" } else { "N" },
787 csv_escape(r.error.as_deref().unwrap_or("")),
788 )?;
789 }
790 return Ok(());
791 }
792
793 wprintln!(
795 writer,
796 "Data-residue audit for pattern '{}' across {} file(s):",
797 pattern_str,
798 results.len()
799 )?;
800 for r in &results {
801 if let Some(ref e) = r.error {
802 wprintln!(writer, " {}: error: {}", r.file, e)?;
803 } else if r.match_count == 0 {
804 if opts.verbose {
805 wprintln!(writer, " {}: clean", r.file)?;
806 }
807 } else {
808 wprintln!(
809 writer,
810 " {}: {} match(es) across {} page(s){}",
811 r.file,
812 r.match_count,
813 r.pages_with_matches,
814 if r.capped { " (capped)" } else { "" }
815 )?;
816 }
817 }
818 wprintln!(
819 writer,
820 "\n{} match(es) in {} of {} file(s).",
821 total_matches,
822 files_with_residue,
823 results.len()
824 )?;
825
826 Ok(())
827}
828
829fn execute_integrity(
834 opts: &AuditOptions,
835 ibd_files: &[std::path::PathBuf],
836 datadir: &Path,
837 writer: &mut dyn Write,
838) -> Result<(), IdbError> {
839 let start = Instant::now();
840
841 let pb = if !opts.json && !opts.csv && !opts.prometheus {
842 Some(create_progress_bar(ibd_files.len() as u64, "files"))
843 } else {
844 None
845 };
846
847 let page_size = opts.page_size;
848 let keyring = opts.keyring.clone();
849 let use_mmap = opts.mmap;
850
851 let mut results: Vec<FileIntegrityResult> = ibd_files
852 .par_iter()
853 .map(|path| {
854 let r = audit_file(path, datadir, page_size, &keyring, use_mmap);
855 if let Some(ref pb) = pb {
856 pb.inc(1);
857 }
858 r
859 })
860 .collect();
861
862 if let Some(ref pb) = pb {
863 pb.finish_and_clear();
864 }
865
866 results.sort_by(|a, b| a.file.cmp(&b.file));
868
869 let total_files = results.len();
871 let files_passed = results.iter().filter(|r| r.status == "PASS").count();
872 let files_failed = results.iter().filter(|r| r.status == "FAIL").count();
873 let files_error = results.iter().filter(|r| r.status == "error").count();
874 let total_pages: u64 = results.iter().map(|r| r.total_pages).sum();
875 let corrupt_pages: u64 = results.iter().map(|r| r.invalid_pages).sum();
876 let valid_pages: u64 = results.iter().map(|r| r.valid_pages).sum();
877 let checked_pages = valid_pages + corrupt_pages;
878 let integrity_pct = if checked_pages > 0 {
879 (valid_pages as f64 / checked_pages as f64) * 100.0
880 } else {
881 100.0
882 };
883 let integrity_pct = (integrity_pct * 100.0).round() / 100.0;
884
885 if opts.prometheus {
886 let duration_secs = start.elapsed().as_secs_f64();
887 print_prometheus_integrity(
888 writer,
889 &IntegrityPrometheusParams {
890 datadir: &opts.datadir,
891 results: &results,
892 total_pages,
893 corrupt_pages,
894 integrity_pct,
895 duration_secs,
896 },
897 )?;
898
899 if corrupt_pages > 0 {
900 return Err(IdbError::Parse(format!(
901 "{} corrupt pages found across {} files",
902 corrupt_pages, files_failed
903 )));
904 }
905 return Ok(());
906 }
907
908 if opts.json {
909 let report = AuditReport {
910 datadir: opts.datadir.clone(),
911 files: results,
912 summary: AuditSummary {
913 total_files,
914 files_passed,
915 files_failed,
916 files_error,
917 total_pages,
918 corrupt_pages,
919 integrity_pct,
920 },
921 };
922 let json = serde_json::to_string_pretty(&report)
923 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
924 wprintln!(writer, "{}", json)?;
925 } else if opts.csv {
926 wprintln!(
927 writer,
928 "file,status,total_pages,empty_pages,valid_pages,invalid_pages,lsn_mismatches"
929 )?;
930 for r in &results {
931 wprintln!(
932 writer,
933 "{},{},{},{},{},{},{}",
934 csv_escape(&r.file),
935 r.status,
936 r.total_pages,
937 r.empty_pages,
938 r.valid_pages,
939 r.invalid_pages,
940 r.lsn_mismatches
941 )?;
942 }
943 } else {
944 wprintln!(
945 writer,
946 "Auditing {} ({} files)...\n",
947 opts.datadir,
948 total_files
949 )?;
950
951 for r in &results {
952 let status_colored = match r.status.as_str() {
953 "PASS" => "PASS".green().to_string(),
954 "FAIL" => "FAIL".red().to_string(),
955 _ => "ERROR".yellow().to_string(),
956 };
957
958 if r.status == "error" {
959 wprintln!(
960 writer,
961 " {:<40} {} {}",
962 r.file,
963 status_colored,
964 r.error.as_deref().unwrap_or("unknown error")
965 )?;
966 } else if r.invalid_pages > 0 {
967 wprintln!(
968 writer,
969 " {:<40} {} {} pages, {} corrupt",
970 r.file,
971 status_colored,
972 r.total_pages,
973 r.invalid_pages
974 )?;
975 } else {
976 wprintln!(
977 writer,
978 " {:<40} {} {} pages",
979 r.file,
980 status_colored,
981 r.total_pages
982 )?;
983 }
984
985 if opts.verbose && !r.corrupt_pages.is_empty() {
986 wprintln!(writer, " Corrupt pages: {:?}", r.corrupt_pages)?;
987 }
988 }
989
990 wprintln!(writer)?;
991 wprintln!(writer, "Summary:")?;
992 wprintln!(
993 writer,
994 " Files: {} ({} passed, {} failed{})",
995 total_files,
996 files_passed,
997 files_failed,
998 if files_error > 0 {
999 format!(", {} error", files_error)
1000 } else {
1001 String::new()
1002 }
1003 )?;
1004 wprintln!(
1005 writer,
1006 " Pages: {} total, {} corrupt",
1007 total_pages,
1008 corrupt_pages
1009 )?;
1010 wprintln!(writer, " Integrity: {:.2}%", integrity_pct)?;
1011 }
1012
1013 if corrupt_pages > 0 {
1014 return Err(IdbError::Parse(format!(
1015 "{} corrupt pages found across {} files",
1016 corrupt_pages, files_failed
1017 )));
1018 }
1019
1020 Ok(())
1021}
1022
1023fn execute_health(
1028 opts: &AuditOptions,
1029 ibd_files: &[std::path::PathBuf],
1030 datadir: &Path,
1031 writer: &mut dyn Write,
1032) -> Result<(), IdbError> {
1033 let start = Instant::now();
1034
1035 let pb = if !opts.json && !opts.csv && !opts.prometheus {
1036 Some(create_progress_bar(ibd_files.len() as u64, "files"))
1037 } else {
1038 None
1039 };
1040
1041 let page_size = opts.page_size;
1042 let keyring = opts.keyring.clone();
1043 let use_mmap = opts.mmap;
1044
1045 let do_bloat = opts.bloat || opts.max_bloat_grade.is_some();
1046
1047 let mut results: Vec<FileHealthResult> = ibd_files
1048 .par_iter()
1049 .map(|path| {
1050 let r = audit_file_health(path, datadir, page_size, &keyring, use_mmap, do_bloat);
1051 if let Some(ref pb) = pb {
1052 pb.inc(1);
1053 }
1054 r
1055 })
1056 .collect();
1057
1058 if let Some(ref pb) = pb {
1059 pb.finish_and_clear();
1060 }
1061
1062 let total_files = results.len();
1064 let total_index_pages: u64 = results.iter().map(|r| r.total_index_pages).sum();
1065
1066 let valid_results: Vec<&FileHealthResult> =
1067 results.iter().filter(|r| r.error.is_none()).collect();
1068 let n = valid_results.len() as f64;
1069
1070 let avg_fill = if n > 0.0 {
1071 valid_results.iter().map(|r| r.avg_fill_factor).sum::<f64>() / n
1072 } else {
1073 0.0
1074 };
1075 let avg_frag = if n > 0.0 {
1076 valid_results
1077 .iter()
1078 .map(|r| r.avg_fragmentation)
1079 .sum::<f64>()
1080 / n
1081 } else {
1082 0.0
1083 };
1084 let avg_garbage = if n > 0.0 {
1085 valid_results
1086 .iter()
1087 .map(|r| r.avg_garbage_ratio)
1088 .sum::<f64>()
1089 / n
1090 } else {
1091 0.0
1092 };
1093
1094 if opts.prometheus {
1096 let duration_secs = start.elapsed().as_secs_f64();
1097 print_prometheus_health(
1098 writer,
1099 &HealthPrometheusParams {
1100 datadir: &opts.datadir,
1101 results: &results,
1102 total_files,
1103 total_index_pages,
1104 avg_fill,
1105 avg_frag,
1106 avg_garbage,
1107 duration_secs,
1108 },
1109 )?;
1110 return Ok(());
1111 }
1112
1113 if let Some(min_ff) = opts.min_fill_factor {
1115 let threshold = min_ff / 100.0;
1116 results.retain(|r| r.error.is_some() || r.avg_fill_factor < threshold);
1117 }
1118 if let Some(max_frag) = opts.max_fragmentation {
1119 let threshold = max_frag / 100.0;
1120 results.retain(|r| r.error.is_some() || r.avg_fragmentation > threshold);
1121 }
1122 let dir_worst_bloat = if do_bloat {
1124 results
1125 .iter()
1126 .filter_map(|r| r.worst_bloat_grade.as_ref())
1127 .max_by_key(|g| bloat_grade_ord(g).unwrap_or(0))
1128 .cloned()
1129 } else {
1130 None
1131 };
1132
1133 if let Some(ref grade_str) = opts.max_bloat_grade {
1134 let threshold = bloat_grade_ord(grade_str).ok_or_else(|| {
1135 crate::IdbError::Argument(format!(
1136 "Invalid bloat grade '{}': must be one of A, B, C, D, F",
1137 grade_str
1138 ))
1139 })?;
1140 results.retain(|r| {
1141 r.error.is_some()
1142 || r.worst_bloat_grade
1143 .as_ref()
1144 .and_then(|g| bloat_grade_ord(g))
1145 .map(|ord| ord >= threshold)
1146 .unwrap_or(false)
1147 });
1148 }
1149
1150 results.sort_by(|a, b| {
1152 b.avg_fragmentation
1153 .partial_cmp(&a.avg_fragmentation)
1154 .unwrap_or(std::cmp::Ordering::Equal)
1155 });
1156
1157 if opts.json {
1158 let report = HealthAuditReport {
1159 datadir: opts.datadir.clone(),
1160 tablespaces: results,
1161 summary: DirectoryHealthSummary {
1162 total_files,
1163 total_index_pages,
1164 avg_fill_factor: round2(avg_fill),
1165 avg_fragmentation: round2(avg_frag),
1166 avg_garbage_ratio: round2(avg_garbage),
1167 worst_bloat_grade: dir_worst_bloat,
1168 },
1169 };
1170 let json = serde_json::to_string_pretty(&report)
1171 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
1172 wprintln!(writer, "{}", json)?;
1173 } else if opts.csv {
1174 if do_bloat {
1175 wprintln!(
1176 writer,
1177 "file,avg_fill_factor,avg_fragmentation,avg_garbage_ratio,index_count,total_index_pages,worst_bloat_grade,worst_bloat_score"
1178 )?;
1179 } else {
1180 wprintln!(
1181 writer,
1182 "file,avg_fill_factor,avg_fragmentation,avg_garbage_ratio,index_count,total_index_pages"
1183 )?;
1184 }
1185 for r in &results {
1186 if r.error.is_some() {
1187 continue;
1188 }
1189 if do_bloat {
1190 wprintln!(
1191 writer,
1192 "{},{:.1},{:.1},{:.1},{},{},{},{}",
1193 csv_escape(&r.file),
1194 r.avg_fill_factor * 100.0,
1195 r.avg_fragmentation * 100.0,
1196 r.avg_garbage_ratio * 100.0,
1197 r.index_count,
1198 r.total_index_pages,
1199 r.worst_bloat_grade.as_deref().unwrap_or(""),
1200 r.worst_bloat_score
1201 .map(|s| format!("{:.3}", s))
1202 .unwrap_or_default()
1203 )?;
1204 } else {
1205 wprintln!(
1206 writer,
1207 "{},{:.1},{:.1},{:.1},{},{}",
1208 csv_escape(&r.file),
1209 r.avg_fill_factor * 100.0,
1210 r.avg_fragmentation * 100.0,
1211 r.avg_garbage_ratio * 100.0,
1212 r.index_count,
1213 r.total_index_pages
1214 )?;
1215 }
1216 }
1217 } else {
1218 wprintln!(writer, "Directory Health: {}\n", opts.datadir)?;
1219 if do_bloat {
1220 wprintln!(
1221 writer,
1222 " {:<40} {:>6} {:>6} {:>6} {:>8} {:>6} {:>6}",
1223 "File",
1224 "Fill%",
1225 "Frag%",
1226 "Garb%",
1227 "Indexes",
1228 "Pages",
1229 "Bloat"
1230 )?;
1231 } else {
1232 wprintln!(
1233 writer,
1234 " {:<40} {:>6} {:>6} {:>6} {:>8} {:>6}",
1235 "File",
1236 "Fill%",
1237 "Frag%",
1238 "Garb%",
1239 "Indexes",
1240 "Pages"
1241 )?;
1242 }
1243
1244 for r in &results {
1245 if let Some(ref err) = r.error {
1246 wprintln!(
1247 writer,
1248 " {:<40} {}",
1249 r.file,
1250 format!("ERROR: {}", err).yellow()
1251 )?;
1252 } else if do_bloat {
1253 let grade_str = r.worst_bloat_grade.as_deref().unwrap_or("-");
1254 let grade_colored = match grade_str {
1255 "A" | "B" => grade_str.green().to_string(),
1256 "C" => grade_str.yellow().to_string(),
1257 "D" | "F" => grade_str.red().to_string(),
1258 _ => grade_str.to_string(),
1259 };
1260 wprintln!(
1261 writer,
1262 " {:<40} {:>5.1} {:>5.1} {:>5.1} {:>7} {:>5} {:>5}",
1263 r.file,
1264 r.avg_fill_factor * 100.0,
1265 r.avg_fragmentation * 100.0,
1266 r.avg_garbage_ratio * 100.0,
1267 r.index_count,
1268 r.total_index_pages,
1269 grade_colored
1270 )?;
1271 } else {
1272 wprintln!(
1273 writer,
1274 " {:<40} {:>5.1} {:>5.1} {:>5.1} {:>7} {:>5}",
1275 r.file,
1276 r.avg_fill_factor * 100.0,
1277 r.avg_fragmentation * 100.0,
1278 r.avg_garbage_ratio * 100.0,
1279 r.index_count,
1280 r.total_index_pages
1281 )?;
1282 }
1283 }
1284
1285 wprintln!(writer)?;
1286 wprintln!(
1287 writer,
1288 "Summary: {} files, avg fill {:.1}%, avg frag {:.1}%, avg garbage {:.1}%",
1289 total_files,
1290 avg_fill * 100.0,
1291 avg_frag * 100.0,
1292 avg_garbage * 100.0
1293 )?;
1294 }
1295
1296 Ok(())
1297}
1298
1299fn execute_mismatch(
1304 opts: &AuditOptions,
1305 ibd_files: &[std::path::PathBuf],
1306 datadir: &Path,
1307 writer: &mut dyn Write,
1308) -> Result<(), IdbError> {
1309 let pb = if !opts.json && !opts.csv {
1310 Some(create_progress_bar(ibd_files.len() as u64, "files"))
1311 } else {
1312 None
1313 };
1314
1315 let page_size = opts.page_size;
1316 let keyring = opts.keyring.clone();
1317 let use_mmap = opts.mmap;
1318
1319 let all_results: Vec<(Vec<MismatchEntry>, u64)> = ibd_files
1320 .par_iter()
1321 .map(|path| {
1322 let r = audit_file_mismatches(path, datadir, page_size, &keyring, use_mmap);
1323 if let Some(ref pb) = pb {
1324 pb.inc(1);
1325 }
1326 r
1327 })
1328 .collect();
1329
1330 if let Some(ref pb) = pb {
1331 pb.finish_and_clear();
1332 }
1333
1334 let total_files_scanned = ibd_files.len();
1335 let total_pages_scanned: u64 = all_results.iter().map(|(_, count)| count).sum();
1336
1337 let mut mismatches: Vec<MismatchEntry> = all_results
1338 .into_iter()
1339 .flat_map(|(entries, _)| entries)
1340 .collect();
1341 mismatches.sort_by(|a, b| (&a.file, a.page_number).cmp(&(&b.file, b.page_number)));
1342
1343 if opts.json {
1344 let report = MismatchReport {
1345 datadir: opts.datadir.clone(),
1346 mismatches: mismatches.clone(),
1347 total_files_scanned,
1348 total_pages_scanned,
1349 };
1350 let json = serde_json::to_string_pretty(&report)
1351 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
1352 wprintln!(writer, "{}", json)?;
1353 } else if opts.csv {
1354 wprintln!(
1355 writer,
1356 "file,page_number,stored_checksum,calculated_checksum,algorithm"
1357 )?;
1358 for m in &mismatches {
1359 wprintln!(
1360 writer,
1361 "{},{},{},{},{}",
1362 csv_escape(&m.file),
1363 m.page_number,
1364 m.stored_checksum,
1365 m.calculated_checksum,
1366 m.algorithm
1367 )?;
1368 }
1369 } else if mismatches.is_empty() {
1370 wprintln!(
1371 writer,
1372 "No checksum mismatches found ({} files, {} pages scanned).",
1373 total_files_scanned,
1374 total_pages_scanned
1375 )?;
1376 } else {
1377 wprintln!(
1378 writer,
1379 "{:<40} {:>6} {:>12} {:>12} {:>12}",
1380 "FILE",
1381 "PAGE",
1382 "STORED",
1383 "CALCULATED",
1384 "ALGORITHM"
1385 )?;
1386 for m in &mismatches {
1387 wprintln!(
1388 writer,
1389 "{:<40} {:>6} {:>12} {:>12} {:>12}",
1390 m.file,
1391 m.page_number,
1392 format!("0x{:08X}", m.stored_checksum),
1393 format!("0x{:08X}", m.calculated_checksum),
1394 m.algorithm
1395 )?;
1396 }
1397 }
1398
1399 if !mismatches.is_empty() {
1400 return Err(IdbError::Parse(format!(
1401 "{} checksum mismatches found",
1402 mismatches.len()
1403 )));
1404 }
1405
1406 Ok(())
1407}
1408
1409fn round2(v: f64) -> f64 {
1410 (v * 100.0).round() / 100.0
1411}
1412
1413fn bloat_grade_ord(grade: &str) -> Option<u8> {
1416 match grade {
1417 "A" => Some(0),
1418 "B" => Some(1),
1419 "C" => Some(2),
1420 "D" => Some(3),
1421 "F" => Some(4),
1422 _ => None,
1423 }
1424}
1425
1426struct IntegrityPrometheusParams<'a> {
1431 datadir: &'a str,
1432 results: &'a [FileIntegrityResult],
1433 total_pages: u64,
1434 corrupt_pages: u64,
1435 integrity_pct: f64,
1436 duration_secs: f64,
1437}
1438
1439fn print_prometheus_integrity(
1441 writer: &mut dyn Write,
1442 params: &IntegrityPrometheusParams<'_>,
1443) -> Result<(), IdbError> {
1444 let datadir = params.datadir;
1445 let results = params.results;
1446 let total_pages = params.total_pages;
1447 let corrupt_pages = params.corrupt_pages;
1448 let integrity_pct = params.integrity_pct;
1449 let duration_secs = params.duration_secs;
1450 wprintln!(
1452 writer,
1453 "{}",
1454 prom::help_line("innodb_pages", "Total pages in tablespace")
1455 )?;
1456 wprintln!(writer, "{}", prom::type_line("innodb_pages", "gauge"))?;
1457 for r in results {
1458 if r.status == "error" {
1459 continue;
1460 }
1461 wprintln!(
1462 writer,
1463 "{}",
1464 prom::format_gauge_int(
1465 "innodb_pages",
1466 &[("datadir", datadir), ("file", &r.file)],
1467 r.total_pages
1468 )
1469 )?;
1470 }
1471
1472 wprintln!(
1474 writer,
1475 "{}",
1476 prom::help_line(
1477 "innodb_corrupt_pages",
1478 "Number of corrupt pages in tablespace"
1479 )
1480 )?;
1481 wprintln!(
1482 writer,
1483 "{}",
1484 prom::type_line("innodb_corrupt_pages", "gauge")
1485 )?;
1486 for r in results {
1487 if r.status == "error" {
1488 continue;
1489 }
1490 wprintln!(
1491 writer,
1492 "{}",
1493 prom::format_gauge_int(
1494 "innodb_corrupt_pages",
1495 &[("datadir", datadir), ("file", &r.file)],
1496 r.invalid_pages
1497 )
1498 )?;
1499 }
1500
1501 wprintln!(
1503 writer,
1504 "{}",
1505 prom::help_line("innodb_empty_pages", "Number of empty pages in tablespace")
1506 )?;
1507 wprintln!(writer, "{}", prom::type_line("innodb_empty_pages", "gauge"))?;
1508 for r in results {
1509 if r.status == "error" {
1510 continue;
1511 }
1512 wprintln!(
1513 writer,
1514 "{}",
1515 prom::format_gauge_int(
1516 "innodb_empty_pages",
1517 &[("datadir", datadir), ("file", &r.file)],
1518 r.empty_pages
1519 )
1520 )?;
1521 }
1522
1523 wprintln!(
1525 writer,
1526 "{}",
1527 prom::help_line(
1528 "innodb_audit_integrity_pct",
1529 "Directory-wide integrity percentage"
1530 )
1531 )?;
1532 wprintln!(
1533 writer,
1534 "{}",
1535 prom::type_line("innodb_audit_integrity_pct", "gauge")
1536 )?;
1537 wprintln!(
1538 writer,
1539 "{}",
1540 prom::format_gauge(
1541 "innodb_audit_integrity_pct",
1542 &[("datadir", datadir)],
1543 integrity_pct
1544 )
1545 )?;
1546
1547 wprintln!(
1549 writer,
1550 "{}",
1551 prom::help_line(
1552 "innodb_audit_pages",
1553 "Total pages scanned across data directory"
1554 )
1555 )?;
1556 wprintln!(writer, "{}", prom::type_line("innodb_audit_pages", "gauge"))?;
1557 wprintln!(
1558 writer,
1559 "{}",
1560 prom::format_gauge_int("innodb_audit_pages", &[("datadir", datadir)], total_pages)
1561 )?;
1562
1563 wprintln!(
1565 writer,
1566 "{}",
1567 prom::help_line(
1568 "innodb_audit_corrupt_pages",
1569 "Total corrupt pages across data directory"
1570 )
1571 )?;
1572 wprintln!(
1573 writer,
1574 "{}",
1575 prom::type_line("innodb_audit_corrupt_pages", "gauge")
1576 )?;
1577 wprintln!(
1578 writer,
1579 "{}",
1580 prom::format_gauge_int(
1581 "innodb_audit_corrupt_pages",
1582 &[("datadir", datadir)],
1583 corrupt_pages
1584 )
1585 )?;
1586
1587 wprintln!(
1589 writer,
1590 "{}",
1591 prom::help_line(
1592 "innodb_scan_duration_seconds",
1593 "Time spent scanning the data directory"
1594 )
1595 )?;
1596 wprintln!(
1597 writer,
1598 "{}",
1599 prom::type_line("innodb_scan_duration_seconds", "gauge")
1600 )?;
1601 wprintln!(
1602 writer,
1603 "{}",
1604 prom::format_gauge(
1605 "innodb_scan_duration_seconds",
1606 &[("datadir", datadir)],
1607 duration_secs
1608 )
1609 )?;
1610
1611 Ok(())
1612}
1613
1614struct HealthPrometheusParams<'a> {
1615 datadir: &'a str,
1616 results: &'a [FileHealthResult],
1617 total_files: usize,
1618 total_index_pages: u64,
1619 avg_fill: f64,
1620 avg_frag: f64,
1621 avg_garbage: f64,
1622 duration_secs: f64,
1623}
1624
1625fn print_prometheus_health(
1627 writer: &mut dyn Write,
1628 params: &HealthPrometheusParams<'_>,
1629) -> Result<(), IdbError> {
1630 let datadir = params.datadir;
1631 let results = params.results;
1632 let total_files = params.total_files;
1633 let total_index_pages = params.total_index_pages;
1634 let avg_fill = params.avg_fill;
1635 let avg_frag = params.avg_frag;
1636 let avg_garbage = params.avg_garbage;
1637 let duration_secs = params.duration_secs;
1638 wprintln!(
1640 writer,
1641 "{}",
1642 prom::help_line(
1643 "innodb_fill_factor",
1644 "Average B+Tree fill factor for tablespace"
1645 )
1646 )?;
1647 wprintln!(writer, "{}", prom::type_line("innodb_fill_factor", "gauge"))?;
1648 for r in results {
1649 if r.error.is_some() {
1650 continue;
1651 }
1652 wprintln!(
1653 writer,
1654 "{}",
1655 prom::format_gauge(
1656 "innodb_fill_factor",
1657 &[("datadir", datadir), ("file", &r.file)],
1658 r.avg_fill_factor
1659 )
1660 )?;
1661 }
1662
1663 wprintln!(
1665 writer,
1666 "{}",
1667 prom::help_line(
1668 "innodb_fragmentation_ratio",
1669 "Average fragmentation ratio for tablespace"
1670 )
1671 )?;
1672 wprintln!(
1673 writer,
1674 "{}",
1675 prom::type_line("innodb_fragmentation_ratio", "gauge")
1676 )?;
1677 for r in results {
1678 if r.error.is_some() {
1679 continue;
1680 }
1681 wprintln!(
1682 writer,
1683 "{}",
1684 prom::format_gauge(
1685 "innodb_fragmentation_ratio",
1686 &[("datadir", datadir), ("file", &r.file)],
1687 r.avg_fragmentation
1688 )
1689 )?;
1690 }
1691
1692 wprintln!(
1694 writer,
1695 "{}",
1696 prom::help_line(
1697 "innodb_garbage_ratio",
1698 "Average garbage ratio for tablespace"
1699 )
1700 )?;
1701 wprintln!(
1702 writer,
1703 "{}",
1704 prom::type_line("innodb_garbage_ratio", "gauge")
1705 )?;
1706 for r in results {
1707 if r.error.is_some() {
1708 continue;
1709 }
1710 wprintln!(
1711 writer,
1712 "{}",
1713 prom::format_gauge(
1714 "innodb_garbage_ratio",
1715 &[("datadir", datadir), ("file", &r.file)],
1716 r.avg_garbage_ratio
1717 )
1718 )?;
1719 }
1720
1721 wprintln!(
1723 writer,
1724 "{}",
1725 prom::help_line("innodb_index_pages", "Total INDEX pages in tablespace")
1726 )?;
1727 wprintln!(writer, "{}", prom::type_line("innodb_index_pages", "gauge"))?;
1728 for r in results {
1729 if r.error.is_some() {
1730 continue;
1731 }
1732 wprintln!(
1733 writer,
1734 "{}",
1735 prom::format_gauge_int(
1736 "innodb_index_pages",
1737 &[("datadir", datadir), ("file", &r.file)],
1738 r.total_index_pages
1739 )
1740 )?;
1741 }
1742
1743 let has_bloat = results.iter().any(|r| r.worst_bloat_score.is_some());
1745 if has_bloat {
1746 wprintln!(
1747 writer,
1748 "{}",
1749 prom::help_line(
1750 "innodb_bloat_score",
1751 "Worst bloat score across indexes in tablespace (0-1)"
1752 )
1753 )?;
1754 wprintln!(writer, "{}", prom::type_line("innodb_bloat_score", "gauge"))?;
1755 for r in results {
1756 if let Some(score) = r.worst_bloat_score {
1757 wprintln!(
1758 writer,
1759 "{}",
1760 prom::format_gauge(
1761 "innodb_bloat_score",
1762 &[
1763 ("datadir", datadir),
1764 ("file", &r.file),
1765 ("grade", r.worst_bloat_grade.as_deref().unwrap_or(""))
1766 ],
1767 score
1768 )
1769 )?;
1770 }
1771 }
1772 }
1773
1774 wprintln!(
1776 writer,
1777 "{}",
1778 prom::help_line("innodb_audit_files", "Total tablespace files scanned")
1779 )?;
1780 wprintln!(writer, "{}", prom::type_line("innodb_audit_files", "gauge"))?;
1781 wprintln!(
1782 writer,
1783 "{}",
1784 prom::format_gauge_int(
1785 "innodb_audit_files",
1786 &[("datadir", datadir)],
1787 total_files as u64
1788 )
1789 )?;
1790
1791 wprintln!(
1792 writer,
1793 "{}",
1794 prom::help_line(
1795 "innodb_audit_index_pages",
1796 "Total INDEX pages across data directory"
1797 )
1798 )?;
1799 wprintln!(
1800 writer,
1801 "{}",
1802 prom::type_line("innodb_audit_index_pages", "gauge")
1803 )?;
1804 wprintln!(
1805 writer,
1806 "{}",
1807 prom::format_gauge_int(
1808 "innodb_audit_index_pages",
1809 &[("datadir", datadir)],
1810 total_index_pages
1811 )
1812 )?;
1813
1814 wprintln!(
1815 writer,
1816 "{}",
1817 prom::help_line(
1818 "innodb_audit_avg_fill_factor",
1819 "Directory-wide average fill factor"
1820 )
1821 )?;
1822 wprintln!(
1823 writer,
1824 "{}",
1825 prom::type_line("innodb_audit_avg_fill_factor", "gauge")
1826 )?;
1827 wprintln!(
1828 writer,
1829 "{}",
1830 prom::format_gauge(
1831 "innodb_audit_avg_fill_factor",
1832 &[("datadir", datadir)],
1833 avg_fill
1834 )
1835 )?;
1836
1837 wprintln!(
1838 writer,
1839 "{}",
1840 prom::help_line(
1841 "innodb_audit_avg_fragmentation",
1842 "Directory-wide average fragmentation"
1843 )
1844 )?;
1845 wprintln!(
1846 writer,
1847 "{}",
1848 prom::type_line("innodb_audit_avg_fragmentation", "gauge")
1849 )?;
1850 wprintln!(
1851 writer,
1852 "{}",
1853 prom::format_gauge(
1854 "innodb_audit_avg_fragmentation",
1855 &[("datadir", datadir)],
1856 avg_frag
1857 )
1858 )?;
1859
1860 wprintln!(
1861 writer,
1862 "{}",
1863 prom::help_line(
1864 "innodb_audit_avg_garbage_ratio",
1865 "Directory-wide average garbage ratio"
1866 )
1867 )?;
1868 wprintln!(
1869 writer,
1870 "{}",
1871 prom::type_line("innodb_audit_avg_garbage_ratio", "gauge")
1872 )?;
1873 wprintln!(
1874 writer,
1875 "{}",
1876 prom::format_gauge(
1877 "innodb_audit_avg_garbage_ratio",
1878 &[("datadir", datadir)],
1879 avg_garbage
1880 )
1881 )?;
1882
1883 wprintln!(
1885 writer,
1886 "{}",
1887 prom::help_line(
1888 "innodb_scan_duration_seconds",
1889 "Time spent scanning the data directory"
1890 )
1891 )?;
1892 wprintln!(
1893 writer,
1894 "{}",
1895 prom::type_line("innodb_scan_duration_seconds", "gauge")
1896 )?;
1897 wprintln!(
1898 writer,
1899 "{}",
1900 prom::format_gauge(
1901 "innodb_scan_duration_seconds",
1902 &[("datadir", datadir)],
1903 duration_secs
1904 )
1905 )?;
1906
1907 Ok(())
1908}