use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogConfig {
pub general: GeneralConfig,
pub outputs: OutputsConfig,
pub features: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneralConfig {
pub default_level: String,
pub enable_colors: bool,
pub enable_correlation_ids: bool,
pub max_correlation_id_length: usize,
pub app_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OutputsConfig {
pub console: ConsoleConfig,
pub file: FileConfig,
pub web: WebConfig,
pub structured: StructuredConfig,
#[cfg(feature = "aws-backend")]
pub dynamodb: DynamoConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsoleConfig {
pub enabled: bool,
pub level: String,
pub colors: bool,
pub include_timestamp: bool,
pub include_module: bool,
pub include_thread: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileConfig {
pub enabled: bool,
pub path: String,
pub level: String,
pub max_size: String,
pub max_files: u32,
pub include_timestamp: bool,
pub include_module: bool,
pub include_thread: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfig {
pub enabled: bool,
pub level: String,
pub buffer_size: usize,
pub enable_filtering: bool,
pub max_logs: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructuredConfig {
pub enabled: bool,
pub level: String,
pub path: Option<String>,
pub include_context: bool,
pub include_metrics: bool,
}
#[cfg(feature = "aws-backend")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DynamoConfig {
pub enabled: bool,
pub level: String,
pub table_name: String,
pub region: Option<String>,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
general: GeneralConfig::default(),
outputs: OutputsConfig::default(),
features: Self::default_features(),
}
}
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
default_level: "INFO".to_string(),
enable_colors: true,
enable_correlation_ids: true,
max_correlation_id_length: 64,
app_id: None,
}
}
}
impl Default for ConsoleConfig {
fn default() -> Self {
Self {
enabled: true,
level: "INFO".to_string(),
colors: true,
include_timestamp: true,
include_module: true,
include_thread: false,
}
}
}
impl Default for FileConfig {
fn default() -> Self {
Self {
enabled: false,
path: "logs/datafold.log".to_string(),
level: "DEBUG".to_string(),
max_size: "10MB".to_string(),
max_files: 5,
include_timestamp: true,
include_module: true,
include_thread: true,
}
}
}
impl Default for WebConfig {
fn default() -> Self {
Self {
enabled: true,
level: "INFO".to_string(),
buffer_size: 1000,
enable_filtering: true,
max_logs: 5000,
}
}
}
impl Default for StructuredConfig {
fn default() -> Self {
Self {
enabled: false,
level: "DEBUG".to_string(),
path: Some("logs/datafold-structured.json".to_string()),
include_context: true,
include_metrics: false,
}
}
}
#[cfg(feature = "aws-backend")]
impl Default for DynamoConfig {
fn default() -> Self {
Self {
enabled: false,
level: "INFO".to_string(),
table_name: "datafold-logs".to_string(),
region: None,
}
}
}
impl LogConfig {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
let content = std::fs::read_to_string(path.as_ref()).map_err(ConfigError::Io)?;
let mut config: LogConfig =
toml::from_str(&content).map_err(|e| ConfigError::Parse(e.to_string()))?;
config.apply_env_overrides()?;
Ok(config)
}
pub fn from_env() -> Result<Self, ConfigError> {
let mut config = Self::default();
config.apply_env_overrides()?;
Ok(config)
}
pub fn apply_env_overrides(&mut self) -> Result<(), ConfigError> {
if let Ok(level) = std::env::var("DATAFOLD_LOG_LEVEL") {
self.general.default_level = level;
}
if let Ok(colors) = std::env::var("DATAFOLD_LOG_COLORS") {
self.general.enable_colors = colors.parse().unwrap_or(true);
}
if let Ok(enabled) = std::env::var("DATAFOLD_LOG_CONSOLE_ENABLED") {
self.outputs.console.enabled = enabled.parse().unwrap_or(true);
}
if let Ok(level) = std::env::var("DATAFOLD_LOG_CONSOLE_LEVEL") {
self.outputs.console.level = level;
}
if let Ok(enabled) = std::env::var("DATAFOLD_LOG_FILE_ENABLED") {
self.outputs.file.enabled = enabled.parse().unwrap_or(false);
}
if let Ok(path) = std::env::var("DATAFOLD_LOG_FILE_PATH") {
self.outputs.file.path = path;
}
if let Ok(level) = std::env::var("DATAFOLD_LOG_FILE_LEVEL") {
self.outputs.file.level = level;
}
if let Ok(enabled) = std::env::var("DATAFOLD_LOG_WEB_ENABLED") {
self.outputs.web.enabled = enabled.parse().unwrap_or(true);
}
if let Ok(level) = std::env::var("DATAFOLD_LOG_WEB_LEVEL") {
self.outputs.web.level = level;
}
#[cfg(feature = "aws-backend")]
{
if let Ok(enabled) = std::env::var("DATAFOLD_LOG_DYNAMODB_ENABLED") {
self.outputs.dynamodb.enabled = enabled.parse().unwrap_or(false);
}
if let Ok(level) = std::env::var("DATAFOLD_LOG_DYNAMODB_LEVEL") {
self.outputs.dynamodb.level = level;
}
if let Ok(table) = std::env::var("DATAFOLD_LOG_DYNAMODB_TABLE") {
self.outputs.dynamodb.table_name = table;
}
if let Ok(region) = std::env::var("DATAFOLD_LOG_DYNAMODB_REGION") {
self.outputs.dynamodb.region = Some(region);
}
}
for (key, value) in std::env::vars() {
if let Some(feature) = key.strip_prefix("DATAFOLD_LOG_FEATURE_") {
let feature_name = feature.to_lowercase();
self.features.insert(feature_name, value);
}
}
Ok(())
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), ConfigError> {
let content =
toml::to_string_pretty(self).map_err(|e| ConfigError::Serialize(e.to_string()))?;
if let Some(parent) = path.as_ref().parent() {
std::fs::create_dir_all(parent).map_err(ConfigError::Io)?;
}
std::fs::write(path, content).map_err(ConfigError::Io)?;
Ok(())
}
fn default_features() -> HashMap<String, String> {
let mut features = HashMap::new();
features.insert("query".to_string(), "INFO".to_string());
features.insert("mutation".to_string(), "INFO".to_string());
features.insert("schema".to_string(), "INFO".to_string());
features.insert("ingestion".to_string(), "INFO".to_string());
features.insert("transform".to_string(), "DEBUG".to_string());
features.insert("network".to_string(), "INFO".to_string());
features.insert("permissions".to_string(), "INFO".to_string());
features.insert("http_server".to_string(), "DEBUG".to_string());
features.insert("tcp_server".to_string(), "INFO".to_string());
features.insert("database".to_string(), "WARN".to_string());
features
}
pub fn validate(&self) -> Result<(), ConfigError> {
let valid_levels = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
if !valid_levels.contains(&self.general.default_level.as_str()) {
return Err(ConfigError::InvalidLevel(
self.general.default_level.clone(),
));
}
if !valid_levels.contains(&self.outputs.console.level.as_str()) {
return Err(ConfigError::InvalidLevel(
self.outputs.console.level.clone(),
));
}
if !valid_levels.contains(&self.outputs.file.level.as_str()) {
return Err(ConfigError::InvalidLevel(self.outputs.file.level.clone()));
}
if !valid_levels.contains(&self.outputs.web.level.as_str()) {
return Err(ConfigError::InvalidLevel(self.outputs.web.level.clone()));
}
if !valid_levels.contains(&self.outputs.structured.level.as_str()) {
return Err(ConfigError::InvalidLevel(
self.outputs.structured.level.clone(),
));
}
#[cfg(feature = "aws-backend")]
if !valid_levels.contains(&self.outputs.dynamodb.level.as_str()) {
return Err(ConfigError::InvalidLevel(
self.outputs.dynamodb.level.clone(),
));
}
for (feature, level) in &self.features {
if !valid_levels.contains(&level.as_str()) {
return Err(ConfigError::InvalidFeatureLevel(
feature.clone(),
level.clone(),
));
}
}
if self.outputs.file.enabled {
self.parse_file_size(&self.outputs.file.max_size)?;
}
Ok(())
}
fn parse_file_size(&self, size_str: &str) -> Result<u64, ConfigError> {
let size_str = size_str.to_uppercase();
if let Some(num_str) = size_str.strip_suffix("GB") {
let num: u64 = num_str
.parse()
.map_err(|_| ConfigError::InvalidFileSize(size_str.clone()))?;
Ok(num * 1024 * 1024 * 1024)
} else if let Some(num_str) = size_str.strip_suffix("MB") {
let num: u64 = num_str
.parse()
.map_err(|_| ConfigError::InvalidFileSize(size_str.clone()))?;
Ok(num * 1024 * 1024)
} else if let Some(num_str) = size_str.strip_suffix("KB") {
let num: u64 = num_str
.parse()
.map_err(|_| ConfigError::InvalidFileSize(size_str.clone()))?;
Ok(num * 1024)
} else if let Some(num_str) = size_str.strip_suffix("B") {
num_str
.parse()
.map_err(|_| ConfigError::InvalidFileSize(size_str.clone()))
} else {
size_str
.parse()
.map_err(|_| ConfigError::InvalidFileSize(size_str.clone()))
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse configuration: {0}")]
Parse(String),
#[error("Failed to serialize configuration: {0}")]
Serialize(String),
#[error("Invalid log level: {0}")]
InvalidLevel(String),
#[error("Invalid log level for feature '{0}': {1}")]
InvalidFeatureLevel(String, String),
#[error("Invalid file size format: {0}")]
InvalidFileSize(String),
}