Skip to main content

dm_database_sqllog2db/
error.rs

1//! Structured errors, severity and bounded run statistics.
2
3use std::fmt;
4use std::io;
5use std::path::PathBuf;
6use thiserror::Error;
7
8pub type Result<T> = std::result::Result<T, Error>;
9
10/// Error severity for fatal/non-fatal classification.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ErrorSeverity {
13    Warning,
14    Error,
15    Critical,
16}
17
18impl fmt::Display for ErrorSeverity {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            Self::Warning => write!(f, "WARNING"),
22            Self::Error => write!(f, "ERROR"),
23            Self::Critical => write!(f, "CRITICAL"),
24        }
25    }
26}
27
28#[derive(Debug, Error)]
29pub enum Error {
30    #[error("Configuration error: {0}")]
31    Config(#[from] ConfigError),
32
33    #[error("File error: {0}")]
34    File(#[from] FileError),
35
36    #[error("SQL log parser error: {0}")]
37    Parser(#[from] ParserError),
38
39    #[error("Export error: {0}")]
40    Export(#[from] ExportError),
41
42    #[error("IO error: {0}")]
43    Io(#[from] io::Error),
44
45    #[error("Interrupted by user")]
46    Interrupted,
47}
48
49impl Error {
50    #[must_use]
51    pub fn is_fatal(&self) -> bool {
52        match self {
53            Error::Config(_) | Error::Io(_) | Error::Interrupted => true,
54            Error::File(e) => matches!(
55                e,
56                FileError::AlreadyExists { .. } | FileError::CreateDirectoryFailed { .. }
57            ),
58            Error::Parser(e) => matches!(e, ParserError::ReadDirFailed { .. }),
59            Error::Export(e) => matches!(e, ExportError::Fatal { .. }),
60        }
61    }
62
63    #[must_use]
64    pub fn severity(&self) -> ErrorSeverity {
65        match self {
66            Error::Config(_) | Error::Io(_) | Error::Interrupted => ErrorSeverity::Critical,
67            Error::File(e) => match e {
68                FileError::WriteFailed { .. } => ErrorSeverity::Error,
69                FileError::AlreadyExists { .. } | FileError::CreateDirectoryFailed { .. } => {
70                    ErrorSeverity::Critical
71                }
72            },
73            Error::Parser(_) => ErrorSeverity::Warning,
74            Error::Export(e) => match e {
75                ExportError::WriteFailed { .. } => ErrorSeverity::Error,
76                ExportError::Fatal { .. } => ErrorSeverity::Critical,
77            },
78        }
79    }
80
81    #[must_use]
82    pub fn suggestion(&self) -> &str {
83        match self {
84            Error::Config(e) => match e {
85                ConfigError::NotFound(_) => {
86                    "Create a config file with 'sqllog2db init' or check the file path."
87                }
88                ConfigError::ParseFailed { .. } => "Check TOML syntax in the configuration file.",
89                ConfigError::InvalidLogLevel { .. } => {
90                    "Valid log levels: error, warn, info, debug, trace."
91                }
92                ConfigError::InvalidValue { .. } => {
93                    "Check the field value in the configuration file."
94                }
95                ConfigError::NoExporters => {
96                    "Enable at least one exporter: [exporter.parquet] or [exporter.csv]."
97                }
98            },
99            Error::File(e) => match e {
100                FileError::AlreadyExists { .. } => {
101                    "Use --force to overwrite, or choose a different output path."
102                }
103                FileError::WriteFailed { .. } => "Check disk space and file permissions.",
104                FileError::CreateDirectoryFailed { .. } => "Check parent directory permissions.",
105            },
106            Error::Parser(e) => match e {
107                ParserError::PathNotFound { .. } => {
108                    "Verify the log file exists at the specified path."
109                }
110                ParserError::InvalidPath { .. } => "Check the path format or try an absolute path.",
111                ParserError::ReadDirFailed { .. } => "Check directory permissions.",
112                ParserError::NoFilesFound { .. } => {
113                    "Verify the glob/path entries exist; ensure patterns match .log files in the current directory."
114                }
115            },
116            Error::Export(e) => match e {
117                ExportError::WriteFailed { .. } => {
118                    "Check disk space and output directory permissions."
119                }
120                ExportError::Fatal { .. } => "Check the output file and export configuration.",
121            },
122            Error::Io(_) => "Check filesystem permissions and disk space.",
123            Error::Interrupted => "Run was interrupted by user.",
124        }
125    }
126}
127
128#[derive(Debug, Error)]
129pub enum ConfigError {
130    #[error("Configuration file not found: {0}")]
131    NotFound(PathBuf),
132
133    #[error("Failed to parse configuration file {path}: {reason}")]
134    ParseFailed { path: PathBuf, reason: String },
135
136    #[error("Invalid log level '{level}', valid values: {}", valid_levels.join(", "))]
137    InvalidLogLevel {
138        level: String,
139        valid_levels: Vec<String>,
140    },
141
142    #[error("Invalid configuration value {field} = '{value}': {reason}")]
143    InvalidValue {
144        field: String,
145        value: String,
146        reason: String,
147    },
148
149    #[error("At least one exporter must be configured (parquet/csv)")]
150    NoExporters,
151}
152
153#[derive(Debug, Error)]
154pub enum FileError {
155    #[error("File already exists: {path} (set overwrite=true to replace)")]
156    AlreadyExists { path: PathBuf },
157
158    #[error("Failed to write file {path}: {reason}")]
159    WriteFailed { path: PathBuf, reason: String },
160
161    #[error("Failed to create directory {path}: {reason}")]
162    CreateDirectoryFailed { path: PathBuf, reason: String },
163}
164
165#[derive(Debug, Error)]
166pub enum ParserError {
167    #[error("Path not found: {}", path.display())]
168    PathNotFound { path: PathBuf },
169
170    #[error("Invalid path {}: {reason}{}", path.display(), line_number.map_or_else(String::new, |n| format!(" (line {n})")))]
171    InvalidPath {
172        path: PathBuf,
173        reason: String,
174        line_number: Option<u64>,
175    },
176
177    #[error("Failed to read directory {}: {reason}", path.display())]
178    ReadDirFailed { path: PathBuf, reason: String },
179
180    #[error("No log files found matching inputs: {inputs:?}")]
181    NoFilesFound { inputs: Vec<String> },
182}
183
184#[derive(Debug, Error)]
185pub enum ExportError {
186    /// 文件写入失败(CSV、错误日志等所有文件型导出器通用)
187    #[error("Write failed {path}: {reason}")]
188    WriteFailed { path: PathBuf, reason: String },
189
190    /// 导出过程中不可恢复的错误
191    #[error("Export failed: {reason}")]
192    Fatal { reason: String },
193}
194
195#[cfg(test)]
196#[path = "../tests/unit/error.rs"]
197mod tests;
198
199use std::collections::HashMap;
200
201/// 解析错误的分类枚举。
202#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
203pub enum ErrorKind {
204    EncodingError,
205    FieldMissing,
206    ParseFailed,
207}
208
209impl ErrorKind {
210    #[must_use]
211    pub fn kind_display(self) -> &'static str {
212        match self {
213            Self::EncodingError => "encoding_error",
214            Self::FieldMissing => "field_missing",
215            Self::ParseFailed => "parse_failed",
216        }
217    }
218}
219
220/// 单条解析错误的详细记录。
221#[derive(Debug, Clone)]
222pub struct ParseErrorRecord {
223    pub line_number: u64,
224    pub raw_truncated: String,
225    pub kind: ErrorKind,
226}
227
228/// Accumulated error statistics for a processing run.
229#[derive(Debug, Default, Clone)]
230pub struct ErrorStats {
231    pub total_errors: usize,
232    pub parse_errors: usize,
233    pub export_errors: usize,
234    pub fatal_error: Option<String>,
235    pub by_type: HashMap<ErrorKind, u64>,
236    pub filtered_out: u64,
237    pub parse_error_records: Vec<ParseErrorRecord>,
238    pub records_exported: usize, // 累计成功导出的记录数
239}
240
241impl ErrorStats {
242    #[must_use]
243    pub fn has_errors(&self) -> bool {
244        self.total_errors > 0
245    }
246
247    #[must_use]
248    pub fn has_fatal(&self) -> bool {
249        self.fatal_error.is_some()
250    }
251
252    pub fn add_parse_error(&mut self) {
253        self.total_errors += 1;
254        self.parse_errors += 1;
255    }
256
257    pub fn add_export_error(&mut self) {
258        self.total_errors += 1;
259        self.export_errors += 1;
260    }
261
262    pub fn set_fatal(&mut self, msg: String) {
263        self.fatal_error = Some(msg);
264    }
265
266    pub fn merge(&mut self, other: &ErrorStats) {
267        const MAX_RECORDS: usize = 10_000;
268        self.total_errors += other.total_errors;
269        self.parse_errors += other.parse_errors;
270        self.export_errors += other.export_errors;
271        if self.fatal_error.is_none() && other.fatal_error.is_some() {
272            self.fatal_error.clone_from(&other.fatal_error);
273        }
274        for (kind, count) in &other.by_type {
275            *self.by_type.entry(*kind).or_insert(0) += count;
276        }
277        self.filtered_out += other.filtered_out;
278        self.records_exported += other.records_exported;
279        let remaining_cap = MAX_RECORDS.saturating_sub(self.parse_error_records.len());
280        if remaining_cap > 0 {
281            self.parse_error_records.extend(
282                other
283                    .parse_error_records
284                    .iter()
285                    .take(remaining_cap)
286                    .cloned(),
287            );
288        }
289    }
290}