use crate::constants::LOG_LEVELS;
use crate::error::{ConfigError, Error, Result};
use serde::Deserialize;
use std::path::{Path, PathBuf};
#[cfg(any(
feature = "sqlite",
feature = "duckdb",
feature = "postgres",
feature = "dm"
))]
fn default_table_name() -> String {
"sqllog_records".to_string()
}
#[cfg(any(
feature = "sqlite",
feature = "duckdb",
feature = "postgres",
feature = "dm"
))]
fn default_true() -> bool {
true
}
#[cfg(feature = "postgres")]
fn default_postgres_host() -> String {
"localhost".to_string()
}
#[cfg(feature = "postgres")]
fn default_postgres_port() -> u16 {
5432
}
#[cfg(feature = "postgres")]
fn default_postgres_username() -> String {
"postgres".to_string()
}
#[cfg(feature = "postgres")]
fn default_postgres_database() -> String {
"sqllog".to_string()
}
#[cfg(feature = "postgres")]
fn default_postgres_schema() -> String {
"public".to_string()
}
#[cfg_attr(feature = "csv", derive(Default))]
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
#[serde(default)]
pub sqllog: SqllogConfig,
pub error: ErrorConfig,
pub logging: LoggingConfig,
pub features: FeaturesConfig,
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()?;
self.sqllog.validate()?;
Ok(())
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct SqllogConfig {
pub directory: String,
}
impl Default for SqllogConfig {
fn default() -> Self {
Self {
directory: "sqllogs".to_string(),
}
}
}
impl SqllogConfig {
#[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 {
pub file: 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 {
pub file: String,
pub level: String,
#[serde(default = "default_retention_days")]
pub retention_days: usize,
}
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(),
}));
}
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 ReplaceParametersFeature {
pub enable: bool,
pub symbols: Option<Vec<String>>,
}
#[derive(Debug, Deserialize, Clone, Default)]
pub struct FeaturesConfig {
#[serde(default)]
pub replace_parameters: Option<ReplaceParametersFeature>,
}
impl FeaturesConfig {
#[must_use]
pub fn should_replace_sql_parameters(&self) -> bool {
self.replace_parameters.as_ref().is_some_and(|f| f.enable)
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct ExporterConfig {
#[cfg(feature = "csv")]
pub csv: Option<CsvExporter>,
#[cfg(feature = "parquet")]
pub parquet: Option<ParquetExporter>,
#[cfg(feature = "jsonl")]
pub jsonl: Option<JsonlExporter>,
#[cfg(feature = "sqlite")]
pub sqlite: Option<SqliteExporter>,
#[cfg(feature = "duckdb")]
pub duckdb: Option<DuckdbExporter>,
#[cfg(feature = "postgres")]
pub postgres: Option<PostgresExporter>,
#[cfg(feature = "dm")]
pub dm: Option<DmExporter>,
}
impl ExporterConfig {
#[cfg(feature = "csv")]
#[must_use]
pub fn csv(&self) -> Option<&CsvExporter> {
self.csv.as_ref()
}
#[cfg(feature = "parquet")]
#[must_use]
pub fn parquet(&self) -> Option<&ParquetExporter> {
self.parquet.as_ref()
}
#[cfg(feature = "jsonl")]
#[must_use]
pub fn jsonl(&self) -> Option<&JsonlExporter> {
self.jsonl.as_ref()
}
#[cfg(feature = "sqlite")]
#[must_use]
pub fn sqlite(&self) -> Option<&SqliteExporter> {
self.sqlite.as_ref()
}
#[cfg(feature = "duckdb")]
#[must_use]
pub fn duckdb(&self) -> Option<&DuckdbExporter> {
self.duckdb.as_ref()
}
#[cfg(feature = "postgres")]
#[must_use]
pub fn postgres(&self) -> Option<&PostgresExporter> {
self.postgres.as_ref()
}
#[cfg(feature = "dm")]
#[must_use]
pub fn dm(&self) -> Option<&DmExporter> {
self.dm.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 = "parquet")]
{
found = found || self.parquet.is_some();
}
#[cfg(feature = "jsonl")]
{
found = found || self.jsonl.is_some();
}
#[cfg(feature = "sqlite")]
{
found = found || self.sqlite.is_some();
}
#[cfg(feature = "duckdb")]
{
found = found || self.duckdb.is_some();
}
#[cfg(feature = "postgres")]
{
found = found || self.postgres.is_some();
}
#[cfg(feature = "dm")]
{
found = found || self.dm.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 = "parquet")]
{
if self.parquet.is_some() {
count += 1;
}
}
#[cfg(feature = "jsonl")]
{
if self.jsonl.is_some() {
count += 1;
}
}
#[cfg(feature = "sqlite")]
{
if self.sqlite.is_some() {
count += 1;
}
}
#[cfg(feature = "duckdb")]
{
if self.duckdb.is_some() {
count += 1;
}
}
#[cfg(feature = "postgres")]
{
if self.postgres.is_some() {
count += 1;
}
}
#[cfg(feature = "dm")]
{
if self.dm.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 > Parquet > JSONL > SQLite > DuckDB > PostgreSQL > DM"
);
}
Ok(())
}
}
impl Default for ExporterConfig {
fn default() -> Self {
Self {
#[cfg(feature = "csv")]
csv: Some(CsvExporter::default()),
#[cfg(feature = "parquet")]
parquet: Some(ParquetExporter::default()),
#[cfg(feature = "jsonl")]
jsonl: None,
#[cfg(feature = "sqlite")]
sqlite: None,
#[cfg(feature = "duckdb")]
duckdb: None,
#[cfg(feature = "postgres")]
postgres: None,
#[cfg(feature = "dm")]
dm: None,
}
}
}
#[cfg(feature = "parquet")]
#[derive(Debug, Deserialize, Clone)]
pub struct ParquetExporter {
pub file: String,
pub overwrite: bool,
pub row_group_size: Option<usize>,
pub use_dictionary: Option<bool>,
}
#[cfg(feature = "parquet")]
impl Default for ParquetExporter {
fn default() -> Self {
Self {
file: "export/sqllog2db.parquet".to_string(),
overwrite: true,
row_group_size: Some(100_000),
use_dictionary: Some(true),
}
}
}
#[cfg(feature = "csv")]
#[derive(Debug, Deserialize, Clone)]
pub struct CsvExporter {
pub file: String,
pub overwrite: bool,
pub append: bool,
}
#[cfg(feature = "csv")]
impl Default for CsvExporter {
fn default() -> Self {
Self {
file: "outputs/sqllog.csv".to_string(),
overwrite: true,
append: false,
}
}
}
#[cfg(feature = "jsonl")]
#[derive(Debug, Deserialize, Clone)]
pub struct JsonlExporter {
pub file: String,
pub overwrite: bool,
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 {
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 = "duckdb")]
#[derive(Debug, Deserialize, Clone)]
pub struct DuckdbExporter {
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 = "duckdb")]
impl Default for DuckdbExporter {
fn default() -> Self {
Self {
database_url: "export/sqllog2db.duckdb".to_string(),
table_name: "sqllog_records".to_string(),
overwrite: true,
append: false,
}
}
}
#[cfg(feature = "postgres")]
#[derive(Debug, Deserialize, Clone)]
pub struct PostgresExporter {
#[serde(default = "default_postgres_host")]
pub host: String,
#[serde(default = "default_postgres_port")]
pub port: u16,
#[serde(default = "default_postgres_username")]
pub username: String,
pub password: String,
#[serde(default = "default_postgres_database")]
pub database: String,
#[serde(default = "default_postgres_schema")]
pub schema: String,
#[serde(default = "default_table_name")]
pub table_name: String,
#[serde(default = "default_true")]
pub overwrite: bool,
#[serde(default)]
pub append: bool,
}
#[cfg(feature = "postgres")]
impl Default for PostgresExporter {
fn default() -> Self {
Self {
host: "localhost".to_string(),
port: 5432,
username: "postgres".to_string(),
password: "postgres".to_string(),
database: "sqllog".to_string(),
schema: "public".to_string(),
table_name: "sqllog_records".to_string(),
overwrite: true,
append: false,
}
}
}
#[cfg(feature = "postgres")]
impl PostgresExporter {
#[must_use]
pub fn connection_string(&self) -> String {
if self.password.is_empty() {
format!(
"host={} port={} user={} dbname={}",
self.host, self.port, self.username, self.database
)
} else {
format!(
"host={} port={} user={} password={} dbname={}",
self.host, self.port, self.username, self.password, self.database
)
}
}
}
#[cfg(feature = "dm")]
#[derive(Debug, Deserialize, Clone)]
pub struct DmExporter {
pub userid: String,
#[serde(default = "default_table_name")]
pub table_name: String,
pub control_file: String,
pub log_dir: String,
}
#[cfg(feature = "dm")]
impl Default for DmExporter {
fn default() -> Self {
Self {
userid: "SYSDBA/SYSDBA@localhost:5236".to_string(),
table_name: "sqllog_records".to_string(),
control_file: "export/sqllog.ctl".to_string(),
log_dir: "export/log".to_string(),
}
}
}