diff --git a/docker/migrations/009_add_offline_settings.sql b/docker/migrations/009_add_offline_settings.sql new file mode 100644 index 0000000..56fdb1e --- /dev/null +++ b/docker/migrations/009_add_offline_settings.sql @@ -0,0 +1,40 @@ +-- 009_add_offline_settings.sql +-- Добавляет настройки для уведомлений при offline и дефолтные параметры + +-- Поля для offline уведомлений в таблице servers +ALTER TABLE servers ADD COLUMN offline_timeout INT DEFAULT 300 COMMENT 'Таймаут в секундах до считания сервера offline (0 = выключено)'; +ALTER TABLE servers ADD COLUMN notify_on_offline TINYINT(1) DEFAULT 1 COMMENT 'Уведомлять при offline'; +ALTER TABLE servers ADD COLUMN last_offline_alert_at TIMESTAMP NULL COMMENT 'Время последнего алерта offline (анти-спам)'; + +-- Таблица дефолтных параметров +CREATE TABLE IF NOT EXISTS default_settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + setting_key VARCHAR(100) UNIQUE NOT NULL, + setting_value TEXT, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Вставляем дефолтные значения +INSERT INTO default_settings (setting_key, setting_value, description) VALUES +('offline_check_interval', '60', 'Интервал проверки offline в секундах'), +('default_offline_timeout', '300', 'Дефолтный таймаут offline в секундах'), +('default_warning_threshold', '70', 'Дефолтный warning threshold (%)'), +('default_critical_threshold', '90', 'Дефолтный critical threshold (%)'), +('default_duration', '0', 'Дефолтная длительность превышения порога в минутах') +ON DUPLICATE KEY UPDATE description = VALUES(description); + +-- Таблица для алертов offline (отдельная от метрик) +CREATE TABLE IF NOT EXISTS offline_alerts ( + id INT AUTO_INCREMENT PRIMARY KEY, + server_id INT NOT NULL, + triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + resolved TINYINT(1) DEFAULT 0, + resolved_at TIMESTAMP NULL, + notified_at TIMESTAMP NULL, + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Индекс для быстрого поиска активных offline алертов +CREATE INDEX idx_offline_alerts_active ON offline_alerts (server_id, resolved); diff --git a/public/check_offline.php b/public/check_offline.php new file mode 100755 index 0000000..0d33f2d --- /dev/null +++ b/public/check_offline.php @@ -0,0 +1,73 @@ +query(" + SELECT + s.id, + s.name, + s.offline_timeout, + s.notify_on_offline, + s.last_offline_alert_at, + s.last_metrics_at, + TIMESTAMPDIFF(SECOND, s.last_metrics_at, NOW()) as seconds_since_update, + (SELECT COUNT(*) FROM offline_alerts oa WHERE oa.server_id = s.id AND oa.resolved = 0) as active_offline_alerts + FROM servers s + WHERE s.offline_timeout > 0 +"); + +$servers = $stmt->fetchAll(); + +foreach ($servers as $server) { + $timeout = (int)$server['offline_timeout']; + $secondsSinceUpdate = (int)$server['seconds_since_update']; + $isOffline = $secondsSinceUpdate > $timeout; + $activeAlerts = (int)$server['active_offline_alerts']; + + if ($isOffline) { + if ($activeAlerts === 0) { + $stmtInsert = $pdo->prepare(" + INSERT INTO offline_alerts (server_id, triggered_at, resolved, notified_at) + VALUES (:server_id, NOW(), 0, NOW()) + "); + $stmtInsert->execute([':server_id' => $server['id']]); + + if ($server['notify_on_offline']) { + $notificationService->sendOfflineNotification($server['name'], $secondsSinceUpdate); + echo "[OFFLINE] Server '{$server['name']}' is offline (no metrics for {$secondsSinceUpdate}s)\n"; + } + + $stmtUpdate = $pdo->prepare(" + UPDATE servers SET last_offline_alert_at = NOW() WHERE id = :id + "); + $stmtUpdate->execute([':id' => $server['id']]); + } + } else { + if ($activeAlerts > 0) { + $stmtResolve = $pdo->prepare(" + UPDATE offline_alerts + SET resolved = 1, resolved_at = NOW() + WHERE server_id = :server_id AND resolved = 0 + "); + $stmtResolve->execute([':server_id' => $server['id']]); + + $notificationService->sendOnlineNotification($server['name']); + echo "[ONLINE] Server '{$server['name']}' is back online\n"; + } + } +} + +echo "[" . date('Y-m-d H:i:s') . "] Offline check completed. Processed {$stmt->rowCount()} servers.\n"; diff --git a/public/index.php b/public/index.php index 825c71a..ef57800 100755 --- a/public/index.php +++ b/public/index.php @@ -210,6 +210,8 @@ $adminGroup = $app->group('/admin', function ($group) use ($adminController) { $group->get('/notifications', [$adminController, 'notificationSettings']); $group->post("/notifications/save", [$adminController, "saveNotificationSettings"]); $group->get("/notifications/test", [$adminController, "testNotification"]); + $group->get('/defaults', [$adminController, 'defaultSettings']); + $group->post("/defaults/save", [$adminController, "saveDefaultSettings"]); })->add($csrfMiddleware)->add(AuthMiddleware::class); // API route for agents (public, no auth middleware, no csrf) diff --git a/src/Controllers/AdminController.php b/src/Controllers/AdminController.php index c6380d2..7ed0c34 100755 --- a/src/Controllers/AdminController.php +++ b/src/Controllers/AdminController.php @@ -5,6 +5,7 @@ namespace App\Controllers; use App\Models\Model; use App\Services\NotificationService; +use PDO; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Slim\Views\Twig; @@ -267,4 +268,60 @@ class AdminController extends Model return $response->withHeader('Location', '/admin/notifications')->withStatus(302); } + + // ==================== ДЕФОЛТНЫЕ ПАРАМЕТРЫ ==================== + + public function defaultSettings(Request $request, Response $response, $args) + { + if ($_SESSION['role'] !== 'admin') { + return $response->withHeader('Location', '/')->withStatus(302); + } + + $stmt = $this->pdo->query("SELECT * FROM default_settings ORDER BY id"); + $settings = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); + + return $this->twig->render($response, 'admin/defaults.twig', [ + 'title' => 'Дефолтные параметры', + 'settings' => $settings + ]); + } + + public function saveDefaultSettings(Request $request, Response $response, $args) + { + if ($_SESSION['role'] !== 'admin') { + return $response->withHeader('Location', '/')->withStatus(302); + } + + $data = $request->getParsedBody(); + + $settingsToUpdate = [ + 'offline_check_interval' => (int)($data['offline_check_interval'] ?? 60), + 'default_offline_timeout' => (int)($data['default_offline_timeout'] ?? 300), + 'default_warning_threshold' => (float)($data['default_warning_threshold'] ?? 70), + 'default_critical_threshold' => (float)($data['default_critical_threshold'] ?? 90), + 'default_duration' => (int)($data['default_duration'] ?? 0), + ]; + + foreach ($settingsToUpdate as $key => $value) { + $stmt = $this->pdo->prepare(" + INSERT INTO default_settings (setting_key, setting_value) + VALUES (:key, :value) + ON DUPLICATE KEY UPDATE setting_value = :value2 + "); + $stmt->execute([':key' => $key, ':value' => $value, ':value2' => $value]); + } + + $_SESSION['flash_message'] = 'Дефолтные параметры сохранены'; + $_SESSION['flash_type'] = 'success'; + + return $response->withHeader('Location', '/admin/defaults')->withStatus(302); + } + + public function getDefaultSetting($key, $default = null) + { + $stmt = $this->pdo->prepare("SELECT setting_value FROM default_settings WHERE setting_key = :key"); + $stmt->execute([':key' => $key]); + $result = $stmt->fetch(); + return $result ? $result['setting_value'] : $default; + } } diff --git a/src/Controllers/ServerController.php b/src/Controllers/ServerController.php index dfcb36f..635571d 100755 --- a/src/Controllers/ServerController.php +++ b/src/Controllers/ServerController.php @@ -57,23 +57,33 @@ class ServerController extends Model { $params = $request->getParsedBody(); + // Получаем дефолтные значения + $stmtDefault = $this->pdo->query("SELECT setting_key, setting_value FROM default_settings"); + $defaults = []; + while ($row = $stmtDefault->fetch()) { + $defaults[$row['setting_key']] = $row['setting_value']; + } + + $defaultOfflineTimeout = (int)($defaults['default_offline_timeout'] ?? 300); + // Генерируем уникальный токен - $token = bin2hex(random_bytes(16)); // 32-символьный токен + $token = bin2hex(random_bytes(16)); $this->pdo->beginTransaction(); try { - // Сохраняем сервер + // Сохраняем сервер с дефолтными значениями $stmt = $this->pdo->prepare(" - INSERT INTO servers (name, address, group_id, description) - VALUES (:name, :address, :group_id, :description) + INSERT INTO servers (name, address, group_id, description, offline_timeout, notify_on_offline) + VALUES (:name, :address, :group_id, :description, :offline_timeout, 1) "); $result = $stmt->execute([ ':name' => $params['name'], ':address' => $params['address'] ?? '', ':group_id' => $params['group_id'] ?? null, - ':description' => $params['description'] ?? '' + ':description' => $params['description'] ?? '', + ':offline_timeout' => $defaultOfflineTimeout ]); $serverId = $this->pdo->lastInsertId(); @@ -110,7 +120,6 @@ class ServerController extends Model } catch (\Exception $e) { $this->pdo->rollback(); - // TODO: Обработка ошибки return $response->withHeader('Location', '/servers/create')->withStatus(302); } } @@ -153,7 +162,12 @@ class ServerController extends Model $stmt = $this->pdo->prepare(" UPDATE servers - SET name = :name, address = :address, group_id = :group_id, description = :description + SET name = :name, + address = :address, + group_id = :group_id, + description = :description, + offline_timeout = :offline_timeout, + notify_on_offline = :notify_on_offline WHERE id = :id "); @@ -162,13 +176,14 @@ class ServerController extends Model ':name' => $params['name'], ':address' => $params['address'] ?? '', ':group_id' => $params['group_id'] ?? null, - ':description' => $params['description'] ?? '' + ':description' => $params['description'] ?? '', + ':offline_timeout' => (int)($params['offline_timeout'] ?? 300), + ':notify_on_offline' => isset($params['notify_on_offline']) ? 1 : 0 ]); if ($result) { return $response->withHeader('Location', '/servers')->withStatus(302); } else { - // TODO: Обработка ошибки return $response->withHeader('Location', '/servers/' . $id . '/edit')->withStatus(302); } } diff --git a/src/Services/NotificationService.php b/src/Services/NotificationService.php index f8a7a3e..10f3c9f 100755 --- a/src/Services/NotificationService.php +++ b/src/Services/NotificationService.php @@ -62,6 +62,36 @@ class NotificationService $this->sendNotification($subject, $message); } + /** + * Отправить уведомление о недоступности сервера (offline) + */ + public function sendOfflineNotification($serverName, $secondsSinceUpdate) + { + $minutes = round($secondsSinceUpdate / 60); + $subject = "🛑 Сервер недоступен: {$serverName}"; + $message = "🖥 Сервер: {$serverName}\n"; + $message .= "📊 Статус: OFFLINE\n"; + $message .= "⏱️ Последние метрики: {$minutes} мин. назад\n"; + $message .= "🕒 Время: " . date('d.m.Y H:i:s') . "\n"; + $message .= "🔔 Требуется проверка!"; + + $this->sendNotification($subject, $message); + } + + /** + * Отправить уведомление о восстановлении сервера (online) + */ + public function sendOnlineNotification($serverName) + { + $subject = "✅ Сервер восстановлен: {$serverName}"; + $message = "🖥 Сервер: {$serverName}\n"; + $message .= "📊 Статус: ONLINE\n"; + $message .= "🕒 Время: " . date('d.m.Y H:i:s') . "\n"; + $message .= "🟢 Метрики снова поступают"; + + $this->sendNotification($subject, $message); + } + /** * Отправить уведомление о сервисе (остановка/запуск) */ diff --git a/templates/admin/defaults.twig b/templates/admin/defaults.twig new file mode 100644 index 0000000..19ed3b5 --- /dev/null +++ b/templates/admin/defaults.twig @@ -0,0 +1,137 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+

Дефолтные параметры

+

Эти параметры будут использоваться по умолчанию для новых серверов и метрик

+
+
+ +{% if session.flash_message is defined and session.flash_message %} +
+ {{ session.flash_message|nl2br }} + +
+{% endif %} + +
+ + + + +
+
+
+
+
Мониторинг недоступности (Offline)
+
+
+
+ +
+ + секунд +
+
CRON будет проверять серверы каждые N секунд (минимум 30)
+
+ +
+ +
+ + секунд +
+
Сервер считается offline если метрики не приходили N секунд (по умолчанию 300 = 5 мин)
+
+
+
+
+ +
+
+
+
Дефолтные пороги метрик
+
+
+
+ +
+ + % +
+
При превышении этого значения будет Warning
+
+ +
+ +
+ + % +
+
При превышении этого значения будет Critical
+
+ +
+ +
+ + минут +
+
Алерт отправится только если порог превышен N минут (0 = сразу)
+
+
+
+
+
+ + +
+
+
+
+
Настройка CRON
+
+
+

Для автоматической проверки offline серверов добавьте в CRON:

+
+ * * * * * php {{ app_path|default('/var/www/mon/public') }}/check_offline.php +
+

+ + Это запустит проверку каждую минуту. Интервал в настройках определяет как часто проверяется каждый сервер. +

+
+
+
+
+ + +
+
+ + + Назад в админку + +
+
+
+ +{% endblock %} diff --git a/templates/layout.twig b/templates/layout.twig index 6b8ea31..0e6a2d8 100755 --- a/templates/layout.twig +++ b/templates/layout.twig @@ -52,6 +52,8 @@ {% endif %} diff --git a/templates/servers/edit.twig b/templates/servers/edit.twig index ac9f5f8..18a1fef 100755 --- a/templates/servers/edit.twig +++ b/templates/servers/edit.twig @@ -38,7 +38,43 @@ Дополнительная информация о сервере - + +
+ +
Настройки мониторинга недоступности
+ +
+
+
+ +
+ + секунд +
+ Сервер будет считаться offline если метрики не приходили N секунд (0 = выключено) +
+
+
+
+ +
+ + +
+
+
+
+ + {% if server.last_offline_alert_at %} +
+ Последний offline алерт: {{ server.last_offline_alert_at|date('d.m.Y H:i:s') }} +
+ {% endif %} +
Назад