浏览代码

feat: US-011 - Database Configuration and Startup

Implement database configuration system with:
- YAML config file support (smartbotic-db.yaml)
- Config options: port, address, data directory, snapshot interval,
  log level, and encryption key path
- Command-line argument overrides (--port, --address, --data-dir, etc.)
- Environment variable support for secrets (SMARTBOTIC_DB_ENCRYPTION_KEY)
- Graceful shutdown on SIGTERM/SIGINT with proper signal handling
- Systemd service file for local testing (deploy/smartbotic-db.service)

Configuration priority (highest to lowest):
1. Command-line arguments
2. Environment variables
3. Config file (smartbotic-db.yaml)
4. Built-in defaults
Fszontagh 6 月之前
父节点
当前提交
85020d57a5

+ 13 - 0
CMakeLists.txt

@@ -55,6 +55,13 @@ FetchContent_Declare(
     GIT_TAG v3.11.3
 )
 
+# yaml-cpp - YAML parser library
+FetchContent_Declare(
+    yaml-cpp
+    GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git
+    GIT_TAG 0.8.0
+)
+
 # gRPC and Protobuf
 FetchContent_Declare(
     grpc
@@ -77,6 +84,12 @@ FetchContent_MakeAvailable(spdlog)
 message(STATUS "Fetching nlohmann_json...")
 FetchContent_MakeAvailable(nlohmann_json)
 
+message(STATUS "Fetching yaml-cpp...")
+set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
+set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
+set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE)
+FetchContent_MakeAvailable(yaml-cpp)
+
 message(STATUS "Fetching gRPC (this may take a while)...")
 set(gRPC_BUILD_TESTS OFF CACHE BOOL "" FORCE)
 set(gRPC_INSTALL OFF CACHE BOOL "" FORCE)

+ 43 - 0
config/smartbotic-db.yaml

@@ -0,0 +1,43 @@
+# SmartBotic Database Configuration
+# This is the default configuration file for smartbotic-db
+#
+# Configuration priority (highest to lowest):
+# 1. Command-line arguments (e.g., --port 50052)
+# 2. Environment variables (e.g., SMARTBOTIC_DB_PORT=50052)
+# 3. This config file
+# 4. Built-in defaults
+
+# Server configuration
+server:
+  # Address to bind to (default: 0.0.0.0)
+  address: "0.0.0.0"
+  # Port to listen on (default: 50051)
+  port: 50051
+
+# Storage configuration
+storage:
+  # Data directory for snapshots and metadata
+  # Can be overridden with SMARTBOTIC_DB_DATA_DIR environment variable
+  data_dir: "./data"
+
+# Snapshot configuration
+snapshot:
+  # Enable automatic snapshots (default: true)
+  enabled: true
+  # Interval between automatic snapshots in seconds (default: 300 = 5 minutes)
+  interval_seconds: 300
+  # Number of document changes to trigger a snapshot (default: 100)
+  change_threshold: 100
+
+# Logging configuration
+logging:
+  # Log level: trace, debug, info, warn, error, critical, off
+  # Can be overridden with SMARTBOTIC_DB_LOG_LEVEL environment variable
+  level: "info"
+
+# Security configuration
+security:
+  # Path to encryption key file (optional)
+  # The encryption key can also be provided via SMARTBOTIC_DB_ENCRYPTION_KEY environment variable
+  # which is recommended for production environments to avoid storing secrets in files
+  # encryption_key_path: "/etc/smartbotic/encryption.key"

+ 2 - 0
database/CMakeLists.txt

