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()
}
fn default_true() -> bool {
true
}
#[cfg_attr(feature = "csv", derive(Default))]
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
#[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()?;
self.sqllog.validate()?;
FeaturesConfig::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 {
#[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(),
}));
}
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 {
#[cfg(feature = "csv")]
#[must_use]
pub fn csv(&self) -> Option<&CsvExporter> {
self.csv.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()
}
#[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 {
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 {
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 {
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,
}
}
}