dm-database-sqllog2db 0.3.2

高性能 CLI 工具:流式解析达梦数据库 SQL 日志并导出到 CSV/JSONL/SQLite
Documentation
use crate::constants::LOG_LEVELS;
use crate::error::{ConfigError, Error, Result};
pub use crate::features::{FeaturesConfig, FiltersFeature};
use serde::Deserialize;
use std::path::{Path, PathBuf};

/// 默认表名
#[cfg(feature = "sqlite")]
fn default_table_name() -> String {
    "sqllog_records".to_string()
}

/// 默认 true 值
fn default_true() -> bool {
    true
}

#[cfg_attr(feature = "csv", derive(Default))]
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
    /// 新增:SQL 日志输入相关配置
    #[serde(default)]
    pub sqllog: SqllogConfig,
    #[serde(default)]
    pub error: ErrorConfig,
    #[serde(default)]
    pub logging: LoggingConfig,
    #[serde(default)]
    pub features: FeaturesConfig,
    #[serde(default)]
    pub exporter: ExporterConfig,
}

impl Config {
    /// 从文件加载配置
    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
        let path = path.as_ref();
        let content = std::fs::read_to_string(path)
            .map_err(|_| Error::Config(ConfigError::NotFound(path.to_path_buf())))?;
        Self::from_str(&content, path.to_path_buf())
    }

    /// 从字符串解析配置
    pub fn from_str(content: &str, path: PathBuf) -> Result<Self> {
        let config: Config = toml::from_str(content).map_err(|e| {
            Error::Config(ConfigError::ParseFailed {
                path,
                reason: e.to_string(),
            })
        })?;

        // 验证配置
        config.validate()?;

        Ok(config)
    }

    /// 验证配置的有效性
    pub fn validate(&self) -> Result<()> {
        // 验证日志级别
        self.logging.validate()?;

        // 验证导出器配置
        self.exporter.validate()?;

        // 验证 sqllog 配置
        self.sqllog.validate()?;

        // 验证功能配置
        FeaturesConfig::validate();

        Ok(())
    }
}

/// SQL 日志输入配置
#[derive(Debug, Deserialize, Clone)]
pub struct SqllogConfig {
    /// SQL 日志输入目录(可包含多个日志文件)
    pub directory: String,
}

impl Default for SqllogConfig {
    fn default() -> Self {
        Self {
            directory: "sqllogs".to_string(),
        }
    }
}

impl SqllogConfig {
    /// 获取 SQL 日志输入目录
    #[must_use]
    pub fn directory(&self) -> &str {
        &self.directory
    }

    /// 验证配置
    pub fn validate(&self) -> Result<()> {
        if self.directory.trim().is_empty() {
            return Err(Error::Config(ConfigError::InvalidValue {
                field: "sqllog.directory".to_string(),
                value: self.directory.clone(),
                reason: "Input directory cannot be empty".to_string(),
            }));
        }
        Ok(())
    }
}

#[derive(Debug, Deserialize, Clone)]
pub struct ErrorConfig {
    /// 错误日志输出文件路径
    #[serde(default = "default_error_file")]
    pub file: String,
}

fn default_error_file() -> String {
    "export/errors.log".to_string()
}

impl ErrorConfig {
    /// 获取错误日志输出文件路径
    #[must_use]
    pub fn file(&self) -> &str {
        &self.file
    }
}

impl Default for ErrorConfig {
    fn default() -> Self {
        Self {
            file: "export/errors.log".to_string(),
        }
    }
}

#[derive(Debug, Deserialize, Clone)]
pub struct LoggingConfig {
    /// 应用日志输出文件路径
    #[serde(default = "default_logging_file")]
    pub file: String,
    #[serde(default = "default_logging_level")]
    pub level: String,
    #[serde(default = "default_retention_days")]
    pub retention_days: usize,
}

fn default_logging_file() -> String {
    "logs/sqllog2db.log".to_string()
}

fn default_logging_level() -> String {
    "info".to_string()
}

fn default_retention_days() -> usize {
    7
}

impl LoggingConfig {
    /// 获取日志输出文件路径
    #[must_use]
    pub fn file(&self) -> &str {
        &self.file
    }

    /// 获取日志级别
    #[must_use]
    pub fn level(&self) -> &str {
        &self.level
    }

    /// 获取日志保留天数
    #[must_use]
    pub fn retention_days(&self) -> usize {
        self.retention_days
    }

    /// 验证日志级别是否有效
    pub fn validate(&self) -> Result<()> {
        if !LOG_LEVELS
            .iter()
            .any(|&l| l.eq_ignore_ascii_case(self.level.as_str()))
        {
            return Err(Error::Config(ConfigError::InvalidLogLevel {
                level: self.level.clone(),
                valid_levels: LOG_LEVELS.iter().map(|s| (*s).to_string()).collect(),
            }));
        }

        // 验证保留天数(1-365天)
        if self.retention_days == 0 || self.retention_days > 365 {
            return Err(Error::Config(ConfigError::InvalidValue {
                field: "logging.retention_days".to_string(),
                value: self.retention_days.to_string(),
                reason: "Retention days must be between 1 and 365".to_string(),
            }));
        }

        Ok(())
    }
}

impl Default for LoggingConfig {
    fn default() -> Self {
        Self {
            file: "logs/sqllog2db.log".to_string(),
            level: "info".to_string(),
            retention_days: 7,
        }
    }
}

#[derive(Debug, Deserialize, Clone)]
pub struct ExporterConfig {
    #[cfg(feature = "csv")]
    pub csv: Option<CsvExporter>,
    #[cfg(feature = "jsonl")]
    pub jsonl: Option<JsonlExporter>,
    #[cfg(feature = "sqlite")]
    pub sqlite: Option<SqliteExporter>,
}

impl ExporterConfig {
    /// 获取 CSV 导出器配置
    #[cfg(feature = "csv")]
    #[must_use]
    pub fn csv(&self) -> Option<&CsvExporter> {
        self.csv.as_ref()
    }

    #[cfg(feature = "jsonl")]
    /// 获取 JSONL 导出器配置
    #[must_use]
    pub fn jsonl(&self) -> Option<&JsonlExporter> {
        self.jsonl.as_ref()
    }

    #[cfg(feature = "sqlite")]
    /// 获取 `SQLite` 导出器配置
    #[must_use]
    pub fn sqlite(&self) -> Option<&SqliteExporter> {
        self.sqlite.as_ref()
    }

    /// 检查是否有任何导出器配置
    #[must_use]
    pub fn has_exporters(&self) -> bool {
        let mut found = false;
        #[cfg(feature = "csv")]
        {
            found = found || self.csv.is_some();
        }
        #[cfg(feature = "jsonl")]
        {
            found = found || self.jsonl.is_some();
        }
        #[cfg(feature = "sqlite")]
        {
            found = found || self.sqlite.is_some();
        }
        found
    }

    /// 统计配置的导出器总数
    #[must_use]
    pub fn total_exporters(&self) -> usize {
        let mut count = 0;
        #[cfg(feature = "csv")]
        {
            if self.csv.is_some() {
                count += 1;
            }
        }
        #[cfg(feature = "jsonl")]
        {
            if self.jsonl.is_some() {
                count += 1;
            }
        }
        #[cfg(feature = "sqlite")]
        {
            if self.sqlite.is_some() {
                count += 1;
            }
        }
        count
    }

    /// 验证导出器配置(只支持单个导出器)
    pub fn validate(&self) -> Result<()> {
        if !self.has_exporters() {
            return Err(Error::Config(ConfigError::NoExporters));
        }

        let total = self.total_exporters();
        if total > 1 {
            eprintln!("Warning: {total} exporters configured, but only one is supported.");
            eprintln!("Will use the first exporter by priority: CSV > JSONL > SQLite");
        }

        Ok(())
    }
}

impl Default for ExporterConfig {
    fn default() -> Self {
        Self {
            #[cfg(feature = "csv")]
            csv: Some(CsvExporter::default()),
            #[cfg(feature = "jsonl")]
            jsonl: None,
            #[cfg(feature = "sqlite")]
            sqlite: None,
        }
    }
}

#[cfg(feature = "jsonl")]
#[derive(Debug, Deserialize, Clone)]
pub struct JsonlExporter {
    /// JSONL 输出文件路径
    pub file: String,
    /// 是否覆盖已存在的文件
    #[serde(default = "default_true")]
    pub overwrite: bool,
    /// 是否追加模式
    #[serde(default)]
    pub append: bool,
}

#[cfg(feature = "jsonl")]
impl Default for JsonlExporter {
    fn default() -> Self {
        Self {
            file: "export/sqllog2db.jsonl".to_string(),
            overwrite: true,
            append: false,
        }
    }
}

#[cfg(feature = "sqlite")]
#[derive(Debug, Deserialize, Clone)]
pub struct SqliteExporter {
    /// `SQLite` 数据库文件路径
    pub database_url: String,
    /// 表名
    #[serde(default = "default_table_name")]
    pub table_name: String,
    /// 是否覆盖已存在的表
    #[serde(default = "default_true")]
    pub overwrite: bool,
    /// 是否追加模式
    #[serde(default)]
    pub append: bool,
}

#[cfg(feature = "sqlite")]
impl Default for SqliteExporter {
    fn default() -> Self {
        Self {
            database_url: "export/sqllog2db.db".to_string(),
            table_name: "sqllog_records".to_string(),
            overwrite: true,
            append: false,
        }
    }
}

#[cfg(feature = "csv")]
#[derive(Debug, Deserialize, Clone)]
pub struct CsvExporter {
    /// CSV 输出文件路径
    pub file: String,
    /// 是否覆盖已存在的文件
    #[serde(default = "default_true")]
    pub overwrite: bool,
    /// 是否追加模式(暂未实现)
    #[serde(default)]
    pub append: bool,
}

#[cfg(feature = "csv")]
impl Default for CsvExporter {
    fn default() -> Self {
        Self {
            file: "outputs/sqllog.csv".to_string(),
            overwrite: true,
            append: false,
        }
    }
}