@@ -7,6 +7,7 @@ add_library(smartbotic_database STATIC
     src/collection_meta.cpp
     src/snapshot.cpp
     src/database.cpp
+    src/config.cpp
 )
 
 target_include_directories(smartbotic_database
@@ -22,6 +23,7 @@ target_link_libraries(smartbotic_database
         smartbotic::common
         spdlog::spdlog
         nlohmann_json::nlohmann_json
+        yaml-cpp::yaml-cpp
 )
 
 add_library(smartbotic::database ALIAS smartbotic_database)

+ 105 - 0
database/include/smartbotic/database/config.hpp

@@ -0,0 +1,105 @@
+#pragma once
+
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace smartbotic::database {
+
+/// Log level configuration
+enum class LogLevel : uint8_t {
+    kTrace,
+    kDebug,
+    kInfo,
+    kWarn,
+    kError,
+    kCritical,
+    kOff
+};
+
+/// Convert log level to string
+[[nodiscard]] auto LogLevelToString(LogLevel level) -> std::string;
+
+/// Parse log level from string (case-insensitive)
+[[nodiscard]] auto ParseLogLevel(const std::string& str) -> std::optional<LogLevel>;
+
+/// Complete database configuration
+/// Supports loading from:
+/// 1. Config file (YAML) - smartbotic-db.yaml
+/// 2. Command-line arguments (override config file)
+/// 3. Environment variables (for secrets like encryption key)
+struct DatabaseConfig {
+    // Server configuration
+    std::string address = "0.0.0.0";  // Listen address
+    uint16_t port = 50051;            // Listen port
+
+    // Data storage configuration
+    std::string data_dir = "./data";  // Data directory for snapshots and metadata
+
+    // Snapshot configuration
+    int64_t snapshot_interval_seconds = 300;  // Default 5 minutes
+    uint64_t snapshot_change_threshold = 100; // Snapshot after N document changes
+    bool snapshot_enabled = true;             // Enable/disable automatic snapshots
+
+    // Logging configuration
+    LogLevel log_level = LogLevel::kInfo;  // Default log level
+
+    // Security configuration
+    std::string encryption_key_path;  // Path to encryption key file (optional)
+    // Note: Encryption key can also be provided via SMARTBOTIC_DB_ENCRYPTION_KEY env var
+
+    /// Get the full address string (address:port)
+    [[nodiscard]] auto GetListenAddress() const -> std::string {
+        return address + ":" + std::to_string(port);
+    }
+
+    /// Get snapshot directory path
+    [[nodiscard]] auto GetSnapshotDir() const -> std::string {
+        return data_dir + "/snapshots";
+    }
+
+    /// Create default configuration
+    [[nodiscard]] static auto Default() -> DatabaseConfig { return DatabaseConfig{}; }
+};
+
+/// Configuration loader that handles:
+/// - YAML config file parsing
+/// - Command-line argument parsing
+/// - Environment variable substitution for secrets
+class ConfigLoader {
+public:
+    /// Load configuration from all sources
+    /// Priority (highest to lowest):
+    /// 1. Command-line arguments
+    /// 2. Environment variables (for secrets)
+    /// 3. Config file
+    /// 4. Built-in defaults
+    [[nodiscard]] static auto Load(int argc, char* argv[]) -> DatabaseConfig;
+
+    /// Load configuration from a specific config file
+    [[nodiscard]] static auto LoadFromFile(const std::string& path) -> std::optional<DatabaseConfig>;
+
+    /// Parse command-line arguments into a config
+    /// Returns nullopt if help was requested
+    [[nodiscard]] static auto ParseCommandLine(int argc, char* argv[], DatabaseConfig& config)
+        -> std::optional<bool>;
+
+    /// Apply environment variables to config (for secrets)
+    static void ApplyEnvironment(DatabaseConfig& config);
+
+    /// Get the default config file paths to search
+    [[nodiscard]] static auto GetDefaultConfigPaths() -> std::vector<std::string>;
+
+    /// Print help message
+    static void PrintHelp(const char* program_name);
+
+    /// Print current configuration
+    static void PrintConfig(const DatabaseConfig& config);
+
+private:
+    /// Find the first existing config file from the default paths
+    [[nodiscard]] static auto FindConfigFile() -> std::optional<std::string>;
+};
+
+}  // namespace smartbotic::database

+ 456 - 0
database/src/config.cpp

@@ -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

+ 71 - 54
database/src/main.cpp

@@ -1,10 +1,12 @@
 #include <spdlog/spdlog.h>
 
+#include <atomic>
 #include <csignal>
 #include <cstdlib>
 #include <string>
 
 #include "smartbotic/common.hpp"
+#include "smartbotic/database/config.hpp"
 #include "smartbotic/database/grpc/server.hpp"
 
 namespace {
@@ -12,80 +14,95 @@ namespace {
 // Global server pointer for signal handling
 smartbotic::database::grpc::GrpcServer* g_server = nullptr;
 
+// Atomic flag for shutdown tracking
+std::atomic<bool> g_shutdown_requested{false};
+
 void SignalHandler(int signal) {
-    spdlog::info("Received signal {}, initiating shutdown...", signal);
+    if (g_shutdown_requested.exchange(true)) {
+        // Second signal - force exit
+        spdlog::warn("Received second signal {}, forcing immediate exit", signal);
+        std::_Exit(1);
+    }
+
+    const char* signal_name = (signal == SIGINT) ? "SIGINT" : "SIGTERM";
+    spdlog::info("Received {} ({}), initiating graceful shutdown...", signal_name, signal);
+
     if (g_server != nullptr) {
         g_server->Stop();
     }
 }
 
-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 SetLogLevel(smartbotic::database::LogLevel level) {
+    switch (level) {
+        case smartbotic::database::LogLevel::kTrace:
+            spdlog::set_level(spdlog::level::trace);
+            break;
+        case smartbotic::database::LogLevel::kDebug:
+            spdlog::set_level(spdlog::level::debug);
+            break;
+        case smartbotic::database::LogLevel::kInfo:
+            spdlog::set_level(spdlog::level::info);
+            break;
+        case smartbotic::database::LogLevel::kWarn:
+            spdlog::set_level(spdlog::level::warn);
+            break;
+        case smartbotic::database::LogLevel::kError:
+            spdlog::set_level(spdlog::level::err);
+            break;
+        case smartbotic::database::LogLevel::kCritical:
+            spdlog::set_level(spdlog::level::critical);
+            break;
+        case smartbotic::database::LogLevel::kOff:
+            spdlog::set_level(spdlog::level::off);
+            break;
     }
 }
 
 }  // namespace
 
 int main(int argc, char* argv[]) {
+    // Initialize common logging first (with default settings)
     smartbotic::common::Initialize("smartbotic-db");
 
     spdlog::info("SmartBotic Database Service starting...");
 
-    // Parse configuration from environment variables or command line
-    smartbotic::database::ServerConfig config;
-    config.address = GetEnvOrDefault("SMARTBOTIC_DB_ADDRESS", "0.0.0.0");
-    config.port = GetEnvOrDefault("SMARTBOTIC_DB_PORT", static_cast<uint16_t>(50051));
-    config.data_dir = GetEnvOrDefault("SMARTBOTIC_DB_DATA_DIR", "./data");
-
-    // Simple command-line argument parsing
-    for (int i = 1; i < argc; ++i) {
-        std::string arg = argv[i];
-        if (arg == "--port" && i + 1 < argc) {
-            config.port = static_cast<uint16_t>(std::stoi(argv[++i]));
-        } else if (arg == "--address" && i + 1 < argc) {
-            config.address = argv[++i];
-        } else if (arg == "--data-dir" && i + 1 < argc) {
-            config.data_dir = argv[++i];
-        } else if (arg == "--help" || arg == "-h") {
-            spdlog::info("Usage: {} [options]", argv[0]);
-            spdlog::info("Options:");
-            spdlog::info("  --port <port>       Port to listen on (default: 50051)");
-            spdlog::info("  --address <addr>    Address to bind to (default: 0.0.0.0)");
-            spdlog::info("  --data-dir <path>   Data directory (default: ./data)");
-            spdlog::info("  --help, -h          Show this help message");
-            spdlog::info("");
-            spdlog::info("Environment variables:");
-            spdlog::info("  SMARTBOTIC_DB_PORT      Port to listen on");
-            spdlog::info("  SMARTBOTIC_DB_ADDRESS   Address to bind to");
-            spdlog::info("  SMARTBOTIC_DB_DATA_DIR  Data directory");
-            return 0;
-        }
-    }
+    // Load configuration from file, environment, and command line
+    auto config = smartbotic::database::ConfigLoader::Load(argc, argv);
+
+    // Apply configured log level
+    SetLogLevel(config.log_level);
 
-    spdlog::info("Configuration:");
-    spdlog::info("  Address:  {}", config.address);
-    spdlog::info("  Port:     {}", config.port);
-    spdlog::info("  Data Dir: {}", config.data_dir);
+    // Print configuration
+    smartbotic::database::ConfigLoader::PrintConfig(config);
+
+    // Convert DatabaseConfig to ServerConfig for the gRPC server
+    smartbotic::database::ServerConfig server_config;
+    server_config.address = config.address;
+    server_config.port = config.port;
+    server_config.data_dir = config.data_dir;
 
     // Create and start the server
-    smartbotic::database::grpc::GrpcServer server(config);
+    smartbotic::database::grpc::GrpcServer server(server_config);
     g_server = &server;
 
-    // Set up signal handlers for graceful shutdown
-    std::signal(SIGINT, SignalHandler);
-    std::signal(SIGTERM, SignalHandler);
+    // Set up signal handlers for graceful shutdown (SIGTERM/SIGINT)
+    struct sigaction sa{};
+    sa.sa_handler = SignalHandler;
+    sigemptyset(&sa.sa_mask);
+    sa.sa_flags = 0;
+
+    if (sigaction(SIGINT, &sa, nullptr) == -1) {
+        spdlog::error("Failed to set SIGINT handler");
+        smartbotic::common::Shutdown();
+        return 1;
+    }
+    if (sigaction(SIGTERM, &sa, nullptr) == -1) {
+        spdlog::error("Failed to set SIGTERM handler");
+        smartbotic::common::Shutdown();
+        return 1;
+    }
+
+    spdlog::info("Signal handlers registered for SIGINT and SIGTERM");
 
     if (!server.Start()) {
         spdlog::error("Failed to start server");
@@ -96,7 +113,7 @@ int main(int argc, char* argv[]) {
     // Wait for server to stop (either by signal or error)
     server.Wait();
 
-    spdlog::info("SmartBotic Database Service shutting down");
+    spdlog::info("SmartBotic Database Service shutdown complete");
     g_server = nullptr;
     smartbotic::common::Shutdown();
 

+ 57 - 0
deploy/smartbotic-db.service

@@ -0,0 +1,57 @@
+[Unit]
+Description=SmartBotic Database Service
+Documentation=https://github.com/smartbotic/smartbotic-crm
+After=network.target
+Wants=network-online.target
+
+[Service]
+Type=simple
+User=smartbotic
+Group=smartbotic
+WorkingDirectory=/opt/smartbotic
+
+# Main executable
+ExecStart=/opt/smartbotic/bin/smartbotic-db --config /etc/smartbotic/smartbotic-db.yaml
+
+# Graceful shutdown handling
+ExecStop=/bin/kill -SIGTERM $MAINPID
+TimeoutStopSec=30
+KillMode=mixed
+KillSignal=SIGTERM
+
+# Restart policy
+Restart=on-failure
+RestartSec=5
+StartLimitIntervalSec=60
+StartLimitBurst=3
+
+# Security hardening
+NoNewPrivileges=yes
+ProtectSystem=strict
+ProtectHome=yes
+PrivateTmp=yes
+PrivateDevices=yes
+ProtectKernelTunables=yes
+ProtectKernelModules=yes
+ProtectControlGroups=yes
+RestrictRealtime=yes
+RestrictSUIDSGID=yes
+LockPersonality=yes
+
+# Allow write access to data directory
+ReadWritePaths=/var/lib/smartbotic
+
+# Environment variables for secrets (can be overridden in drop-in files)
+# Create /etc/systemd/system/smartbotic-db.service.d/encryption.conf with:
+# [Service]
+# Environment="SMARTBOTIC_DB_ENCRYPTION_KEY=your-base64-key"
+Environment="SMARTBOTIC_DB_DATA_DIR=/var/lib/smartbotic"
+Environment="SMARTBOTIC_DB_LOG_LEVEL=info"
+
+# Logging
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=smartbotic-db
+
+[Install]
+WantedBy=multi-user.target