dm_database_sqllog2db/
config.rs

1use crate::constants::LOG_LEVELS;
2use crate::error::{ConfigError, Error, Result};
3use serde::Deserialize;
4use std::path::{Path, PathBuf};
5
6/// 默认表名
7#[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/// 默认 true 值
18#[cfg(any(
19    feature = "sqlite",
20    feature = "duckdb",
21    feature = "postgres",
22    feature = "dm"
23))]
24fn default_true() -> bool {
25    true
26}
27
28/// PostgreSQL 默认主机
29#[cfg(feature = "postgres")]
30fn default_postgres_host() -> String {
31    "localhost".to_string()
32}
33
34/// PostgreSQL 默认端口
35#[cfg(feature = "postgres")]
36fn default_postgres_port() -> u16 {
37    5432
38}
39
40/// PostgreSQL 默认用户名
41#[cfg(feature = "postgres")]
42fn default_postgres_username() -> String {
43    "postgres".to_string()
44}
45
46/// PostgreSQL 默认数据库
47#[cfg(feature = "postgres")]
48fn default_postgres_database() -> String {
49    "sqllog".to_string()
50}
51
52/// PostgreSQL 默认 schema
53#[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    /// 新增:SQL 日志输入相关配置
62    #[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    /// 从文件加载配置
72    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    /// 从字符串解析配置
80    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        // 验证配置
89        config.validate()?;
90
91        Ok(config)
92    }
93
94    /// 验证配置的有效性
95    pub fn validate(&self) -> Result<()> {
96        // 验证日志级别
97        self.logging.validate()?;
98
99        // 验证导出器配置
100        self.exporter.validate()?;
101
102        // 验证 sqllog 配置
103        self.sqllog.validate()?;
104
105        Ok(())
106    }
107}
108
109/// SQL 日志输入配置
110#[derive(Debug, Deserialize, Clone)]
111pub struct SqllogConfig {
112    /// SQL 日志输入目录(可包含多个日志文件)
113    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    /// 获取 SQL 日志输入目录
126    pub fn directory(&self) -> &str {
127        &self.directory
128    }
129
130    /// 验证配置
131    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    /// 错误日志输出文件路径
146    pub file: String,
147}
148
149impl ErrorConfig {
150    /// 获取错误日志输出文件路径
151    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    /// 应用日志输出文件路径
167    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    /// 获取日志输出文件路径
179    pub fn file(&self) -> &str {
180        &self.file
181    }
182
183    /// 获取日志级别
184    pub fn level(&self) -> &str {
185        &self.level
186    }
187
188    /// 获取日志保留天数
189    pub fn retention_days(&self) -> usize {
190        self.retention_days
191    }
192
193    /// 验证日志级别是否有效
194    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        // 验证保留天数(1-365天)
206        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/// 通用的 feature 开关
229#[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    /// 对应配置文件中的 `[features.replace_parameters]`
238    #[serde(default)]
239    pub replace_parameters: Option<ReplaceParametersFeature>,
240}
241
242impl FeaturesConfig {
243    /// 是否启用 SQL 参数替换
244    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    /// 获取 CSV 导出器配置
272    #[cfg(feature = "csv")]
273    pub fn csv(&self) -> Option<&CsvExporter> {
274        self.csv.as_ref()
275    }
276
277    #[cfg(feature = "parquet")]
278    /// 获取 Parquet 导出器配置
279    pub fn parquet(&self) -> Option<&ParquetExporter> {
280        self.parquet.as_ref()
281    }
282
283    #[cfg(feature = "jsonl")]
284    /// 获取 JSONL 导出器配置
285    pub fn jsonl(&self) -> Option<&JsonlExporter> {
286        self.jsonl.as_ref()
287    }
288
289    #[cfg(feature = "sqlite")]
290    /// 获取 SQLite 导出器配置
291    pub fn sqlite(&self) -> Option<&SqliteExporter> {
292        self.sqlite.as_ref()
293    }
294
295    #[cfg(feature = "duckdb")]
296    /// 获取 DuckDB 导出器配置
297    pub fn duckdb(&self) -> Option<&DuckdbExporter> {
298        self.duckdb.as_ref()
299    }
300
301    #[cfg(feature = "postgres")]
302    /// 获取 PostgreSQL 导出器配置
303    pub fn postgres(&self) -> Option<&PostgresExporter> {
304        self.postgres.as_ref()
305    }
306
307    #[cfg(feature = "dm")]
308    /// 获取 DM 导出器配置
309    pub fn dm(&self) -> Option<&DmExporter> {
310        self.dm.as_ref()
311    }
312
313    /// 检查是否有任何导出器配置
314    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    /// 统计配置的导出器总数
348    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    /// 验证导出器配置(只支持单个导出器)
396    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    /// Parquet 输出文件路径
439    pub file: String,
440    /// 是否覆盖已存在的文件
441    pub overwrite: bool,
442    /// 每个 row group 的行数
443    pub row_group_size: Option<usize>,
444    /// 是否启用字典编码
445    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    /// CSV 输出文件路径
464    pub file: String,
465    /// 是否覆盖已存在的文件
466    pub overwrite: bool,
467    /// 是否追加模式(暂未实现)
468    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    /// JSONL 输出文件路径
486    pub file: String,
487    /// 是否覆盖已存在的文件
488    pub overwrite: bool,
489    /// 是否追加模式
490    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    /// SQLite 数据库文件路径
508    pub database_url: String,
509    /// 表名
510    #[serde(default = "default_table_name")]
511    pub table_name: String,
512    /// 是否覆盖已存在的表
513    #[serde(default = "default_true")]
514    pub overwrite: bool,
515    /// 是否追加模式
516    #[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    /// DuckDB 数据库文件路径
536    pub database_url: String,
537    /// 表名
538    #[serde(default = "default_table_name")]
539    pub table_name: String,
540    /// 是否覆盖已存在的表
541    #[serde(default = "default_true")]
542    pub overwrite: bool,
543    /// 是否追加模式
544    #[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    /// PostgreSQL 主机地址
564    #[serde(default = "default_postgres_host")]
565    pub host: String,
566    /// PostgreSQL 端口
567    #[serde(default = "default_postgres_port")]
568    pub port: u16,
569    /// 用户名
570    #[serde(default = "default_postgres_username")]
571    pub username: String,
572    /// 密码
573    pub password: String,
574    /// 数据库名
575    #[serde(default = "default_postgres_database")]
576    pub database: String,
577    /// Schema 名称
578    #[serde(default = "default_postgres_schema")]
579    pub schema: String,
580    /// 表名
581    #[serde(default = "default_table_name")]
582    pub table_name: String,
583    /// 是否覆盖已存在的表
584    #[serde(default = "default_true")]
585    pub overwrite: bool,
586    /// 是否追加模式
587    #[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    /// 获取连接字符串
611    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    /// DM 数据库连接字符串 (例如: SYSDBA/SYSDBA@localhost:5236)
635    pub userid: String,
636    /// 表名
637    #[serde(default = "default_table_name")]
638    pub table_name: String,
639    /// 控制文件路径
640    pub control_file: String,
641    /// 日志目录
642    pub log_dir: String,
643    /// 是否覆盖已存在的表数据 (mode='REPLACE')
644    #[serde(default = "default_true")]
645    pub overwrite: bool,
646    /// 字符集 (例如: UTF-8)
647    #[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}