dm_database_sqllog2db/
error_logger.rs1use crate::error::{Error, ExportError, Result};
3use log::{debug, info};
4use std::collections::HashMap;
5use std::fs::{File, OpenOptions};
6use std::io::{BufWriter, Write};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug)]
11pub struct ParseErrorRecord {
12 pub file_path: String,
14 pub error_message: String,
16 pub raw_content: Option<String>,
18 pub line_number: Option<usize>,
20}
21
22#[derive(Debug, Default)]
24pub struct ErrorMetrics {
25 pub total: usize,
27 pub by_category: HashMap<String, usize>,
29 pub parse_variants: HashMap<String, usize>,
31}
32
33impl ErrorMetrics {
34 fn incr_category(&mut self, cat: &str) {
35 *self.by_category.entry(cat.to_string()).or_insert(0) += 1;
36 self.total += 1;
37 }
38
39 fn incr_parse_variant(&mut self, variant: &str) {
40 *self.parse_variants.entry(variant.to_string()).or_insert(0) += 1;
41 }
42}
43
44#[derive(Debug)]
45pub struct ErrorLogger {
46 writer: BufWriter<File>,
47 path: String,
48 count: usize,
49 metrics: ErrorMetrics,
50}
51
52impl ErrorLogger {
53 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
55 let path_ref = path.as_ref();
56 let path_str = path_ref.to_string_lossy().to_string();
57
58 if let Some(parent) = path_ref.parent().filter(|p| !p.exists()) {
60 std::fs::create_dir_all(parent).map_err(|e| {
61 Error::Export(ExportError::FileCreateFailed {
62 path: parent.to_path_buf(),
63 reason: e.to_string(),
64 })
65 })?;
66 }
67
68 let file = OpenOptions::new()
70 .create(true)
71 .append(true)
72 .open(path_ref)
73 .map_err(|e| {
74 Error::Export(ExportError::FileCreateFailed {
75 path: path_ref.to_path_buf(),
76 reason: e.to_string(),
77 })
78 })?;
79
80 info!("Error logger initialized: {path_str}");
81
82 Ok(Self {
83 writer: BufWriter::new(file),
84 path: path_str,
85 count: 0,
86 metrics: ErrorMetrics::default(),
87 })
88 }
89
90 pub fn log_error(&mut self, record: &ParseErrorRecord) -> Result<()> {
92 let raw = record.raw_content.clone().unwrap_or_default();
94 let line_no = record
95 .line_number
96 .map(|n| n.to_string())
97 .unwrap_or_default();
98 let line = format!(
99 "{} | {} | {} | {}",
100 record.file_path,
101 record.error_message,
102 raw.replace('\n', "\\n"),
103 line_no
104 );
105
106 writeln!(self.writer, "{line}").map_err(|e| {
107 Error::Export(ExportError::FileWriteFailed {
108 path: PathBuf::from(&self.path),
109 reason: e.to_string(),
110 })
111 })?;
112
113 self.count += 1;
114 self.metrics.incr_category("parse");
116 Ok(())
117 }
118
119 pub fn log_parse_error(
121 &mut self,
122 file_path: &str,
123 error: &dm_database_parser_sqllog::ParseError,
124 ) -> Result<()> {
125 let record = ParseErrorRecord {
126 file_path: file_path.to_string(),
127 error_message: format!("{error:?}"),
128 raw_content: None, line_number: None,
130 };
131 let variant = format!("{error:?}");
133 self.metrics.incr_parse_variant(&variant);
134 self.log_error(&record)
135 }
136
137 pub fn flush(&mut self) -> Result<()> {
140 self.writer.flush().map_err(|e| {
141 Error::Export(ExportError::FileWriteFailed {
142 path: PathBuf::from(&self.path),
143 reason: format!("Flush failed: {e}"),
144 })
145 })?;
146 Ok(())
147 }
148
149 pub fn finalize(&mut self) -> Result<()> {
151 self.flush()?;
152
153 if self.count > 0 {
154 info!(
155 "Error log written: {} ({} records, categories: {:?})",
156 self.path, self.count, self.metrics.by_category
157 );
158 } else {
159 debug!("No error records to write");
160 }
161 Ok(())
162 }
163}