#include "smartbotic/database/config.hpp" #include #include #include #include #include #include #include namespace smartbotic::database { auto LogLevelToString(LogLevel level) -> std::string { switch (level) { case LogLevel::kTrace: return "trace"; case LogLevel::kDebug: return "debug"; case LogLevel::kInfo: return "info"; case LogLevel::kWarn: return "warn"; case LogLevel::kError: return "error"; case LogLevel::kCritical: return "critical"; case LogLevel::kOff: return "off"; } return "info"; } auto ParseLogLevel(const std::string& str) -> std::optional { std::string lower; lower.reserve(str.size()); std::transform(str.begin(), str.end(), std::back_inserter(lower), [](unsigned char c) { return std::tolower(c); }); if (lower == "trace") return LogLevel::kTrace; if (lower == "debug") return LogLevel::kDebug; if (lower == "info") return LogLevel::kInfo; if (lower == "warn" || lower == "warning") return LogLevel::kWarn; if (lower == "error" || lower == "err") return LogLevel::kError; if (lower == "critical" || lower == "crit" || lower == "fatal") return LogLevel::kCritical; if (lower == "off" || lower == "none") return LogLevel::kOff; return std::nullopt; } namespace { auto GetEnvOrDefault(const char* name, const std::string& default_value) -> std::string { const char* value = std::getenv(name); return value != nullptr ? std::string(value) : default_value; } auto GetEnvOrDefault(const char* name, uint16_t default_value) -> uint16_t { const char* value = std::getenv(name); if (value == nullptr) { return default_value; } try { return static_cast(std::stoi(value)); } catch (...) { return default_value; } } auto GetEnvOrDefault(const char* name, int64_t default_value) -> int64_t { const char* value = std::getenv(name); if (value == nullptr) { return default_value; } try { return std::stoll(value); } catch (...) { return default_value; } } } // namespace auto ConfigLoader::GetDefaultConfigPaths() -> std::vector { std::vector paths; // Current directory paths.emplace_back("./smartbotic-db.json"); // Config subdirectory paths.emplace_back("./config/smartbotic-db.json"); // /etc for system-wide config paths.emplace_back("/etc/smartbotic/smartbotic-db.json"); // Home directory const char* home = std::getenv("HOME"); if (home != nullptr) { paths.push_back(std::string(home) + "/.config/smartbotic/smartbotic-db.json"); } return paths; } auto ConfigLoader::FindConfigFile() -> std::optional { // Check environment variable for explicit config path const char* config_path_env = std::getenv("SMARTBOTIC_CRM_DB_CONFIG"); if (config_path_env != nullptr && std::filesystem::exists(config_path_env)) { return std::string(config_path_env); } // Search default paths for (const auto& path : GetDefaultConfigPaths()) { if (std::filesystem::exists(path)) { return path; } } return std::nullopt; } auto ConfigLoader::LoadFromFile(const std::string& path) -> std::optional { try { if (!std::filesystem::exists(path)) { spdlog::warn("Config file not found: {}", path); return std::nullopt; } std::ifstream file(path); if (!file.is_open()) { spdlog::error("Failed to open config file: {}", path); return std::nullopt; } nlohmann::json json = nlohmann::json::parse(file); DatabaseConfig config; // Server section if (json.contains("server")) { const auto& server = json["server"]; if (server.contains("address")) { config.address = server["address"].get(); } if (server.contains("port")) { config.port = server["port"].get(); } } // Storage section if (json.contains("storage")) { const auto& storage = json["storage"]; if (storage.contains("data_dir")) { config.data_dir = storage["data_dir"].get(); } } // Snapshot section if (json.contains("snapshot")) { const auto& snapshot = json["snapshot"]; if (snapshot.contains("enabled")) { config.snapshot_enabled = snapshot["enabled"].get(); } if (snapshot.contains("interval_seconds")) { config.snapshot_interval_seconds = snapshot["interval_seconds"].get(); } if (snapshot.contains("change_threshold")) { config.snapshot_change_threshold = snapshot["change_threshold"].get(); } } // Logging section if (json.contains("logging")) { const auto& logging = json["logging"]; if (logging.contains("level")) { auto level_str = logging["level"].get(); auto level = ParseLogLevel(level_str); if (level) { config.log_level = *level; } else { spdlog::warn("Invalid log level '{}', using default", level_str); } } } // Security section if (json.contains("security")) { const auto& security = json["security"]; if (security.contains("encryption_key_path")) { config.encryption_key_path = security["encryption_key_path"].get(); } } spdlog::info("Loaded configuration from: {}", path); return config; } catch (const nlohmann::json::exception& e) { spdlog::error("Failed to parse config file {}: {}", path, e.what()); return std::nullopt; } catch (const std::exception& e) { spdlog::error("Error loading config file {}: {}", path, e.what()); return std::nullopt; } } void ConfigLoader::ApplyEnvironment(DatabaseConfig& config) { // Server settings from environment (can override config file) config.address = GetEnvOrDefault("SMARTBOTIC_CRM_DB_ADDRESS", config.address); config.port = GetEnvOrDefault("SMARTBOTIC_CRM_DB_PORT", config.port); config.data_dir = GetEnvOrDefault("SMARTBOTIC_CRM_DB_DATA_DIR", config.data_dir); // Snapshot settings from environment config.snapshot_interval_seconds = GetEnvOrDefault("SMARTBOTIC_CRM_DB_SNAPSHOT_INTERVAL", config.snapshot_interval_seconds); // Log level from environment const char* log_level_env = std::getenv("SMARTBOTIC_CRM_DB_LOG_LEVEL"); if (log_level_env != nullptr) { auto level = ParseLogLevel(log_level_env); if (level) { config.log_level = *level; } } // Encryption key path from environment (for secrets) const char* key_path_env = std::getenv("SMARTBOTIC_CRM_DB_ENCRYPTION_KEY_PATH"); if (key_path_env != nullptr) { config.encryption_key_path = key_path_env; } } auto ConfigLoader::ParseCommandLine(int argc, char* argv[], DatabaseConfig& config) -> std::optional { std::string config_file_override; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--help" || arg == "-h") { PrintHelp(argv[0]); return std::nullopt; // Help requested, exit } if (arg == "--version" || arg == "-v") { spdlog::info("SmartBotic Database v0.1.0"); return std::nullopt; // Version requested, exit } if (arg == "--config" || arg == "-c") { if (i + 1 < argc) { config_file_override = argv[++i]; } else { spdlog::error("--config requires a path argument"); return false; } continue; } if (arg == "--port" || arg == "-p") { if (i + 1 < argc) { try { config.port = static_cast(std::stoi(argv[++i])); } catch (...) { spdlog::error("Invalid port number: {}", argv[i]); return false; } } else { spdlog::error("--port requires a number argument"); return false; } continue; } if (arg == "--address" || arg == "-a") { if (i + 1 < argc) { config.address = argv[++i]; } else { spdlog::error("--address requires an address argument"); return false; } continue; } if (arg == "--data-dir" || arg == "-d") { if (i + 1 < argc) { config.data_dir = argv[++i]; } else { spdlog::error("--data-dir requires a path argument"); return false; } continue; } if (arg == "--snapshot-interval") { if (i + 1 < argc) { try { config.snapshot_interval_seconds = std::stoll(argv[++i]); } catch (...) { spdlog::error("Invalid snapshot interval: {}", argv[i]); return false; } } else { spdlog::error("--snapshot-interval requires a number argument"); return false; } continue; } if (arg == "--log-level" || arg == "-l") { if (i + 1 < argc) { auto level = ParseLogLevel(argv[++i]); if (level) { config.log_level = *level; } else { spdlog::error("Invalid log level: {}", argv[i]); return false; } } else { spdlog::error("--log-level requires a level argument"); return false; } continue; } if (arg == "--encryption-key-path") { if (i + 1 < argc) { config.encryption_key_path = argv[++i]; } else { spdlog::error("--encryption-key-path requires a path argument"); return false; } continue; } spdlog::error("Unknown argument: {}", arg); PrintHelp(argv[0]); return false; } // If config file was specified on command line, load it if (!config_file_override.empty()) { auto file_config = LoadFromFile(config_file_override); if (!file_config) { spdlog::error("Failed to load specified config file: {}", config_file_override); return false; } // Start with file config, then re-apply command line args below config = *file_config; // Re-parse command line to override file settings // (Skip the config file argument this time) for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--config" || arg == "-c") { ++i; // Skip the config path continue; } if ((arg == "--port" || arg == "-p") && i + 1 < argc) { config.port = static_cast(std::stoi(argv[++i])); } else if ((arg == "--address" || arg == "-a") && i + 1 < argc) { config.address = argv[++i]; } else if ((arg == "--data-dir" || arg == "-d") && i + 1 < argc) { config.data_dir = argv[++i]; } else if (arg == "--snapshot-interval" && i + 1 < argc) { config.snapshot_interval_seconds = std::stoll(argv[++i]); } else if ((arg == "--log-level" || arg == "-l") && i + 1 < argc) { auto level = ParseLogLevel(argv[++i]); if (level) config.log_level = *level; } else if (arg == "--encryption-key-path" && i + 1 < argc) { config.encryption_key_path = argv[++i]; } } } return true; } auto ConfigLoader::Load(int argc, char* argv[]) -> DatabaseConfig { DatabaseConfig config = DatabaseConfig::Default(); // 1. Try to find and load config file (lowest priority for non-secrets) auto config_file = FindConfigFile(); if (config_file) { auto file_config = LoadFromFile(*config_file); if (file_config) { config = *file_config; } } // 2. Apply environment variables (can override config file) ApplyEnvironment(config); // 3. Parse command-line arguments (highest priority) auto result = ParseCommandLine(argc, argv, config); if (!result.has_value()) { // Help or version was requested, exit cleanly std::exit(0); } if (!result.value()) { // Error parsing arguments std::exit(1); } return config; } void ConfigLoader::PrintHelp(const char* program_name) { spdlog::info("Usage: {} [options]", program_name); spdlog::info(""); spdlog::info("SmartBotic Database Service"); spdlog::info(""); spdlog::info("Options:"); spdlog::info(" -h, --help Show this help message"); spdlog::info(" -v, --version Show version information"); spdlog::info(" -c, --config Path to config file (default: smartbotic-db.json)"); spdlog::info(" -p, --port Port to listen on (default: 50051)"); spdlog::info(" -a, --address Address to bind to (default: 0.0.0.0)"); spdlog::info(" -d, --data-dir Data directory (default: ./data)"); spdlog::info(" --snapshot-interval Snapshot interval in seconds (default: 300)"); spdlog::info(" -l, --log-level Log level: trace, debug, info, warn, error, critical, off"); spdlog::info(" --encryption-key-path

