Skip to main content

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, Clone)]
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: "sqllogs".to_string(),
120        }
121    }
122}
123
124impl SqllogConfig {
125    /// 获取 SQL 日志输入目录
126    #[must_use]
127    pub fn directory(&self) -> &str {
128        &self.directory
129    }
130
131    /// 验证配置
132    pub fn validate(&self) -> Result<()> {
133        if self.directory.trim().is_empty() {
134            return Err(Error::Config(ConfigError::InvalidValue {
135                field: "sqllog.directory".to_string(),
136                value: self.directory.clone(),
137                reason: "Input directory cannot be empty".to_string(),
138            }));
139        }
140        Ok(())
141    }
142}
143
144#[derive(Debug, Deserialize, Clone)]
145pub struct ErrorConfig {
146    /// 错误日志输出文件路径
147    pub file: String,
148}
149
150impl ErrorConfig {
151    /// 获取错误日志输出文件路径
152    #[must_use]
153    pub fn file(&self) -> &str {
154        &self.file
155    }
156}
157
158impl Default for ErrorConfig {
159    fn default() -> Self {
160        Self {
161            file: "export/errors.log".to_string(),
162        }
163    }
164}
165
166#[derive(Debug, Deserialize, Clone)]
167pub struct LoggingConfig {
168    /// 应用日志输出文件路径
169    pub file: String,
170    pub level: String,
171    #[serde(default = "default_retention_days")]
172    pub retention_days: usize,
173}
174
175fn default_retention_days() -> usize {
176    7
177}
178
179impl LoggingConfig {
180    /// 获取日志输出文件路径
181    #[must_use]
182    pub fn file(&self) -> &str {
183        &self.file
184    }
185
186    /// 获取日志级别
187    #[must_use]
188    pub fn level(&self) -> &str {
189        &self.level
190    }
191
192    /// 获取日志保留天数
193    #[must_use]
194    pub fn retention_days(&self) -> usize {
195        self.retention_days
196    }
197
198    /// 验证日志级别是否有效
199    pub fn validate(&self) -> Result<()> {
200        if !LOG_LEVELS
201            .iter()
202            .any(|&l| l.eq_ignore_ascii_case(self.level.as_str()))
203        {
204            return Err(Error::Config(ConfigError::InvalidLogLevel {
205                level: self.level.clone(),
206                valid_levels: LOG_LEVELS.iter().map(|s| (*s).to_string()).collect(),
207            }));
208        }
209
210        // 验证保留天数(1-365天)
211        if self.retention_days == 0 || self.retention_days > 365 {
212            return Err(Error::Config(ConfigError::InvalidValue {
213                field: "logging.retention_days".to_string(),
214                value: self.retention_days.to_string(),
215                reason: "Retention days must be between 1 and 365".to_string(),
216            }));
217        }
218
219        Ok(())
220    }
221}
222
223impl Default for LoggingConfig {
224    fn default() -> Self {
225        Self {
226            file: "logs/sqllog2db.log".to_string(),
227            level: "info".to_string(),
228            retention_days: 7,
229        }
230    }
231}
232
233/// 通用的 feature 开关
234#[derive(Debug, Deserialize, Clone)]
235pub struct ReplaceParametersFeature {
236    pub enable: bool,
237    pub symbols: Option<Vec<String>>,
238}
239
240#[derive(Debug, Deserialize, Clone, Default)]
241pub struct FeaturesConfig {
242    /// 对应配置文件中的 `[features.replace_parameters]`
243    #[serde(default)]
244    pub replace_parameters: Option<ReplaceParametersFeature>,
245}
246
247impl FeaturesConfig {
248    /// 是否启用 SQL 参数替换
249    #[must_use]
250    pub fn should_replace_sql_parameters(&self) -> bool {
251        self.replace_parameters.as_ref().is_some_and(|f| f.enable)
252    }
253}
254
255#[derive(Debug, Deserialize, Clone)]
256pub struct ExporterConfig {
257    #[cfg(feature = "csv")]
258    pub csv: Option<CsvExporter>,
259    #[cfg(feature = "parquet")]
260    pub parquet: Option<ParquetExporter>,
261    #[cfg(feature = "jsonl")]
262    pub jsonl: Option<JsonlExporter>,
263    #[cfg(feature = "sqlite")]
264    pub sqlite: Option<SqliteExporter>,
265    #[cfg(feature = "duckdb")]
266    pub duckdb: Option<DuckdbExporter>,
267    #[cfg(feature = "postgres")]
268    pub postgres: Option<PostgresExporter>,
269    #[cfg(feature = "dm")]
270    pub dm: Option<DmExporter>,
271}
272
273impl ExporterConfig {
274    /// 获取 CSV 导出器配置
275    #[cfg(feature = "csv")]
276    #[must_use]
277    pub fn csv(&self) -> Option<&CsvExporter> {
278        self.csv.as_ref()
279    }
280
281    #[cfg(feature = "parquet")]
282    /// 获取 Parquet 导出器配置
283    #[must_use]
284    pub fn parquet(&self) -> Option<&ParquetExporter> {
285        self.parquet.as_ref()
286    }
287
288    #[cfg(feature = "jsonl")]
289    /// 获取 JSONL 导出器配置
290    #[must_use]
291    pub fn jsonl(&self) -> Option<&JsonlExporter> {
292        self.jsonl.as_ref()
293    }
294
295    #[cfg(feature = "sqlite")]
296    /// 获取 `SQLite` 导出器配置
297    #[must_use]
298    pub fn sqlite(&self) -> Option<&SqliteExporter> {
299        self.sqlite.as_ref()
300    }
301
302    #[cfg(feature = "duckdb")]
303    /// 获取 `DuckDB` 导出器配置
304    #[must_use]
305    pub fn duckdb(&self) -> Option<&DuckdbExporter> {
306        self.duckdb.as_ref()
307    }
308
309    #[cfg(feature = "postgres")]
310    /// 获取 `PostgreSQL` 导出器配置
311    #[must_use]
312    pub fn postgres(&self) -> Option<&PostgresExporter> {
313        self.postgres.as_ref()
314    }
315
316    #[cfg(feature = "dm")]
317    /// 获取 DM 导出器配置
318    #[must_use]
319    pub fn dm(&self) -> Option<&DmExporter> {
320        self.dm.as_ref()
321    }
322
323    /// 检查是否有任何导出器配置
324    #[must_use]
325    pub fn has_exporters(&self) -> bool {
326        let mut found = false;
327        #[cfg(feature = "csv")]
328        {
329            found = found || self.csv.is_some();
330        }
331        #[cfg(feature = "parquet")]
332        {
333            found = found || self.parquet.is_some();
334        }
335        #[cfg(feature = "jsonl")]
336        {
337            found = found || self.jsonl.is_some();
338        }
339        #[cfg(feature = "sqlite")]
340        {
341            found = found || self.sqlite.is_some();
342        }
343        #[cfg(feature = "duckdb")]
344        {
345            found = found || self.duckdb.is_some();
346        }
347        #[cfg(feature = "postgres")]
348        {
349            found = found || self.postgres.is_some();
350        }
351        #[cfg(feature = "dm")]
352        {
353            found = found || self.dm.is_some();
354        }
355        found
356    }
357
358    /// 统计配置的导出器总数
359    #[must_use]
360    pub fn total_exporters(&self) -> usize {
361        let mut count = 0;
362        #[cfg(feature = "csv")]
363        {
364            if self.csv.is_some() {
365                count += 1;
366            }
367        }
368        #[cfg(feature = "parquet")]
369        {
370            if self.parquet.is_some() {
371                count += 1;
372            }
373        }
374        #[cfg(feature = "jsonl")]
375        {
376            if self.jsonl.is_some() {
377                count += 1;
378            }
379        }
380        #[cfg(feature = "sqlite")]
381        {
382            if self.sqlite.is_some() {
383                count += 1;
384            }
385        }
386        #[cfg(feature = "duckdb")]
387        {
388            if self.duckdb.is_some() {
389                count += 1;
390            }
391        }
392        #[cfg(feature = "postgres")]
393        {
394            if self.postgres.is_some() {
395                count += 1;
396            }
397        }
398        #[cfg(feature = "dm")]
399        {
400            if self.dm.is_some() {
401                count += 1;
402            }
403        }
404        count
405    }
406
407    /// 验证导出器配置(只支持单个导出器)
408    pub fn validate(&self) -> Result<()> {
409        if !self.has_exporters() {
410            return Err(Error::Config(ConfigError::NoExporters));
411        }
412
413        let total = self.total_exporters();
414        if total > 1 {
415            eprintln!("Warning: {total} exporters configured, but only one is supported.");
416            eprintln!(
417                "Will use the first exporter by priority: CSV > Parquet > JSONL > SQLite > DuckDB > PostgreSQL > DM"
418            );
419        }
420
421        Ok(())
422    }
423}
424
425impl Default for ExporterConfig {
426    fn default() -> Self {
427        Self {
428            #[cfg(feature = "csv")]
429            csv: Some(CsvExporter::default()),
430            #[cfg(feature = "parquet")]
431            parquet: Some(ParquetExporter::default()),
432            #[cfg(feature = "jsonl")]
433            jsonl: None,
434            #[cfg(feature = "sqlite")]
435            sqlite: None,
436            #[cfg(feature = "duckdb")]
437            duckdb: None,
438            #[cfg(feature = "postgres")]
439            postgres: None,
440            #[cfg(feature = "dm")]
441            dm: None,
442        }
443    }
444}
445
446#[cfg(feature = "parquet")]
447#[derive(Debug, Deserialize, Clone)]
448pub struct ParquetExporter {
449    /// Parquet 输出文件路径
450    pub file: String,
451    /// 是否覆盖已存在的文件
452    pub overwrite: bool,
453    /// 每个 row group 的行数
454    pub row_group_size: Option<usize>,
455    /// 是否启用字典编码
456    pub use_dictionary: Option<bool>,
457}
458
459#[cfg(feature = "parquet")]
460impl Default for ParquetExporter {
461    fn default() -> Self {
462        Self {
463            file: "export/sqllog2db.parquet".to_string(),
464            overwrite: true,
465            row_group_size: Some(100_000),
466            use_dictionary: Some(true),
467        }
468    }
469}
470
471#[cfg(feature = "csv")]
472#[derive(Debug, Deserialize, Clone)]
473pub struct CsvExporter {
474    /// CSV 输出文件路径
475    pub file: String,
476    /// 是否覆盖已存在的文件
477    pub overwrite: bool,
478    /// 是否追加模式(暂未实现)
479    pub append: bool,
480}
481
482#[cfg(feature = "csv")]
483impl Default for CsvExporter {
484    fn default() -> Self {
485        Self {
486            file: "outputs/sqllog.csv".to_string(),
487            overwrite: true,
488            append: false,
489        }
490    }
491}
492
493#[cfg(feature = "jsonl")]
494#[derive(Debug, Deserialize, Clone)]
495pub struct JsonlExporter {
496    /// JSONL 输出文件路径
497    pub file: String,
498    /// 是否覆盖已存在的文件
499    pub overwrite: bool,
500    /// 是否追加模式
501    pub append: bool,
502}
503
504#[cfg(feature = "jsonl")]
505impl Default for JsonlExporter {
506    fn default() -> Self {
507        Self {
508            file: "export/sqllog2db.jsonl".to_string(),
509            overwrite: true,
510            append: false,
511        }
512    }
513}
514
515#[cfg(feature = "sqlite")]
516#[derive(Debug, Deserialize, Clone)]
517pub struct SqliteExporter {
518    /// `SQLite` 数据库文件路径
519    pub database_url: String,
520    /// 表名
521    #[serde(default = "default_table_name")]
522    pub table_name: String,
523    /// 是否覆盖已存在的表
524    #[serde(default = "default_true")]
525    pub overwrite: bool,
526    /// 是否追加模式
527    #[serde(default)]
528    pub append: bool,
529}
530
531#[cfg(feature = "sqlite")]
532impl Default for SqliteExporter {
533    fn default() -> Self {
534        Self {
535            database_url: "export/sqllog2db.db".to_string(),
536            table_name: "sqllog_records".to_string(),
537            overwrite: true,
538            append: false,
539        }
540    }
541}
542
543#[cfg(feature = "duckdb")]
544#[derive(Debug, Deserialize, Clone)]
545pub struct DuckdbExporter {
546    /// `DuckDB` 数据库文件路径
547    pub database_url: String,
548    /// 表名
549    #[serde(default = "default_table_name")]
550    pub table_name: String,
551    /// 是否覆盖已存在的表
552    #[serde(default = "default_true")]
553    pub overwrite: bool,
554    /// 是否追加模式
555    #[serde(default)]
556    pub append: bool,
557}
558
559#[cfg(feature = "duckdb")]
560impl Default for DuckdbExporter {
561    fn default() -> Self {
562        Self {
563            database_url: "export/sqllog2db.duckdb".to_string(),
564            table_name: "sqllog_records".to_string(),
565            overwrite: true,
566            append: false,
567        }
568    }
569}
570
571#[cfg(feature = "postgres")]
572#[derive(Debug, Deserialize, Clone)]
573pub struct PostgresExporter {
574    /// `PostgreSQL` 主机地址
575    #[serde(default = "default_postgres_host")]
576    pub host: String,
577    /// `PostgreSQL` 端口
578    #[serde(default = "default_postgres_port")]
579    pub port: u16,
580    /// 用户名
581    #[serde(default = "default_postgres_username")]
582    pub username: String,
583    /// 密码
584    pub password: String,
585    /// 数据库名
586    #[serde(default = "default_postgres_database")]
587    pub database: String,
588    /// Schema 名称
589    #[serde(default = "default_postgres_schema")]
590    pub schema: String,
591    /// 表名
592    #[serde(default = "default_table_name")]
593    pub table_name: String,
594    /// 是否覆盖已存在的表
595    #[serde(default = "default_true")]
596    pub overwrite: bool,
597    /// 是否追加模式
598    #[serde(default)]
599    pub append: bool,
600}
601
602#[cfg(feature = "postgres")]
603impl Default for PostgresExporter {
604    fn default() -> Self {
605        Self {
606            host: "localhost".to_string(),
607            port: 5432,
608            username: "postgres".to_string(),
609            password: "postgres".to_string(),
610            database: "sqllog".to_string(),
611            schema: "public".to_string(),
612            table_name: "sqllog_records".to_string(),
613            overwrite: true,
614            append: false,
615        }
616    }
617}
618
619#[cfg(feature = "postgres")]
620impl PostgresExporter {
621    /// 获取连接字符串
622    #[must_use]
623    pub fn connection_string(&self) -> String {
624        if self.password.is_empty() {
625            format!(
626                "host={} port={} user={} dbname={}",
627                self.host, self.port, self.username, self.database
628            )
629        } else {
630            format!(
631                "host={} port={} user={} password={} dbname={}",
632                self.host, self.port, self.username, self.password, self.database
633            )
634        }
635    }
636}
637
638#[cfg(feature = "dm")]
639#[derive(Debug, Deserialize, Clone)]
640pub struct DmExporter {
641    /// DM 数据库连接字符串 (例如: SYSDBA/SYSDBA@localhost:5236)
642    pub userid: String,
643    /// 表名
644    #[serde(default = "default_table_name")]
645    pub table_name: String,
646    /// 控制文件路径
647    pub control_file: String,
648    /// 日志目录
649    pub log_dir: String,
650}
651
652#[cfg(feature = "dm")]
653impl Default for DmExporter {
654    fn default() -> Self {
655        Self {
656            userid: "SYSDBA/SYSDBA@localhost:5236".to_string(),
657            table_name: "sqllog_records".to_string(),
658            control_file: "export/sqllog.ctl".to_string(),
659            log_dir: "export/log".to_string(),
660        }
661    }
662}