Docker: production-ready setup with immutable images, versioned migrations, env vars
- Dockerfile: PHP 8.3 FPM + composer install (no dev) - docker-compose.yml: app + nginx + MariaDB 10.11 - Versioned migrations (001-007) with schema_migrations tracking - DatabaseConfig.php: env vars with fallbacks - init.sh: wait-for-db + auto-migrate - nginx.conf: reverse proxy + gzip + security rules - .env.example: config template - .dockerignore: build optimization Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
99761ca5d8
commit
6d8bd99277
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
// config/DatabaseConfig.php
|
||||
|
||||
namespace Config;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
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 $charset = 'utf8mb4';
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
$dsn = "mysql:host={$this->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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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 |
|
||||
|
||||
Код приложения **не** монтируется — он внутри образа. Обновление = новый образ.
|
||||
|
|
@ -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
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- 002: Добавляем encrypted_token в agent_tokens
|
||||
|
||||
ALTER TABLE agent_tokens ADD COLUMN IF NOT EXISTS encrypted_token TEXT AFTER token_hash;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- 006: Меняем тип value в server_metrics на TEXT (для JSON процессов и т.д.)
|
||||
|
||||
ALTER TABLE server_metrics MODIFY COLUMN value TEXT;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
$this->notificationService->sendThresholdNotification(
|
||||
$serverName,
|
||||
$metricName,
|
||||
$value,
|
||||
$severity,
|
||||
$threshold
|
||||
);
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue