1use crate::constants::LOG_LEVELS;
2use crate::error::{ConfigError, Error, Result};
3use serde::Deserialize;
4use std::path::{Path, PathBuf};
5
6#[cfg(any(
8 feature = "sqlite",
9 feature = "duckdb",
10 feature = "postgres",
11 feature = "dm"
12))]
13fn default_table_name() -> String {
14 "sqllog_records".to_string()
15}
16
17#[cfg(any(
19 feature = "sqlite",
20 feature = "duckdb",
21 feature = "postgres",
22 feature = "dm"
23))]
24fn default_true() -> bool {
25 true
26}
27
28#[cfg(feature = "postgres")]
30fn default_postgres_host() -> String {
31 "localhost".to_string()
32}
33
34#[cfg(feature = "postgres")]
36fn default_postgres_port() -> u16 {
37 5432
38}
39
40#[cfg(feature = "postgres")]
42fn default_postgres_username() -> String {
43 "postgres".to_string()
44}
45
46#[cfg(feature = "postgres")]
48fn default_postgres_database() -> String {
49 "sqllog".to_string()
50}
51
52#[cfg(feature = "postgres")]
54fn default_postgres_schema() -> String {
55 "public".to_string()
56}
57
58#[cfg_attr(feature = "csv", derive(Default))]
59#[derive(Debug, Deserialize)]
60pub struct Config {
61 #[serde(default)]
63 pub sqllog: SqllogConfig,
64 pub error: ErrorConfig,
65 pub logging: LoggingConfig,
66 pub features: FeaturesConfig,
67 pub exporter: ExporterConfig,
68}
69
70impl Config {
71 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
73 let path = path.as_ref();
74 let content = std::fs::read_to_string(path)
75 .map_err(|_| Error::Config(ConfigError::NotFound(path.to_path_buf())))?;
76 Self::from_str(&content, path.to_path_buf())
77 }
78
79 pub fn from_str(content: &str, path: PathBuf) -> Result<Self> {
81 let config: Config = toml::from_str(content).map_err(|e| {
82 Error::Config(ConfigError::ParseFailed {
83 path,
84 reason: e.to_string(),
85 })
86 })?;
87
88 config.validate()?;
90
91 Ok(config)
92 }
93
94 pub fn validate(&self) -> Result<()> {
96 self.logging.validate()?;
98
99 self.exporter.validate()?;
101
102 self.sqllog.validate()?;
104
105 Ok(())
106 }
107}
108
109#[derive(Debug, Deserialize, Clone)]
111pub struct SqllogConfig {
112 pub directory: String,
114}
115
116impl Default for SqllogConfig {
117 fn default() -> Self {
118 Self {
119 directory: "sqllog".to_string(),
120 }
121 }
122}
123
124impl SqllogConfig {
125 pub fn directory(&self) -> &str {
127 &self.directory
128 }
129
130 pub fn validate(&self) -> Result<()> {
132 if self.directory.trim().is_empty() {
133 return Err(Error::Config(ConfigError::InvalidValue {
134 field: "sqllog.directory".to_string(),
135 value: self.directory.clone(),
136 reason: "Input directory cannot be empty".to_string(),
137 }));
138 }
139 Ok(())
140 }
141}
142
143#[derive(Debug, Deserialize)]
144pub struct ErrorConfig {
145 pub file: String,
147}
148
149impl ErrorConfig {
150 pub fn file(&self) -> &str {
152 &self.file
153 }
154}
155
156impl Default for ErrorConfig {
157 fn default() -> Self {
158 Self {
159 file: "errors.json".to_string(),
160 }
161 }
162}
163
164#[derive(Debug, Deserialize)]
165pub struct LoggingConfig {
166 pub file: String,
168 pub level: String,
169 #[serde(default = "default_retention_days")]
170 pub retention_days: usize,
171}
172
173fn default_retention_days() -> usize {
174 7
175}
176
177impl LoggingConfig {
178 pub fn file(&self) -> &str {
180 &self.file
181 }
182
183 pub fn level(&self) -> &str {
185 &self.level
186 }
187
188 pub fn retention_days(&self) -> usize {
190 self.retention_days
191 }
192
193 pub fn validate(&self) -> Result<()> {
195 if !LOG_LEVELS
196 .iter()
197 .any(|&l| l.eq_ignore_ascii_case(self.level.as_str()))
198 {
199 return Err(Error::Config(ConfigError::InvalidLogLevel {
200 level: self.level.clone(),
201 valid_levels: LOG_LEVELS.iter().map(|s| s.to_string()).collect(),
202 }));
203 }
204
205 if self.retention_days == 0 || self.retention_days > 365 {
207 return Err(Error::Config(ConfigError::InvalidValue {
208 field: "logging.retention_days".to_string(),
209 value: self.retention_days.to_string(),
210 reason: "Retention days must be between 1 and 365".to_string(),
211 }));
212 }
213
214 Ok(())
215 }
216}
217
218impl Default for LoggingConfig {
219 fn default() -> Self {
220 Self {
221 file: "logs/sqllog2db.log".to_string(),
222 level: "info".to_string(),
223 retention_days: 7,
224 }
225 }
226}
227
228#[derive(Debug, Deserialize, Clone)]
230pub struct ReplaceParametersFeature {
231 pub enable: bool,
232 pub symbols: Option<Vec<String>>,
233}
234
235#[derive(Debug, Deserialize, Clone, Default)]
236pub struct FeaturesConfig {
237 #[serde(default)]
239 pub replace_parameters: Option<ReplaceParametersFeature>,
240}
241
242impl FeaturesConfig {
243 pub fn should_replace_sql_parameters(&self) -> bool {
245 self.replace_parameters
246 .as_ref()
247 .map(|f| f.enable)
248 .unwrap_or(false)
249 }
250}
251
252#[derive(Debug, Deserialize)]
253pub struct ExporterConfig {
254 #[cfg(feature = "csv")]
255 pub csv: Option<CsvExporter>,
256 #[cfg(feature = "parquet")]
257 pub parquet: Option<ParquetExporter>,
258 #[cfg(feature = "jsonl")]
259 pub jsonl: Option<JsonlExporter>,
260 #[cfg(feature = "sqlite")]
261 pub sqlite: Option<SqliteExporter>,
262 #[cfg(feature = "duckdb")]
263 pub duckdb: Option<DuckdbExporter>,
264 #[cfg(feature = "postgres")]
265 pub postgres: Option<PostgresExporter>,
266 #[cfg(feature = "dm")]
267 pub dm: Option<DmExporter>,
268}
269
270impl ExporterConfig {
271 #[cfg(feature = "csv")]
273 pub fn csv(&self) -> Option<&CsvExporter> {
274 self.csv.as_ref()
275 }
276
277 #[cfg(feature = "parquet")]
278 pub fn parquet(&self) -> Option<&ParquetExporter> {
280 self.parquet.as_ref()
281 }
282
283 #[cfg(feature = "jsonl")]
284 pub fn jsonl(&self) -> Option<&JsonlExporter> {
286 self.jsonl.as_ref()
287 }
288
289 #[cfg(feature = "sqlite")]
290 pub fn sqlite(&self) -> Option<&SqliteExporter> {
292 self.sqlite.as_ref()
293 }
294
295 #[cfg(feature = "duckdb")]
296 pub fn duckdb(&self) -> Option<&DuckdbExporter> {
298 self.duckdb.as_ref()
299 }
300
301 #[cfg(feature = "postgres")]
302 pub fn postgres(&self) -> Option<&PostgresExporter> {
304 self.postgres.as_ref()
305 }
306
307 #[cfg(feature = "dm")]
308 pub fn dm(&self) -> Option<&DmExporter> {
310 self.dm.as_ref()
311 }
312
313 pub fn has_exporters(&self) -> bool {
315 let mut found = false;
316 #[cfg(feature = "csv")]
317 {
318 found = found || self.csv.is_some();
319 }
320 #[cfg(feature = "parquet")]
321 {
322 found = found || self.parquet.is_some();
323 }
324 #[cfg(feature = "jsonl")]
325 {
326 found = found || self.jsonl.is_some();
327 }
328 #[cfg(feature = "sqlite")]
329 {
330 found = found || self.sqlite.is_some();
331 }
332 #[cfg(feature = "duckdb")]
333 {
334 found = found || self.duckdb.is_some();
335 }
336 #[cfg(feature = "postgres")]
337 {
338 found = found || self.postgres.is_some();
339 }
340 #[cfg(feature = "dm")]
341 {
342 found = found || self.dm.is_some();
343 }
344 found
345 }
346
347 pub fn total_exporters(&self) -> usize {
349 let mut count = 0;
350 #[cfg(feature = "csv")]
351 {
352 if self.csv.is_some() {
353 count += 1;
354 }
355 }
356 #[cfg(feature = "parquet")]
357 {
358 if self.parquet.is_some() {
359 count += 1;
360 }
361 }
362 #[cfg(feature = "jsonl")]
363 {
364 if self.jsonl.is_some() {
365 count += 1;
366 }
367 }
368 #[cfg(feature = "sqlite")]
369 {
370 if self.sqlite.is_some() {
371 count += 1;
372 }
373 }
374 #[cfg(feature = "duckdb")]
375 {
376 if self.duckdb.is_some() {
377 count += 1;
378 }
379 }
380 #[cfg(feature = "postgres")]
381 {
382 if self.postgres.is_some() {
383 count += 1;
384 }
385 }
386 #[cfg(feature = "dm")]
387 {
388 if self.dm.is_some() {
389 count += 1;
390 }
391 }
392 count
393 }
394
395 pub fn validate(&self) -> Result<()> {
397 if !self.has_exporters() {
398 return Err(Error::Config(ConfigError::NoExporters));
399 }
400
401 let total = self.total_exporters();
402 if total > 1 {
403 eprintln!(
404 "Warning: {} exporters configured, but only one is supported.",
405 total
406 );
407 eprintln!("Will use the first exporter by priority: CSV > Parquet > JSONL");
408 }
409
410 Ok(())
411 }
412}
413
414impl Default for ExporterConfig {
415 fn default() -> Self {
416 Self {
417 #[cfg(feature = "csv")]
418 csv: Some(CsvExporter::default()),
419 #[cfg(feature = "parquet")]
420 parquet: Some(ParquetExporter::default()),
421 #[cfg(feature = "jsonl")]
422 jsonl: None,
423 #[cfg(feature = "sqlite")]
424 sqlite: None,
425 #[cfg(feature = "duckdb")]
426 duckdb: None,
427 #[cfg(feature = "postgres")]
428 postgres: None,
429 #[cfg(feature = "dm")]
430 dm: None,
431 }
432 }
433}
434
435#[cfg(feature = "parquet")]
436#[derive(Debug, Deserialize)]
437pub struct ParquetExporter {
438 pub file: String,
440 pub overwrite: bool,
442 pub row_group_size: Option<usize>,
444 pub use_dictionary: Option<bool>,
446}
447
448#[cfg(feature = "parquet")]
449impl Default for ParquetExporter {
450 fn default() -> Self {
451 Self {
452 file: "export/sqllog2db.parquet".to_string(),
453 overwrite: true,
454 row_group_size: Some(100000),
455 use_dictionary: Some(true),
456 }
457 }
458}
459
460#[cfg(feature = "csv")]
461#[derive(Debug, Deserialize)]
462pub struct CsvExporter {
463 pub file: String,
465 pub overwrite: bool,
467 pub append: bool,
469}
470
471#[cfg(feature = "csv")]
472impl Default for CsvExporter {
473 fn default() -> Self {
474 Self {
475 file: "outputs/sqllog.csv".to_string(),
476 overwrite: true,
477 append: false,
478 }
479 }
480}
481
482#[cfg(feature = "jsonl")]
483#[derive(Debug, Deserialize)]
484pub struct JsonlExporter {
485 pub file: String,
487 pub overwrite: bool,
489 pub append: bool,
491}
492
493#[cfg(feature = "jsonl")]
494impl Default for JsonlExporter {
495 fn default() -> Self {
496 Self {
497 file: "export/sqllog2db.jsonl".to_string(),
498 overwrite: true,
499 append: false,
500 }
501 }
502}
503
504#[cfg(feature = "sqlite")]
505#[derive(Debug, Deserialize)]
506pub struct SqliteExporter {
507 pub database_url: String,
509 #[serde(default = "default_table_name")]
511 pub table_name: String,
512 #[serde(default = "default_true")]
514 pub overwrite: bool,
515 #[serde(default)]
517 pub append: bool,
518}
519
520#[cfg(feature = "sqlite")]
521impl Default for SqliteExporter {
522 fn default() -> Self {
523 Self {
524 database_url: "export/sqllog2db.db".to_string(),
525 table_name: "sqllog_records".to_string(),
526 overwrite: true,
527 append: false,
528 }
529 }
530}
531
532#[cfg(feature = "duckdb")]
533#[derive(Debug, Deserialize)]
534pub struct DuckdbExporter {
535 pub database_url: String,
537 #[serde(default = "default_table_name")]
539 pub table_name: String,
540 #[serde(default = "default_true")]
542 pub overwrite: bool,
543 #[serde(default)]
545 pub append: bool,
546}
547
548#[cfg(feature = "duckdb")]
549impl Default for DuckdbExporter {
550 fn default() -> Self {
551 Self {
552 database_url: "export/sqllog2db.duckdb".to_string(),
553 table_name: "sqllog_records".to_string(),
554 overwrite: true,
555 append: false,
556 }
557 }
558}
559
560#[cfg(feature = "postgres")]
561#[derive(Debug, Deserialize)]
562pub struct PostgresExporter {
563 #[serde(default = "default_postgres_host")]
565 pub host: String,
566 #[serde(default = "default_postgres_port")]
568 pub port: u16,
569 #[serde(default = "default_postgres_username")]
571 pub username: String,
572 pub password: String,
574 #[serde(default = "default_postgres_database")]
576 pub database: String,
577 #[serde(default = "default_postgres_schema")]
579 pub schema: String,
580 #[serde(default = "default_table_name")]
582 pub table_name: String,
583 #[serde(default = "default_true")]
585 pub overwrite: bool,
586 #[serde(default)]
588 pub append: bool,
589}
590
591#[cfg(feature = "postgres")]
592impl Default for PostgresExporter {
593 fn default() -> Self {
594 Self {
595 host: "localhost".to_string(),
596 port: 5432,
597 username: "postgres".to_string(),
598 password: "postgres".to_string(),
599 database: "sqllog".to_string(),
600 schema: "public".to_string(),
601 table_name: "sqllog_records".to_string(),
602 overwrite: true,
603 append: false,
604 }
605 }
606}
607
608#[cfg(feature = "postgres")]
609impl PostgresExporter {
610 pub fn connection_string(&self) -> String {
612 if self.password.is_empty() {
613 format!(
614 "host={} port={} user={} dbname={}",
615 self.host, self.port, self.username, self.database
616 )
617 } else {
618 format!(
619 "host={} port={} user={} password={} dbname={}",
620 self.host, self.port, self.username, self.password, self.database
621 )
622 }
623 }
624}
625
626#[cfg(feature = "dm")]
627fn default_charset() -> String {
628 "UTF-8".to_string()
629}
630
631#[cfg(feature = "dm")]
632#[derive(Debug, Deserialize)]
633pub struct DmExporter {
634 pub userid: String,
636 #[serde(default = "default_table_name")]
638 pub table_name: String,
639 pub control_file: String,
641 pub log_dir: String,
643 #[serde(default = "default_true")]
645 pub overwrite: bool,
646 #[serde(default = "default_charset")]
648 pub charset: String,
649}
650
651#[cfg(feature = "dm")]
652impl Default for DmExporter {
653 fn default() -> Self {
654 Self {
655 userid: "SYSDBA/SYSDBA@localhost:5236".to_string(),
656 table_name: "sqllog_records".to_string(),
657 control_file: "export/sqllog.ctl".to_string(),
658 log_dir: "export/log".to_string(),
659 overwrite: true,
660 charset: "UTF-8".to_string(),
661 }
662 }
663}