|
|
@@ -0,0 +1,456 @@
|
|
|
+#include "smartbotic/database/config.hpp"
|
|
|
+
|
|
|
+#include <spdlog/spdlog.h>
|
|
|
+#include <yaml-cpp/yaml.h>
|
|
|
+
|
|
|
+#include <algorithm>
|
|
|
+#include <cctype>
|
|
|
+#include <cstdlib>
|
|
|
+#include <filesystem>
|
|
|
+#include <fstream>
|
|
|
+
|
|
|
+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<LogLevel> {
|
|
|
+ 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<uint16_t>(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::string> {
|
|
|
+ std::vector<std::string> paths;
|
|
|
+
|
|
|
+ // Current directory
|
|
|
+ paths.emplace_back("./smartbotic-db.yaml");
|
|
|
+ paths.emplace_back("./smartbotic-db.yml");
|
|
|
+
|
|
|
+ // Config subdirectory
|
|
|
+ paths.emplace_back("./config/smartbotic-db.yaml");
|
|
|
+ paths.emplace_back("./config/smartbotic-db.yml");
|
|
|
+
|
|
|
+ // /etc for system-wide config
|
|
|
+ paths.emplace_back("/etc/smartbotic/smartbotic-db.yaml");
|
|
|
+ paths.emplace_back("/etc/smartbotic/smartbotic-db.yml");
|
|
|
+
|
|
|
+ // Home directory
|
|
|
+ const char* home = std::getenv("HOME");
|
|
|
+ if (home != nullptr) {
|
|
|
+ paths.push_back(std::string(home) + "/.config/smartbotic/smartbotic-db.yaml");
|
|
|
+ paths.push_back(std::string(home) + "/.config/smartbotic/smartbotic-db.yml");
|
|
|
+ }
|
|
|
+
|
|
|
+ return paths;
|
|
|
+}
|
|
|
+
|
|
|
+auto ConfigLoader::FindConfigFile() -> std::optional<std::string> {
|
|
|
+ // Check environment variable for explicit config path
|
|
|
+ const char* config_path_env = std::getenv("SMARTBOTIC_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<DatabaseConfig> {
|
|
|
+ try {
|
|
|
+ if (!std::filesystem::exists(path)) {
|
|
|
+ spdlog::warn("Config file not found: {}", path);
|
|
|
+ return std::nullopt;
|
|
|
+ }
|
|
|
+
|
|
|
+ YAML::Node yaml = YAML::LoadFile(path);
|
|
|
+ DatabaseConfig config;
|
|
|
+
|
|
|
+ // Server section
|
|
|
+ if (yaml["server"]) {
|
|
|
+ const auto& server = yaml["server"];
|
|
|
+ if (server["address"]) {
|
|
|
+ config.address = server["address"].as<std::string>();
|
|
|
+ }
|
|
|
+ if (server["port"]) {
|
|
|
+ config.port = server["port"].as<uint16_t>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Storage section
|
|
|
+ if (yaml["storage"]) {
|
|
|
+ const auto& storage = yaml["storage"];
|
|
|
+ if (storage["data_dir"]) {
|
|
|
+ config.data_dir = storage["data_dir"].as<std::string>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Snapshot section
|
|
|
+ if (yaml["snapshot"]) {
|
|
|
+ const auto& snapshot = yaml["snapshot"];
|
|
|
+ if (snapshot["enabled"]) {
|
|
|
+ config.snapshot_enabled = snapshot["enabled"].as<bool>();
|
|
|
+ }
|
|
|
+ if (snapshot["interval_seconds"]) {
|
|
|
+ config.snapshot_interval_seconds = snapshot["interval_seconds"].as<int64_t>();
|
|
|
+ }
|
|
|
+ if (snapshot["change_threshold"]) {
|
|
|
+ config.snapshot_change_threshold = snapshot["change_threshold"].as<uint64_t>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Logging section
|
|
|
+ if (yaml["logging"]) {
|
|
|
+ const auto& logging = yaml["logging"];
|
|
|
+ if (logging["level"]) {
|
|
|
+ auto level_str = logging["level"].as<std::string>();
|
|
|
+ auto level = ParseLogLevel(level_str);
|
|
|
+ if (level) {
|
|
|
+ config.log_level = *level;
|
|
|
+ } else {
|
|
|
+ spdlog::warn("Invalid log level '{}', using default", level_str);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Security section
|
|
|
+ if (yaml["security"]) {
|
|
|
+ const auto& security = yaml["security"];
|
|
|
+ if (security["encryption_key_path"]) {
|
|
|
+ config.encryption_key_path = security["encryption_key_path"].as<std::string>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ spdlog::info("Loaded configuration from: {}", path);
|
|
|
+ return config;
|
|
|
+
|
|
|
+ } catch (const YAML::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_DB_ADDRESS", config.address);
|
|
|
+ config.port = GetEnvOrDefault("SMARTBOTIC_DB_PORT", config.port);
|
|
|
+ config.data_dir = GetEnvOrDefault("SMARTBOTIC_DB_DATA_DIR", config.data_dir);
|
|
|
+
|
|
|
+ // Snapshot settings from environment
|
|
|
+ config.snapshot_interval_seconds =
|
|
|
+ GetEnvOrDefault("SMARTBOTIC_DB_SNAPSHOT_INTERVAL", config.snapshot_interval_seconds);
|
|
|
+
|
|
|
+ // Log level from environment
|
|
|
+ const char* log_level_env = std::getenv("SMARTBOTIC_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_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<bool> {
|
|
|
+ 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<uint16_t>(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<uint16_t>(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> Path to config file (default: smartbotic-db.yaml)");
|
|
|
+ spdlog::info(" -p, --port <port> Port to listen on (default: 50051)");
|
|
|
+ spdlog::info(" -a, --address <addr> Address to bind to (default: 0.0.0.0)");
|
|
|
+ spdlog::info(" -d, --data-dir <path> Data directory (default: ./data)");
|
|
|
+ spdlog::info(" --snapshot-interval <sec> Snapshot interval in seconds (default: 300)");
|
|
|
+ spdlog::info(" -l, --log-level <level> Log level: trace, debug, info, warn, error, critical, off");
|
|
|
+ spdlog::info(" --encryption-key-path <p> Path to encryption key file");
|
|
|
+ spdlog::info("");
|
|
|
+ spdlog::info("Environment Variables:");
|
|
|
+ spdlog::info(" SMARTBOTIC_DB_CONFIG Path to config file");
|
|
|
+ spdlog::info(" SMARTBOTIC_DB_ADDRESS Address to bind to");
|
|
|
+ spdlog::info(" SMARTBOTIC_DB_PORT Port to listen on");
|
|
|
+ spdlog::info(" SMARTBOTIC_DB_DATA_DIR Data directory");
|
|
|
+ spdlog::info(" SMARTBOTIC_DB_SNAPSHOT_INTERVAL Snapshot interval in seconds");
|
|
|
+ spdlog::info(" SMARTBOTIC_DB_LOG_LEVEL Log level");
|
|
|
+ spdlog::info(" SMARTBOTIC_DB_ENCRYPTION_KEY_PATH Path to encryption key file");
|
|
|
+ spdlog::info(" SMARTBOTIC_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_DB_ENCRYPTION_KEY");
|
|
|
+ if (key_env != nullptr) {
|
|
|
+ spdlog::info(" Encryption Key: (from environment variable)");
|
|
|
+ } else {
|
|
|
+ spdlog::info(" Encryption Key: (not configured)");
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace smartbotic::database
|