diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1da891d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +# Docker build ignore +.git +.gitignore +node_modules +vendor +*.md +*.log +*.sql +!composer.lock +tests +.env +monitoring_system_dump.sql +*fixes* +*.svg +*.pyc +__pycache__ + +# Backups +*.bak.* +*.broken diff --git a/config/DatabaseConfig.php b/config/DatabaseConfig.php index ea73a28..691e486 100755 --- a/config/DatabaseConfig.php +++ b/config/DatabaseConfig.php @@ -11,14 +11,20 @@ class DatabaseConfig private static $instance = null; private $connection; - private $host = 'localhost'; - private $db_name = 'monitoring_system'; - private $username = "mon_user"; - private $password = 'mon_password_123'; + private $host; + private $db_name; + private $username; + private $password; private $charset = 'utf8mb4'; private function __construct() { + // Читаем из переменных окружения с фоллбэками для совместимости + $this->host = getenv('DB_HOST') ?: 'localhost'; + $this->db_name = getenv('DB_NAME') ?: 'monitoring_system'; + $this->username = getenv('DB_USERNAME') ?: 'mon_user'; + $this->password = getenv('DB_PASSWORD') ?: 'mon_password_123'; + $dsn = "mysql:host={$this->host};dbname={$this->db_name};charset={$this->charset}"; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, @@ -41,4 +47,4 @@ class DatabaseConfig return self::$instance->connection; } -} \ No newline at end of file +} diff --git a/config/DatabaseConfig.php.bak.20260414 b/config/DatabaseConfig.php.bak.20260414 new file mode 100755 index 0000000..ea73a28 --- /dev/null +++ b/config/DatabaseConfig.php.bak.20260414 @@ -0,0 +1,44 @@ +host};dbname={$this->db_name};charset={$this->charset}"; + $options = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + try { + $this->connection = new PDO($dsn, $this->username, $this->password, $options); + } catch (PDOException $e) { + throw new PDOException($e->getMessage(), (int)$e->getCode()); + } + } + + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new self(); + } + + return self::$instance->connection; + } +} \ No newline at end of file diff --git a/docker/.env b/docker/.env new file mode 100644 index 0000000..ac4d66a --- /dev/null +++ b/docker/.env @@ -0,0 +1,41 @@ +# ========================================== +# MirvMon — Environment Configuration +# ========================================== +# Скопируйте этот файл в .env и заполните значения + +# ------------------------------------------ +# Приложение +# ------------------------------------------ +APP_PORT=8082 +APP_TIMEZONE=Asia/Irkutsk + +# ------------------------------------------ +# База данных +# ------------------------------------------ +DB_NAME=monitoring_system +DB_USERNAME=mon_user +DB_PASSWORD=mon_password_123 +DB_ROOT_PASSWORD=mirvmon_db_root_2026 + +# ------------------------------------------ +# Пользователь веб-интерфейса (первый запуск) +# ------------------------------------------ +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin_change_me + +# ------------------------------------------ +# Уведомления — Email (опционально) +# ------------------------------------------ +# SMTP_HOST=smtp.gmail.com +# SMTP_PORT=587 +# SMTP_USERNAME=your@email.com +# SMTP_PASSWORD=your_app_password +# SMTP_ENCRYPTION=tls +# SMTP_FROM_EMAIL=your@email.com + +# ------------------------------------------ +# Уведомления — Telegram (опционально) +# ------------------------------------------ +# TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 +# TELEGRAM_CHAT_ID=-1001234567890 +# TELEGRAM_PROXY=http://127.0.0.1:1081 diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..4691b2f --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,41 @@ +# ========================================== +# MirvMon — Environment Configuration +# ========================================== +# Скопируйте этот файл в .env и заполните значения + +# ------------------------------------------ +# Приложение +# ------------------------------------------ +APP_PORT=8080 +APP_TIMEZONE=Asia/Irkutsk + +# ------------------------------------------ +# База данных +# ------------------------------------------ +DB_NAME=monitoring_system +DB_USERNAME=mon_user +DB_PASSWORD=mon_password_123 +DB_ROOT_PASSWORD=root_password_change_me + +# ------------------------------------------ +# Пользователь веб-интерфейса (первый запуск) +# ------------------------------------------ +ADMIN_USERNAME=admin +ADMIN_PASSWORD=admin_change_me + +# ------------------------------------------ +# Уведомления — Email (опционально) +# ------------------------------------------ +# SMTP_HOST=smtp.gmail.com +# SMTP_PORT=587 +# SMTP_USERNAME=your@email.com +# SMTP_PASSWORD=your_app_password +# SMTP_ENCRYPTION=tls +# SMTP_FROM_EMAIL=your@email.com + +# ------------------------------------------ +# Уведомления — Telegram (опционально) +# ------------------------------------------ +# TELEGRAM_BOT_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 +# TELEGRAM_CHAT_ID=-1001234567890 +# TELEGRAM_PROXY=http://127.0.0.1:1081 diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..86e8daf --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,42 @@ +FROM php:8.3-fpm + +# Устанавливаем системные зависимости +RUN apt-get update && apt-get install -y \ + libmariadb-dev \ + zip \ + unzip \ + git \ + default-mysql-client \ + && docker-php-ext-install pdo pdo_mysql \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Устанавливаем Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +WORKDIR /var/www + +# Копируем composer.json и ставим зависимости (кэш слой) +COPY composer.json composer.lock* ./ +RUN composer install --no-dev --optimize-autoloader --no-interaction --no-progress 2>/dev/null \ + || composer install --no-dev --optimize-autoloader --no-interaction --no-progress + +# Копируем всё приложение +COPY . . + +# Создаём директории для кэша и логов +RUN mkdir -p /var/www/var/cache /var/www/var/log \ + && chown -R www-data:www-data /var/www + +# Скрипт инициализации +COPY docker/init.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Скрипт миграций +COPY docker/migrate.sh /usr/local/bin/migrate.sh +RUN chmod +x /usr/local/bin/migrate.sh + +EXPOSE 9000 + +ENTRYPOINT ["docker-entrypoint.sh"] +CMD ["php-fpm"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..568ab47 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,76 @@ +# MirvMon — Docker Setup + +## Быстрый старт + +```bash +# 1. Копируем конфиг +cp .env.example .env + +# 2. Меняем пароли в .env +nano .env + +# 3. Запускаем +docker compose up -d --build + +# 4. Открываем http://localhost:8080 +# Логин: admin, Пароль: admin (сменить сразу!) +``` + +## Обновление + +```bash +# Обновить код и пересобрать +git pull +docker compose up -d --build + +# Или если образы в registry: +docker compose pull +docker compose up -d +``` + +## Структура + +``` +docker/ +├── Dockerfile # PHP 8.3 FPM + приложение +├── docker-compose.yml # app + nginx + db +├── nginx.conf # конфиг nginx +├── init.sh # entrypoint (ждёт БД + миграции) +├── migrate.sh # скрипт миграций +├── migrations/ # SQL миграции с версионированием +│ ├── 001_create_base_schema.sql +│ ├── 002_add_encrypted_token.sql +│ ├── ... +├── .env.example # шаблон конфига +└── README.md # этот файл +``` + +## Миграции + +Каждая миграция — SQL файл с номером в имени. Система отслеживает применённые в таблице `schema_migrations`. + +Добавить новую: +```bash +echo "ALTER TABLE servers ADD COLUMN foo VARCHAR(50);" > docker/migrations/009_add_foo.sql +docker compose up -d --build +``` + +## Переменные окружения (.env) + +| Переменная | Описание | По умолчанию | +|---|---|---| +| `APP_PORT` | Порт веб-интерфейса | `8080` | +| `DB_HOST` | Хост БД | `db` | +| `DB_NAME` | Имя базы | `monitoring_system` | +| `DB_USERNAME` | Пользователь БД | `mon_user` | +| `DB_PASSWORD` | Пароль БД | `mon_password_123` | +| `DB_ROOT_PASSWORD` | Root пароль БД | (обязательно сменить!) | + +## Тома (persistent data) + +| Volume | Что хранит | +|---|---| +| `db_data` | База данных MariaDB | +| `app_var` | Кэш Twig и логи PHP | + +Код приложения **не** монтируется — он внутри образа. Обновление = новый образ. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..7f21c27 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,59 @@ +services: + app: + build: + context: .. + dockerfile: docker/Dockerfile + restart: unless-stopped + env_file: + - .env + depends_on: + db: + condition: service_healthy + networks: + - mon_net + volumes: + - app_var:/var/www/var + + nginx: + image: nginx:alpine + restart: unless-stopped + ports: + - "${APP_PORT:-80}:80" + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + depends_on: + - app + networks: + - mon_net + + db: + image: mariadb:10.11 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + MYSQL_DATABASE: ${DB_NAME} + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + TZ: ${APP_TIMEZONE:-Asia/Irkutsk} + command: > + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + --max-connections=100 + volumes: + - db_data:/var/lib/mysql + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + start_period: 10s + interval: 10s + timeout: 5s + retries: 5 + networks: + - mon_net + +volumes: + db_data: + app_var: + +networks: + mon_net: + driver: bridge diff --git a/docker/init.sh b/docker/init.sh new file mode 100644 index 0000000..95b8850 --- /dev/null +++ b/docker/init.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# docker/init.sh — Entry point script +# Ждёт БД, запускает миграции, потом стартует PHP-FPM + +set -e + +DB_HOST="${DB_HOST:-db}" +DB_PORT="${DB_PORT:-3306}" +DB_NAME="${DB_NAME:-monitoring_system}" +DB_USER="${DB_USERNAME:-mon_user}" +DB_PASS="${DB_PASSWORD:-mon_password_123}" + +# Флаг для отключения SSL (MariaDB в контейнере без SSL) +MYSQL_OPTS="--skip-ssl" + +echo "🚀 MirvMon — Starting up..." + +# ------------------------------------------ +# 1. Ожидание готовности БД +# ------------------------------------------ +echo "⏳ Waiting for MariaDB at ${DB_HOST}:${DB_PORT}..." +MAX_RETRIES=30 +RETRY=0 + +while ! mysql $MYSQL_OPTS -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASS" -e "SELECT 1;" "$DB_NAME" >/dev/null 2>&1; do + RETRY=$((RETRY + 1)) + if [ $RETRY -ge $MAX_RETRIES ]; then + echo "❌ Database not ready after $MAX_RETRIES attempts" + exit 1 + fi + echo " Retry $RETRY/$MAX_RETRIES..." + sleep 2 +done +echo "✅ Database is ready" + +# ------------------------------------------ +# 2. Запуск миграций +# ------------------------------------------ +echo "📦 Running database migrations..." + +# Экспортием переменные для migrate.sh +export DB_HOST DB_PORT DB_NAME DB_USERNAME DB_PASSWORD + +migrate.sh + +echo "" + +# ------------------------------------------ +# 3. Запуск PHP-FPM +# ------------------------------------------ +echo "🟢 Starting PHP-FPM..." +exec "$@" diff --git a/docker/migrate.sh b/docker/migrate.sh new file mode 100644 index 0000000..cc7003c --- /dev/null +++ b/docker/migrate.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# docker/migrate.sh — Run pending database migrations +# Использует таблицу migrations для отслеживания применённых миграций + +set -e + +DB_HOST="${DB_HOST:-db}" +DB_PORT="${DB_PORT:-3306}" +DB_NAME="${DB_NAME:-monitoring_system}" +DB_USER="${DB_USERNAME:-mon_user}" +DB_PASS="${DB_PASSWORD:-mon_password_123}" + +MIGRATIONS_DIR="/var/www/docker/migrations" + +MYSQL_CMD="mysql --skip-ssl -h${DB_HOST} -P${DB_PORT} -u${DB_USER} -p${DB_PASS} ${DB_NAME}" + +echo "" +echo "📋 Checking migrations..." + +# ------------------------------------------ +# 1. Создаём таблицу отслеживания миграций +# ------------------------------------------ +$MYSQL_CMD -e " +CREATE TABLE IF NOT EXISTS schema_migrations ( + id INT AUTO_INCREMENT PRIMARY KEY, + filename VARCHAR(255) NOT NULL UNIQUE, + applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +" 2>/dev/null + +# ------------------------------------------ +# 2. Применяем миграции по порядку +# ------------------------------------------ +APPLIED=0 +SKIPPED=0 + +for migration_file in $(ls "$MIGRATIONS_DIR"/*.sql 2>/dev/null | sort); do + filename=$(basename "$migration_file") + + # Проверяем была ли уже применена + EXISTS=$($MYSQL_CMD -sN -e "SELECT COUNT(*) FROM schema_migrations WHERE filename='${filename}';" 2>/dev/null) + + if [ "$EXISTS" = "1" ]; then + echo " ⏭️ $filename (already applied)" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + echo " ▶️ $filename ..." + + # Применяем миграцию + if $MYSQL_CMD < "$migration_file" 2>&1; then + # Записываем в tracking + $MYSQL_CMD -e "INSERT INTO schema_migrations (filename) VALUES ('${filename}');" + echo " ✅ $filename applied" + APPLIED=$((APPLIED + 1)) + else + echo " ❌ Failed to apply $filename" + exit 1 + fi +done + +echo "" +echo "📊 Migrations: $APPLIED applied, $SKIPPED skipped" diff --git a/docker/migrations/001_create_base_schema.sql b/docker/migrations/001_create_base_schema.sql new file mode 100644 index 0000000..34389c8 --- /dev/null +++ b/docker/migrations/001_create_base_schema.sql @@ -0,0 +1,109 @@ +-- 001: Базовая схема (пользователи, серверы, метрики, алерты) + +-- Таблица пользователей +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + email VARCHAR(100), + role ENUM('admin', 'operator', 'user') DEFAULT 'user', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Таблица настроек уведомлений пользователей +CREATE TABLE IF NOT EXISTS user_notification_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + enabled_notifications TINYINT(1) DEFAULT 1, + notify_on_warning TINYINT(1) DEFAULT 1, + notify_on_critical TINYINT(1) DEFAULT 1, + telegram_chat_id VARCHAR(50), + email_for_alerts VARCHAR(100), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- Таблица групп серверов +CREATE TABLE IF NOT EXISTS server_groups ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + description TEXT, + icon VARCHAR(50), + color VARCHAR(20) +); + +-- Таблица серверов +CREATE TABLE IF NOT EXISTS servers ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + address VARCHAR(255), + group_id INT, + description TEXT, + last_metrics_at TIMESTAMP NULL, + last_service_check_at TIMESTAMP NULL, + service_check_enabled TINYINT(1) DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (group_id) REFERENCES server_groups(id) ON DELETE SET NULL +); + +-- Таблица названий метрик +CREATE TABLE IF NOT EXISTS metric_names ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + unit VARCHAR(20), + description TEXT +); + +-- Стандартные метрики +INSERT IGNORE INTO metric_names (name, unit, description) VALUES +('cpu_load', '%', 'Загрузка процессора'), +('ram_used', '%', 'Использование оперативной памяти'), +('disk_used', '%', 'Использование диска'), +('network_in', 'MB/s', 'Скорость приема сети'), +('network_out', 'MB/s', 'Скорость передачи сети'); + +-- Таблица пороговых значений метрик +CREATE TABLE IF NOT EXISTS metric_thresholds ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT NOT NULL, + metric_name_id INT NOT NULL, + warning_threshold DECIMAL(8,2), + critical_threshold DECIMAL(8,2), + duration INT DEFAULT 0, + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE, + FOREIGN KEY (metric_name_id) REFERENCES metric_names(id) +); + +-- Таблица метрик серверов +CREATE TABLE IF NOT EXISTS server_metrics ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT NOT NULL, + metric_name_id INT NOT NULL, + value DECIMAL(8,2), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_server_metric_time (server_id, metric_name_id, created_at), + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE, + FOREIGN KEY (metric_name_id) REFERENCES metric_names(id) +); + +-- Таблица токенов агентов +CREATE TABLE IF NOT EXISTS agent_tokens ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT UNIQUE NOT NULL, + token_hash VARCHAR(64) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_used_at TIMESTAMP NULL, + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE +); + +-- Таблица алертов +CREATE TABLE IF NOT EXISTS alerts ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT NOT NULL, + metric_name VARCHAR(50) NOT NULL, + value DECIMAL(8,2), + severity ENUM('warning', 'critical') NOT NULL, + resolved BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP NULL, + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE +); diff --git a/docker/migrations/002_add_encrypted_token.sql b/docker/migrations/002_add_encrypted_token.sql new file mode 100644 index 0000000..8b49845 --- /dev/null +++ b/docker/migrations/002_add_encrypted_token.sql @@ -0,0 +1,3 @@ +-- 002: Добавляем encrypted_token в agent_tokens + +ALTER TABLE agent_tokens ADD COLUMN IF NOT EXISTS encrypted_token TEXT AFTER token_hash; diff --git a/docker/migrations/003_add_agent_configs.sql b/docker/migrations/003_add_agent_configs.sql new file mode 100644 index 0000000..19212e5 --- /dev/null +++ b/docker/migrations/003_add_agent_configs.sql @@ -0,0 +1,12 @@ +-- 003: Таблица конфигурации агентов + +CREATE TABLE IF NOT EXISTS agent_configs ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT NOT NULL UNIQUE, + interval_seconds INT DEFAULT 60, + monitor_services LONGTEXT COMMENT 'Массив сервисов для мониторинга' CHECK (json_valid(monitor_services)), + enabled TINYINT(1) DEFAULT 1 COMMENT 'Включен ли агент', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/docker/migrations/004_add_global_notification_settings.sql b/docker/migrations/004_add_global_notification_settings.sql new file mode 100644 index 0000000..8ab63ef --- /dev/null +++ b/docker/migrations/004_add_global_notification_settings.sql @@ -0,0 +1,22 @@ +-- 004: Глобальные настройки уведомлений + +CREATE TABLE IF NOT EXISTS global_notification_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + smtp_host VARCHAR(255) DEFAULT '', + smtp_port INT DEFAULT 587, + smtp_username VARCHAR(255) DEFAULT '', + smtp_password VARCHAR(255) DEFAULT '', + smtp_encryption ENUM('tls', 'ssl', 'none') DEFAULT 'tls', + smtp_from_email VARCHAR(255) DEFAULT '', + telegram_bot_token VARCHAR(255) DEFAULT '', + telegram_chat_id VARCHAR(100) DEFAULT '', + telegram_proxy VARCHAR(255) DEFAULT 'http://127.0.0.1:1081', + email_enabled TINYINT(1) DEFAULT 0, + telegram_enabled TINYINT(1) DEFAULT 0, + notify_on_warning TINYINT(1) DEFAULT 1, + notify_on_critical TINYINT(1) DEFAULT 1, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Создаём запись по умолчанию если нет +INSERT INTO global_notification_settings (id) SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM global_notification_settings WHERE id = 1); diff --git a/docker/migrations/005_add_service_tables.sql b/docker/migrations/005_add_service_tables.sql new file mode 100644 index 0000000..dbc0a14 --- /dev/null +++ b/docker/migrations/005_add_service_tables.sql @@ -0,0 +1,29 @@ +-- 005: Таблицы мониторинга сервисов + +CREATE TABLE IF NOT EXISTS service_status ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT NOT NULL, + service_name VARCHAR(255) NOT NULL, + status ENUM('running', 'stopped', 'unknown') NOT NULL, + load_state VARCHAR(50) DEFAULT NULL, + active_state VARCHAR(50) DEFAULT NULL, + sub_state VARCHAR(50) DEFAULT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_server_service (server_id, service_name), + KEY idx_server_updated (server_id, updated_at), + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS service_alerts ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT NOT NULL, + service_name VARCHAR(100) NOT NULL, + status ENUM('stopped', 'running', 'unknown') NOT NULL, + severity ENUM('warning', 'critical') DEFAULT 'warning' COMMENT 'Уровень важности', + resolved TINYINT(1) DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved_at TIMESTAMP NULL, + KEY idx_server_service (server_id, service_name), + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/docker/migrations/006_server_metrics_value_to_text.sql b/docker/migrations/006_server_metrics_value_to_text.sql new file mode 100644 index 0000000..b3d8c7f --- /dev/null +++ b/docker/migrations/006_server_metrics_value_to_text.sql @@ -0,0 +1,3 @@ +-- 006: Меняем тип value в server_metrics на TEXT (для JSON процессов и т.д.) + +ALTER TABLE server_metrics MODIFY COLUMN value TEXT; diff --git a/docker/migrations/007_seed_admin_user.sql b/docker/migrations/007_seed_admin_user.sql new file mode 100644 index 0000000..90b6cb2 --- /dev/null +++ b/docker/migrations/007_seed_admin_user.sql @@ -0,0 +1,14 @@ +-- 007: Создаём админа по умолчанию (если нет ни одного пользователя) + +-- Пароль: admin (нужно сменить при первом входе!) +-- Хеш генерируется через password_hash('admin', PASSWORD_DEFAULT) +-- Это хеш от 'admin' — смените сразу после входа! +INSERT INTO users (username, password_hash, email, role) +SELECT 'admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@localhost', 'admin' +WHERE NOT EXISTS (SELECT 1 FROM users LIMIT 1); + +-- Создаём настройки уведомлений для админа +INSERT INTO user_notification_settings (user_id, enabled_notifications, notify_on_warning, notify_on_critical) +SELECT id, 1, 1, 1 FROM users WHERE username = 'admin' +AND NOT EXISTS (SELECT 1 FROM user_notification_settings WHERE user_id = users.id) +LIMIT 1; diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..8dcaed0 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,42 @@ +server { + listen 80; + server_name _; + + root /var/www/public; + index index.php; + + # Логи + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Статика + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP-FPM + location ~ \.php$ { + fastcgi_pass app:9000; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + include fastcgi_params; + fastcgi_read_timeout 300; + fastcgi_buffer_size 128k; + fastcgi_buffers 4 256k; + } + + # Запрет доступа к скрытым файлам + location ~ /\. { + deny all; + } + + # Запрет доступа к composer.lock и другим служебным файлам + location ~* \.(sql|md|json|lock)$ { + deny all; + } + + # Gzip сжатие + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + gzip_min_length 256; +} diff --git a/src/Controllers/Api/MetricsController.php b/src/Controllers/Api/MetricsController.php index 45f9f54..9378a47 100755 --- a/src/Controllers/Api/MetricsController.php +++ b/src/Controllers/Api/MetricsController.php @@ -191,7 +191,7 @@ class MetricsController extends Model { // Получаем пороговые значения для этой метрики на этом сервере $stmt = $this->pdo->prepare(" - SELECT warning_threshold, critical_threshold + SELECT warning_threshold, critical_threshold, duration FROM metric_thresholds WHERE server_id = :server_id AND metric_name_id = :metric_name_id "); @@ -204,6 +204,7 @@ class MetricsController extends Model if ($thresholds) { $warningThreshold = $thresholds['warning_threshold']; $criticalThreshold = $thresholds['critical_threshold']; + $duration = (int)($thresholds['duration'] ?? 0); $severity = null; $threshold = null; @@ -256,25 +257,61 @@ class MetricsController extends Model ); } } else { - // Нового алерта нет — создаём и отправляем уведомление - $stmt = $this->pdo->prepare(" - INSERT INTO alerts (server_id, metric_name, value, severity) - VALUES (:server_id, :metric_name, :value, :severity) - "); - $stmt->execute([ - ':server_id' => $serverId, - ':metric_name' => $metricName, - ':value' => $value, - ':severity' => $severity - ]); + // Алерта нет — проверяем duration + // Если duration = 0 — алертим сразу + // Если duration > 0 — проверяем что все метрики за последние N минут >= порога + $shouldAlert = true; + + if ($duration > 0) { + // Считаем сколько метрик за последние duration минут были >= порога + $thresholdToCheck = ($severity === 'critical') ? $criticalThreshold : $warningThreshold; + + $stmt = $this->pdo->prepare(" + SELECT COUNT(*) as total_count, + SUM(CASE WHEN CAST(value AS DECIMAL(10,2)) >= :threshold THEN 1 ELSE 0 END) as above_count + FROM server_metrics + WHERE server_id = :server_id + AND metric_name_id = :metric_name_id + AND created_at >= DATE_SUB(NOW(), INTERVAL :duration MINUTE) + "); + $stmt->execute([ + ':server_id' => $serverId, + ':metric_name_id' => $metricId, + ':threshold' => $thresholdToCheck, + ':duration' => $duration + ]); + $durationCheck = $stmt->fetch(); + + $totalCount = (int)$durationCheck['total_count']; + $aboveCount = (int)$durationCheck['above_count']; + + // Если метрик за период нет или не все выше порога — не алертим + if ($totalCount === 0 || $aboveCount < $totalCount) { + $shouldAlert = false; + } + } + + if ($shouldAlert) { + // Создаём алерт и отправляем уведомление + $stmt = $this->pdo->prepare(" + INSERT INTO alerts (server_id, metric_name, value, severity) + VALUES (:server_id, :metric_name, :value, :severity) + "); + $stmt->execute([ + ':server_id' => $serverId, + ':metric_name' => $metricName, + ':value' => $value, + ':severity' => $severity + ]); - $this->notificationService->sendThresholdNotification( - $serverName, - $metricName, - $value, - $severity, - $threshold - ); + $this->notificationService->sendThresholdNotification( + $serverName, + $metricName, + $value, + $severity, + $threshold + ); + } } } }