Path to encryption key file"); spdlog::info(""); spdlog::info("Environment Variables:"); spdlog::info(" SMARTBOTIC_CRM_DB_CONFIG Path to config file"); spdlog::info(" SMARTBOTIC_CRM_DB_ADDRESS Address to bind to"); spdlog::info(" SMARTBOTIC_CRM_DB_PORT Port to listen on"); spdlog::info(" SMARTBOTIC_CRM_DB_DATA_DIR Data directory"); spdlog::info(" SMARTBOTIC_CRM_DB_SNAPSHOT_INTERVAL Snapshot interval in seconds"); spdlog::info(" SMARTBOTIC_CRM_DB_LOG_LEVEL Log level"); spdlog::info(" SMARTBOTIC_CRM_DB_ENCRYPTION_KEY_PATH Path to encryption key file"); spdlog::info(" SMARTBOTIC_CRM_DB_ENCRYPTION_KEY Encryption key (base64 encoded) - for secrets"); spdlog::info(""); spdlog::info("Config File Search Order:"); for (const auto& path : GetDefaultConfigPaths()) { spdlog::info(" {}", path); } } void ConfigLoader::PrintConfig(const DatabaseConfig& config) { spdlog::info("Configuration:"); spdlog::info(" Address: {}", config.address); spdlog::info(" Port: {}", config.port); spdlog::info(" Data Directory: {}", config.data_dir); spdlog::info(" Snapshot Interval: {} seconds", config.snapshot_interval_seconds); spdlog::info(" Snapshot Enabled: {}", config.snapshot_enabled ? "yes" : "no"); spdlog::info(" Log Level: {}", LogLevelToString(config.log_level)); if (!config.encryption_key_path.empty()) { spdlog::info(" Encryption Key: {} (path configured)", config.encryption_key_path); } else { const char* key_env = std::getenv("SMARTBOTIC_CRM_DB_ENCRYPTION_KEY"); if (key_env != nullptr) { spdlog::info(" Encryption Key: (from environment variable)"); } else { spdlog::info(" Encryption Key: (not configured)"); } } } } // namespace smartbotic::database