use crate::error::{Error, ExportError, Result};
use log::{debug, info};
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct ParseErrorRecord {
pub file_path: String,
pub error_message: String,
pub raw_content: Option<String>,
pub line_number: Option<usize>,
}
#[derive(Debug, Default)]
pub struct ErrorMetrics {
pub total: usize,
pub by_category: HashMap<String, usize>,
pub parse_variants: HashMap<String, usize>,
}
impl ErrorMetrics {
fn incr_category(&mut self, cat: &str) {
*self.by_category.entry(cat.to_string()).or_insert(0) += 1;
self.total += 1;
}
fn incr_parse_variant(&mut self, variant: &str) {
*self.parse_variants.entry(variant.to_string()).or_insert(0) += 1;
}
}
#[derive(Debug)]
pub struct ErrorLogger {
writer: BufWriter<File>,
path: String,
count: usize,
metrics: ErrorMetrics,
}
impl ErrorLogger {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
let path_ref = path.as_ref();
let path_str = path_ref.to_string_lossy().to_string();
if let Some(parent) = path_ref.parent().filter(|p| !p.exists()) {
std::fs::create_dir_all(parent).map_err(|e| {
Error::Export(ExportError::FileCreateFailed {
path: parent.to_path_buf(),
reason: e.to_string(),
})
})?;
}
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path_ref)
.map_err(|e| {
Error::Export(ExportError::FileCreateFailed {
path: path_ref.to_path_buf(),
reason: e.to_string(),
})
})?;
info!("Error logger initialized: {path_str}");
Ok(Self {
writer: BufWriter::new(file),
path: path_str,
count: 0,
metrics: ErrorMetrics::default(),
})
}
pub fn log_error(&mut self, record: &ParseErrorRecord) -> Result<()> {
let raw = record.raw_content.clone().unwrap_or_default();
let line_no = record
.line_number
.map(|n| n.to_string())
.unwrap_or_default();
let line = format!(
"{} | {} | {} | {}",
record.file_path,
record.error_message,
raw.replace('\n', "\\n"),
line_no
);
writeln!(self.writer, "{line}").map_err(|e| {
Error::Export(ExportError::FileWriteFailed {
path: PathBuf::from(&self.path),
reason: e.to_string(),
})
})?;
self.count += 1;
self.metrics.incr_category("parse");
Ok(())
}
pub fn log_parse_error(
&mut self,
file_path: &str,
error: &dm_database_parser_sqllog::ParseError,
) -> Result<()> {
let record = ParseErrorRecord {
file_path: file_path.to_string(),
error_message: format!("{error:?}"),
raw_content: None, line_number: None,
};
let variant = format!("{error:?}");
self.metrics.incr_parse_variant(&variant);
self.log_error(&record)
}
pub fn flush(&mut self) -> Result<()> {
self.writer.flush().map_err(|e| {
Error::Export(ExportError::FileWriteFailed {
path: PathBuf::from(&self.path),
reason: format!("Flush failed: {e}"),
})
})?;
Ok(())
}
pub fn finalize(&mut self) -> Result<()> {
self.flush()?;
if self.count > 0 {
info!(
"Error log written: {} ({} records, categories: {:?})",
self.path, self.count, self.metrics.by_category
);
} else {
debug!("No error records to write");
}
Ok(())
}
}