dm_database_sqllog2db/
config.rs1use crate::constants::LOG_LEVELS;
2use crate::error::{ConfigError, Error, Result};
3pub use crate::features::FeaturesConfig;
4#[cfg(feature = "filters")]
5pub use crate::features::FiltersFeature;
6use serde::Deserialize;
7use std::path::{Path, PathBuf};
8
9#[cfg(feature = "sqlite")]
11fn default_table_name() -> String {
12 "sqllog_records".to_string()
13}
14
15fn default_true() -> bool {
17 true
18}
19
20#[cfg_attr(feature = "csv", derive(Default))]
21#[derive(Debug, Deserialize, Clone)]
22pub struct Config {
23 #[serde(default)]
25 pub sqllog: SqllogConfig,
26 #[serde(default)]
27 pub error: ErrorConfig,
28 #[serde(default)]
29 pub logging: LoggingConfig,
30 #[serde(default)]
31 #[warn(dead_code)]
32 pub features: FeaturesConfig,
33 #[serde(default)]
34 pub exporter: ExporterConfig,
35}
36
37impl Config {
38 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
40 let path = path.as_ref();
41 let content = std::fs::read_to_string(path)
42 .map_err(|_| Error::Config(ConfigError::NotFound(path.to_path_buf())))?;
43 Self::from_str(&content, path.to_path_buf())
44 }
45
46 pub fn from_str(content: &str, path: PathBuf) -> Result<Self> {
48 let config: Config = toml::from_str(content).map_err(|e| {
49 Error::Config(ConfigError::ParseFailed {
50 path,
51 reason: e.to_string(),
52 })
53 })?;
54
55 config.validate()?;
57
58 Ok(config)
59 }
60
61 pub fn validate(&self) -> Result<()> {
63 self.logging.validate()?;
65
66 self.exporter.validate()?;
68
69 self.sqllog.validate()?;
71
72 FeaturesConfig::validate();
74
75 Ok(())
76 }
77}
78
79#[derive(Debug, Deserialize, Clone)]
81pub struct SqllogConfig {
82 pub directory: String,
84}
85
86impl Default for SqllogConfig {
87 fn default() -> Self {
88 Self {
89 directory: "sqllogs".to_string(),
90 }
91 }
92}
93
94impl SqllogConfig {
95 #[must_use]
97 pub fn directory(&self) -> &str {
98 &self.directory
99 }
100
101 pub fn validate(&self) -> Result<()> {
103 if self.directory.trim().is_empty() {
104 return Err(Error::Config(ConfigError::InvalidValue {
105 field: "sqllog.directory".to_string(),
106 value: self.directory.clone(),
107 reason: "Input directory cannot be empty".to_string(),
108 }));
109 }
110 Ok(())
111 }
112}
113
114#[derive(Debug, Deserialize, Clone)]
115pub struct ErrorConfig {
116 #[serde(default = "default_error_file")]
118 pub file: String,
119}
120
121fn default_error_file() -> String {
122 "export/errors.log".to_string()
123}
124
125impl ErrorConfig {
126 #[must_use]
128 pub fn file(&self) -> &str {
129 &self.file
130 }
131}
132
133impl Default for ErrorConfig {
134 fn default() -> Self {
135 Self {
136 file: "export/errors.log".to_string(),
137 }
138 }
139}
140
141#[derive(Debug, Deserialize, Clone)]
142pub struct LoggingConfig {
143 #[serde(default = "default_logging_file")]
145 pub file: String,
146 #[serde(default = "default_logging_level")]
147 pub level: String,
148 #[serde(default = "default_retention_days")]
149 pub retention_days: usize,
150}
151
152fn default_logging_file() -> String {
153 "logs/sqllog2db.log".to_string()
154}
155
156fn default_logging_level() -> String {
157 "info".to_string()
158}
159
160fn default_retention_days() -> usize {
161 7
162}
163
164impl LoggingConfig {
165 #[must_use]
167 pub fn file(&self) -> &str {
168 &self.file
169 }
170
171 #[must_use]
173 pub fn level(&self) -> &str {
174 &self.level
175 }
176
177 #[must_use]
179 pub fn retention_days(&self) -> usize {
180 self.retention_days
181 }
182
183 pub fn validate(&self) -> Result<()> {
185 if !LOG_LEVELS
186 .iter()
187 .any(|&l| l.eq_ignore_ascii_case(self.level.as_str()))
188 {
189 return Err(Error::Config(ConfigError::InvalidLogLevel {
190 level: self.level.clone(),
191 valid_levels: LOG_LEVELS.iter().map(|s| (*s).to_string()).collect(),
192 }));
193 }
194
195 if self.retention_days == 0 || self.retention_days > 365 {
197 return Err(Error::Config(ConfigError::InvalidValue {
198 field: "logging.retention_days".to_string(),
199 value: self.retention_days.to_string(),
200 reason: "Retention days must be between 1 and 365".to_string(),
201 }));
202 }
203
204 Ok(())
205 }
206}
207
208impl Default for LoggingConfig {
209 fn default() -> Self {
210 Self {
211 file: "logs/sqllog2db.log".to_string(),
212 level: "info".to_string(),
213 retention_days: 7,
214 }
215 }
216}
217
218#[derive(Debug, Deserialize, Clone)]
219pub struct ExporterConfig {
220 #[cfg(feature = "csv")]
221 pub csv: Option<CsvExporter>,
222 #[cfg(feature = "jsonl")]
223 pub jsonl: Option<JsonlExporter>,
224 #[cfg(feature = "sqlite")]
225 pub sqlite: Option<SqliteExporter>,
226}
227
228impl ExporterConfig {
229 #[cfg(feature = "csv")]
231 #[must_use]
232 pub fn csv(&self) -> Option<&CsvExporter> {
233 self.csv.as_ref()
234 }
235
236 #[cfg(feature = "jsonl")]
237 #[must_use]
239 pub fn jsonl(&self) -> Option<&JsonlExporter> {
240 self.jsonl.as_ref()
241 }
242
243 #[cfg(feature = "sqlite")]
244 #[must_use]
246 pub fn sqlite(&self) -> Option<&SqliteExporter> {
247 self.sqlite.as_ref()
248 }
249
250 #[must_use]
252 pub fn has_exporters(&self) -> bool {
253 let mut found = false;
254 #[cfg(feature = "csv")]
255 {
256 found = found || self.csv.is_some();
257 }
258 #[cfg(feature = "jsonl")]
259 {
260 found = found || self.jsonl.is_some();
261 }
262 #[cfg(feature = "sqlite")]
263 {
264 found = found || self.sqlite.is_some();
265 }
266 found
267 }
268
269 #[must_use]
271 pub fn total_exporters(&self) -> usize {
272 let mut count = 0;
273 #[cfg(feature = "csv")]
274 {
275 if self.csv.is_some() {
276 count += 1;
277 }
278 }
279 #[cfg(feature = "jsonl")]
280 {
281 if self.jsonl.is_some() {
282 count += 1;
283 }
284 }
285 #[cfg(feature = "sqlite")]
286 {
287 if self.sqlite.is_some() {
288 count += 1;
289 }
290 }
291 count
292 }
293
294 pub fn validate(&self) -> Result<()> {
296 if !self.has_exporters() {
297 return Err(Error::Config(ConfigError::NoExporters));
298 }
299
300 let total = self.total_exporters();
301 if total > 1 {
302 eprintln!("Warning: {total} exporters configured, but only one is supported.");
303 eprintln!("Will use the first exporter by priority: CSV > JSONL > SQLite");
304 }
305
306 Ok(())
307 }
308}
309
310impl Default for ExporterConfig {
311 fn default() -> Self {
312 Self {
313 #[cfg(feature = "csv")]
314 csv: Some(CsvExporter::default()),
315 #[cfg(feature = "jsonl")]
316 jsonl: None,
317 #[cfg(feature = "sqlite")]
318 sqlite: None,
319 }
320 }
321}
322
323#[cfg(feature = "jsonl")]
324#[derive(Debug, Deserialize, Clone)]
325pub struct JsonlExporter {
326 pub file: String,
328 #[serde(default = "default_true")]
330 pub overwrite: bool,
331 #[serde(default)]
333 pub append: bool,
334}
335
336#[cfg(feature = "jsonl")]
337impl Default for JsonlExporter {
338 fn default() -> Self {
339 Self {
340 file: "export/sqllog2db.jsonl".to_string(),
341 overwrite: true,
342 append: false,
343 }
344 }
345}
346
347#[cfg(feature = "sqlite")]
348#[derive(Debug, Deserialize, Clone)]
349pub struct SqliteExporter {
350 pub database_url: String,
352 #[serde(default = "default_table_name")]
354 pub table_name: String,
355 #[serde(default = "default_true")]
357 pub overwrite: bool,
358 #[serde(default)]
360 pub append: bool,
361}
362
363#[cfg(feature = "sqlite")]
364impl Default for SqliteExporter {
365 fn default() -> Self {
366 Self {
367 database_url: "export/sqllog2db.db".to_string(),
368 table_name: "sqllog_records".to_string(),
369 overwrite: true,
370 append: false,
371 }
372 }
373}
374
375#[cfg(feature = "csv")]
376#[derive(Debug, Deserialize, Clone)]
377pub struct CsvExporter {
378 pub file: String,
380 #[serde(default = "default_true")]
382 pub overwrite: bool,
383 #[serde(default)]
385 pub append: bool,
386}
387
388#[cfg(feature = "csv")]
389impl Default for CsvExporter {
390 fn default() -> Self {
391 Self {
392 file: "outputs/sqllog.csv".to_string(),
393 overwrite: true,
394 append: false,
395 }
396 }
397}