feat: масштабирование, дашборд, алерты и тултипы
- Исправлена ось времени: старые данные слева, новые справа - Подключён chartjs-plugin-zoom (колёсико, drag, pan) - Переделаны кнопки периода: 1ч/6ч/24ч/7д/30д (по умолчанию 6ч) - Добавлен cmdline в процессы тултипа (показывает полный путь) - Улучшена логика алертов: нет спама, resolved уведомления - Исправлено сохранение порогов (приведение типов) - Исправлена страница алертов (Twig syntax: ends_with -> matches) - Дашборд: цвета прогресс-баров по реальным порогам сервера Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
parent
b875e57e4c
commit
0219fda95f
File diff suppressed because one or more lines are too long
|
|
@ -176,25 +176,106 @@ class MetricsController extends Model
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($severity) {
|
if ($severity) {
|
||||||
// Создаем алерт
|
// Проверяем есть ли уже неразрешённый алерт для этой метрики
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
INSERT INTO alerts (server_id, metric_name, value, severity)
|
SELECT id, severity FROM alerts
|
||||||
VALUES (:server_id, :metric_name, :value, :severity)
|
WHERE server_id = :server_id AND metric_name = :metric_name AND resolved = FALSE
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
");
|
");
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
':server_id' => $serverId,
|
':server_id' => $serverId,
|
||||||
':metric_name' => $metricName,
|
':metric_name' => $metricName
|
||||||
':value' => $value,
|
|
||||||
':severity' => $severity
|
|
||||||
]);
|
]);
|
||||||
|
$existingAlert = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($existingAlert) {
|
||||||
|
// Алерт уже есть — обновляем значение но НЕ отправляем уведомление
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE alerts SET value = :value WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':value' => $value,
|
||||||
|
':id' => $existingAlert['id']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Если серьёзность повысилась (warning -> critical) — отправляем
|
||||||
|
if ($severity === 'critical' && $existingAlert['severity'] === 'warning') {
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE alerts SET severity = :severity WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':severity' => $severity,
|
||||||
|
':id' => $existingAlert['id']
|
||||||
|
]);
|
||||||
|
$this->notificationService->sendAlertNotification(
|
||||||
|
$serverName,
|
||||||
|
$metricName,
|
||||||
|
$value,
|
||||||
|
$severity,
|
||||||
|
$threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->notificationService->sendAlertNotification(
|
||||||
|
$serverName,
|
||||||
|
$metricName,
|
||||||
|
$value,
|
||||||
|
$severity,
|
||||||
|
$threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Всегда проверяем resolved — даже если пороги не настроены или удалены
|
||||||
|
// Если есть неразрешённый алерт а значение сейчас в норме — разрешаем
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
SELECT id FROM alerts
|
||||||
|
WHERE server_id = :server_id AND metric_name = :metric_name AND resolved = FALSE
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
|
");
|
||||||
|
$stmt->execute([
|
||||||
|
':server_id' => $serverId,
|
||||||
|
':metric_name' => $metricName
|
||||||
|
]);
|
||||||
|
$existingAlert = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($existingAlert) {
|
||||||
|
// Проверяем действительно ли значение в норме
|
||||||
|
// (если пороги есть — проверяем по ним, если нет — считаем что в норме)
|
||||||
|
$isNormal = true;
|
||||||
|
if ($thresholds) {
|
||||||
|
$w = $thresholds['warning_threshold'];
|
||||||
|
$c = $thresholds['critical_threshold'];
|
||||||
|
if (($c && $value >= $c) || ($w && $value >= $w)) {
|
||||||
|
$isNormal = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isNormal) {
|
||||||
|
$stmt = $this->pdo->prepare("
|
||||||
|
UPDATE alerts SET resolved = TRUE, resolved_at = NOW() WHERE id = :id
|
||||||
|
");
|
||||||
|
$stmt->execute([':id' => $existingAlert['id']]);
|
||||||
|
|
||||||
// Отправляем уведомление
|
|
||||||
$this->notificationService->sendAlertNotification(
|
$this->notificationService->sendAlertNotification(
|
||||||
$serverName,
|
$serverName,
|
||||||
$metricName,
|
$metricName,
|
||||||
$value,
|
$value,
|
||||||
$severity,
|
'resolved',
|
||||||
$threshold
|
'Порог более не превышен'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,14 @@ class DashboardController
|
||||||
|
|
||||||
// Получаем список серверов со статусами для цветных карточек
|
// Получаем список серверов со статусами для цветных карточек
|
||||||
$servers = $this->serverModel->getServersWithStatus();
|
$servers = $this->serverModel->getServersWithStatus();
|
||||||
|
|
||||||
|
// Загружаем пороги для каждого сервера
|
||||||
|
foreach ($servers as &$server) {
|
||||||
|
$t = $this->serverModel->getThresholds($server['id']);
|
||||||
|
$server['thresholds'] = $t;
|
||||||
|
file_put_contents('/tmp/thresholds_debug.log', "Server {$server['id']}: " . json_encode($t) . "\n", FILE_APPEND);
|
||||||
|
}
|
||||||
|
unset($server);
|
||||||
|
|
||||||
$templateData = [
|
$templateData = [
|
||||||
'title' => 'Дашборд мониторинга',
|
'title' => 'Дашборд мониторинга',
|
||||||
|
|
@ -33,7 +40,6 @@ class DashboardController
|
||||||
'servers' => $servers
|
'servers' => $servers
|
||||||
];
|
];
|
||||||
|
|
||||||
file_put_contents("/tmp/dashboard_debug.log", json_encode($servers) . "\n", FILE_APPEND);
|
|
||||||
return $this->twig->render($response, 'dashboard.twig', $templateData);
|
return $this->twig->render($response, 'dashboard.twig', $templateData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,21 +37,69 @@ class ServerDetailController extends Model
|
||||||
return $response->withHeader('Location', '/servers')->withStatus(302);
|
return $response->withHeader('Location', '/servers')->withStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем даты начала и окончания
|
// Получаем параметры
|
||||||
$queryParams = $request->getQueryParams();
|
$queryParams = $request->getQueryParams();
|
||||||
$startDate = $queryParams['start'] ?? null;
|
$startDate = $queryParams['start'] ?? null;
|
||||||
$endDate = $queryParams['end'] ?? null;
|
$endDate = $queryParams['end'] ?? null;
|
||||||
|
$period = $queryParams['period'] ?? '24h';
|
||||||
|
$zoom = $queryParams['zoom'] ?? null;
|
||||||
|
|
||||||
// Если даты не указаны, используем последние 24 часа по умолчанию
|
// Если даты не указаны, вычисляем по period
|
||||||
if (!$startDate || !$endDate) {
|
if (!$startDate || !$endDate) {
|
||||||
$endDate = new DateTime();
|
$endDate = new DateTime();
|
||||||
$startDate = clone $endDate;
|
$startDate = clone $endDate;
|
||||||
$startDate->modify('-24 hours');
|
|
||||||
|
switch ($period) {
|
||||||
|
case '1h':
|
||||||
|
$startDate->modify('-1 hour');
|
||||||
|
break;
|
||||||
|
case '6h':
|
||||||
|
$startDate->modify('-6 hours');
|
||||||
|
break;
|
||||||
|
case '7d':
|
||||||
|
$startDate->modify('-7 days');
|
||||||
|
break;
|
||||||
|
case '30d':
|
||||||
|
$startDate->modify('-30 days');
|
||||||
|
break;
|
||||||
|
case '24h':
|
||||||
|
default:
|
||||||
|
$startDate->modify('-24 hours');
|
||||||
|
break;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$startDate = new DateTime($startDate);
|
$startDate = new DateTime($startDate);
|
||||||
$endDate = new DateTime($endDate);
|
$endDate = new DateTime($endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Применяем zoom — ограничиваем end по zoom-периоду
|
||||||
|
if ($zoom && $zoom !== 'auto') {
|
||||||
|
$zoomEnd = new DateTime();
|
||||||
|
$zoomStart = clone $zoomEnd;
|
||||||
|
switch ($zoom) {
|
||||||
|
case '1h':
|
||||||
|
$zoomStart->modify('-1 hour');
|
||||||
|
break;
|
||||||
|
case '6h':
|
||||||
|
$zoomStart->modify('-6 hours');
|
||||||
|
break;
|
||||||
|
case '24h':
|
||||||
|
$zoomStart->modify('-24 hours');
|
||||||
|
break;
|
||||||
|
case '7d':
|
||||||
|
$zoomStart->modify('-7 days');
|
||||||
|
break;
|
||||||
|
case '30d':
|
||||||
|
$zoomStart->modify('-30 days');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Zoom не может выйти за рамки выбранного периода
|
||||||
|
if ($zoomStart < $startDate) $zoomStart = clone $startDate;
|
||||||
|
if ($zoomEnd > $endDate) $zoomEnd = clone $endDate;
|
||||||
|
$startDate = $zoomStart;
|
||||||
|
$endDate = $zoomEnd;
|
||||||
|
}
|
||||||
|
|
||||||
// Валидация: end > start
|
// Валидация: end > start
|
||||||
if ($endDate <= $startDate) {
|
if ($endDate <= $startDate) {
|
||||||
$endDate = clone $startDate;
|
$endDate = clone $startDate;
|
||||||
|
|
@ -185,7 +233,9 @@ class ServerDetailController extends Model
|
||||||
'startDate' => $startDate->format('Y-m-d\T H:i'),
|
'startDate' => $startDate->format('Y-m-d\T H:i'),
|
||||||
'endDate' => $endDate->format('Y-m-d\T H:i'),
|
'endDate' => $endDate->format('Y-m-d\T H:i'),
|
||||||
'aggregation' => $aggConfig,
|
'aggregation' => $aggConfig,
|
||||||
'totalMinutes' => $totalMinutes
|
'totalMinutes' => $totalMinutes,
|
||||||
|
'period' => $period,
|
||||||
|
'zoom' => $zoom
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->twig->render($response, 'servers/detail.twig', $templateData);
|
return $this->twig->render($response, 'servers/detail.twig', $templateData);
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,44 @@ class Server
|
||||||
$server['active_alerts'] = (int)$activeAlerts;
|
$server['active_alerts'] = (int)$activeAlerts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Загружаем пороги для каждого сервера
|
||||||
|
foreach ($servers as &$server) {
|
||||||
|
$stmt = $this->db->prepare("SELECT mn.name, mt.warning_threshold, mt.critical_threshold FROM metric_thresholds mt JOIN metric_names mn ON mt.metric_name_id = mn.id WHERE mt.server_id = :server_id");
|
||||||
|
$stmt->execute([':server_id' => $server['id']]);
|
||||||
|
$thresholds = $stmt->fetchAll();
|
||||||
|
$server['thresholds'] = [];
|
||||||
|
foreach ($thresholds as $t) {
|
||||||
|
$server['thresholds'][$t['name']] = [
|
||||||
|
'warning' => (float)$t['warning_threshold'],
|
||||||
|
'critical' => (float)$t['critical_threshold']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($server);
|
||||||
|
|
||||||
return $servers;
|
return $servers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getThresholds($serverId)
|
||||||
|
{
|
||||||
|
error_log("getThresholds called for server $serverId");
|
||||||
|
$stmt = $this->db->prepare("
|
||||||
|
SELECT mn.name, mt.warning_threshold, mt.critical_threshold
|
||||||
|
FROM metric_thresholds mt
|
||||||
|
JOIN metric_names mn ON mt.metric_name_id = mn.id
|
||||||
|
WHERE mt.server_id = :server_id
|
||||||
|
");
|
||||||
|
$stmt->execute([':server_id' => $serverId]);
|
||||||
|
$thresholds = $stmt->fetchAll();
|
||||||
|
error_log("getThresholds result for server $serverId: " . json_encode($thresholds));
|
||||||
|
$result = [];
|
||||||
|
foreach ($thresholds as $t) {
|
||||||
|
$result[$t['name']] = [
|
||||||
|
'warning' => (float)$t['warning_threshold'],
|
||||||
|
'critical' => (float)$t['critical_threshold']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
error_log("getThresholds returning for server $serverId: " . json_encode($result));
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,14 +28,26 @@ class NotificationService
|
||||||
*/
|
*/
|
||||||
public function sendAlertNotification($serverName, $metricName, $value, $severity, $threshold)
|
public function sendAlertNotification($serverName, $metricName, $value, $severity, $threshold)
|
||||||
{
|
{
|
||||||
$severityText = $severity === 'critical' ? 'КРИТИЧЕСКИЙ' : 'ПРЕДУПРЕЖДЕНИЕ';
|
if ($severity === 'resolved') {
|
||||||
$subject = "🚨 {$severityText}: Превышение порога {$metricName}";
|
$severityText = 'ВОССТАНОВЛЕНИЕ';
|
||||||
$message = "Сервер: {$serverName}\n";
|
$emoji = '✅';
|
||||||
$message .= "Метрика: {$metricName}\n";
|
$subject = "{$emoji} {$severityText}: {$metricName} в норме";
|
||||||
$message .= "Значение: {$value}\n";
|
$message = "Сервер: {$serverName}\n";
|
||||||
$message .= "Порог: {$threshold}\n";
|
$message .= "Метрика: {$metricName}\n";
|
||||||
$message .= "Время: " . date('d.m.Y H:i:s') . "\n";
|
$message .= "Текущее значение: {$value}\n";
|
||||||
$message .= "Серьёзность: {$severityText}";
|
$message .= "Статус: Порог более не превышен\n";
|
||||||
|
$message .= "Время: " . date('d.m.Y H:i:s');
|
||||||
|
} else {
|
||||||
|
$severityText = $severity === 'critical' ? 'КРИТИЧЕСКИЙ' : 'ПРЕДУПРЕЖДЕНИЕ';
|
||||||
|
$emoji = '🚨';
|
||||||
|
$subject = "{$emoji} {$severityText}: Превышение порога {$metricName}";
|
||||||
|
$message = "Сервер: {$serverName}\n";
|
||||||
|
$message .= "Метрика: {$metricName}\n";
|
||||||
|
$message .= "Значение: {$value}\n";
|
||||||
|
$message .= "Порог: {$threshold}\n";
|
||||||
|
$message .= "Время: " . date('d.m.Y H:i:s') . "\n";
|
||||||
|
$message .= "Серьёзность: {$severityText}";
|
||||||
|
}
|
||||||
|
|
||||||
// Отправка Email
|
// Отправка Email
|
||||||
if (!empty($this->settings['email_enabled']) && !empty($this->settings['smtp_host'])) {
|
if (!empty($this->settings['email_enabled']) && !empty($this->settings['smtp_host'])) {
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
<tr class="{% if alert.severity == 'critical' %}table-danger{% else %}table-warning{% endif %}">
|
<tr class="{% if alert.severity == 'critical' %}table-danger{% else %}table-warning{% endif %}">
|
||||||
<td>{{ alert.server_name }}</td>
|
<td>{{ alert.server_name }}</td>
|
||||||
<td>{{ alert.metric_name|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}</td>
|
<td>{{ alert.metric_name|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}</td>
|
||||||
<td>{{ alert.value }}{% if alert.metric_name ends_with '_load' or alert.metric_name ends_with '_used' %}%{% endif %}</td>
|
<td>{{ alert.value }}{% if alert.metric_name matches '/_load$/' or alert.metric_name matches '/_used$/' %}%{% endif %}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if alert.severity == 'critical' %}
|
{% if alert.severity == 'critical' %}
|
||||||
<span class="badge bg-danger"><i class="fas fa-exclamation-triangle"></i> Критично</span>
|
<span class="badge bg-danger"><i class="fas fa-exclamation-triangle"></i> Критично</span>
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Карточки серверов -->
|
<!-- Карточки серверов -->
|
||||||
|
<!-- DEBUG: {{ servers|json_encode }} -->
|
||||||
|
{% for s in servers %}<!-- DBG: id={{ s.id }} t={{ s.thresholds|json_encode }} -->{% endfor %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% for server in servers %}
|
{% for server in servers %}
|
||||||
|
<!-- DEBUG: server_id={{ server.id }} thresholds={{ server.thresholds|json_encode }} ram_metric={{ server.latest_metrics["ram_used"].value }} -->
|
||||||
|
|
||||||
<div class="col-xl-4 col-lg-6 col-md-6 mb-4">
|
<div class="col-xl-4 col-lg-6 col-md-6 mb-4">
|
||||||
<div class="card h-100 server-card" data-server-id="{{ server.id }}" data-status="{{ server.status }}">
|
<div class="card h-100 server-card" data-server-id="{{ server.id }}" data-status="{{ server.status }}">
|
||||||
|
|
@ -98,6 +101,7 @@
|
||||||
<div class="mb-2"><span class="badge bg-info">Статус: {{ server.status }}</span></div>
|
<div class="mb-2"><span class="badge bg-info">Статус: {{ server.status }}</span></div>
|
||||||
|
|
||||||
<!-- Метрики -->
|
<!-- Метрики -->
|
||||||
|
{% for s in servers %}<!-- DBG: id={{ s.id }} t={{ s.thresholds|json_encode }} -->{% endfor %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
{% if server.latest_metrics['cpu_load'] is defined %}
|
{% if server.latest_metrics['cpu_load'] is defined %}
|
||||||
<div class="col-6 mb-2">
|
<div class="col-6 mb-2">
|
||||||
|
|
@ -105,8 +109,20 @@
|
||||||
<small class="text-muted"><i class="fas fa-microchip"></i> CPU</small>
|
<small class="text-muted"><i class="fas fa-microchip"></i> CPU</small>
|
||||||
<strong>{{ server.latest_metrics['cpu_load'].value }}{{ server.latest_metrics['cpu_load'].unit }}</strong>
|
<strong>{{ server.latest_metrics['cpu_load'].value }}{{ server.latest_metrics['cpu_load'].unit }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
{% set cpu_t = server.thresholds['cpu_load']|default(null) %}
|
||||||
|
{% if cpu_t and server.latest_metrics['cpu_load'].value >= cpu_t.critical %}
|
||||||
|
{% set cpu_color = 'bg-danger' %}
|
||||||
|
{% elseif cpu_t and server.latest_metrics['cpu_load'].value >= cpu_t.warning %}
|
||||||
|
{% set cpu_color = 'bg-warning' %}
|
||||||
|
{% elseif server.latest_metrics['cpu_load'].value > 80 %}
|
||||||
|
{% set cpu_color = 'bg-danger' %}
|
||||||
|
{% elseif server.latest_metrics['cpu_load'].value > 60 %}
|
||||||
|
{% set cpu_color = 'bg-warning' %}
|
||||||
|
{% else %}
|
||||||
|
{% set cpu_color = 'bg-success' %}
|
||||||
|
{% endif %}
|
||||||
<div class="progress" style="height: 6px;">
|
<div class="progress" style="height: 6px;">
|
||||||
<div class="progress-bar {% if server.latest_metrics['cpu_load'].value > 80 %}bg-danger{% elseif server.latest_metrics['cpu_load'].value > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
<div class="progress-bar {{ cpu_color }}"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: {{ server.latest_metrics['cpu_load'].value }}%"></div>
|
style="width: {{ server.latest_metrics['cpu_load'].value }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -119,8 +135,20 @@
|
||||||
<small class="text-muted"><i class="fas fa-memory"></i> RAM</small>
|
<small class="text-muted"><i class="fas fa-memory"></i> RAM</small>
|
||||||
<strong>{{ server.latest_metrics['ram_used'].value }}{{ server.latest_metrics['ram_used'].unit }}</strong>
|
<strong>{{ server.latest_metrics['ram_used'].value }}{{ server.latest_metrics['ram_used'].unit }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
{% set ram_t = server.thresholds['ram_used']|default(null) %}
|
||||||
|
{% if ram_t and server.latest_metrics['ram_used'].value >= ram_t.critical %}
|
||||||
|
{% set ram_color = 'bg-danger' %}
|
||||||
|
{% elseif ram_t and server.latest_metrics['ram_used'].value >= ram_t.warning %}
|
||||||
|
{% set ram_color = 'bg-warning' %}
|
||||||
|
{% elseif server.latest_metrics['ram_used'].value > 80 %}
|
||||||
|
{% set ram_color = 'bg-danger' %}
|
||||||
|
{% elseif server.latest_metrics['ram_used'].value > 60 %}
|
||||||
|
{% set ram_color = 'bg-warning' %}
|
||||||
|
{% else %}
|
||||||
|
{% set ram_color = 'bg-success' %}
|
||||||
|
{% endif %}
|
||||||
<div class="progress" style="height: 6px;">
|
<div class="progress" style="height: 6px;">
|
||||||
<div class="progress-bar {% if server.latest_metrics['ram_used'].value > 80 %}bg-danger{% elseif server.latest_metrics['ram_used'].value > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
<div class="progress-bar {{ ram_color }}"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: {{ server.latest_metrics['ram_used'].value }}%"></div>
|
style="width: {{ server.latest_metrics['ram_used'].value }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -133,8 +161,20 @@
|
||||||
<small class="text-muted"><i class="fas fa-hdd"></i> Диск</small>
|
<small class="text-muted"><i class="fas fa-hdd"></i> Диск</small>
|
||||||
<strong>{{ server.latest_metrics['disk_used'].value }}{{ server.latest_metrics['disk_used'].unit }}</strong>
|
<strong>{{ server.latest_metrics['disk_used'].value }}{{ server.latest_metrics['disk_used'].unit }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
{% set disk_t = server.thresholds['disk_used']|default(null) %}
|
||||||
|
{% if disk_t and server.latest_metrics['disk_used'].value >= disk_t.critical %}
|
||||||
|
{% set disk_color = 'bg-danger' %}
|
||||||
|
{% elseif disk_t and server.latest_metrics['disk_used'].value >= disk_t.warning %}
|
||||||
|
{% set disk_color = 'bg-warning' %}
|
||||||
|
{% elseif server.latest_metrics['disk_used'].value > 80 %}
|
||||||
|
{% set disk_color = 'bg-danger' %}
|
||||||
|
{% elseif server.latest_metrics['disk_used'].value > 60 %}
|
||||||
|
{% set disk_color = 'bg-warning' %}
|
||||||
|
{% else %}
|
||||||
|
{% set disk_color = 'bg-success' %}
|
||||||
|
{% endif %}
|
||||||
<div class="progress" style="height: 6px;">
|
<div class="progress" style="height: 6px;">
|
||||||
<div class="progress-bar {% if server.latest_metrics['disk_used'].value > 80 %}bg-danger{% elseif server.latest_metrics['disk_used'].value > 60 %}bg-warning{% else %}bg-success{% endif %}"
|
<div class="progress-bar {{ disk_color }}"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
style="width: {{ server.latest_metrics['disk_used'].value }}%"></div>
|
style="width: {{ server.latest_metrics['disk_used'].value }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -88,46 +88,34 @@
|
||||||
<div class="tab-content mt-3">
|
<div class="tab-content mt-3">
|
||||||
<!-- Вкладка "Метрики" -->
|
<!-- Вкладка "Метрики" -->
|
||||||
<div class="tab-pane fade show active" id="metrics" role="tabpanel">
|
<div class="tab-pane fade show active" id="metrics" role="tabpanel">
|
||||||
<div class="row mb-3">
|
<div class="row mt-2 mb-2">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<!-- Отладка: period = {{ request.query.period }} -->
|
<small class="text-muted">Период:</small>
|
||||||
<!-- Отладка: period = {{ period }}, request = {{ request.period }} -->
|
|
||||||
<div class="btn-group d-flex" role="group">
|
|
||||||
<a href="?tab=metrics&period=24h" class="btn btn-outline-primary w-100 {% if period == '24h' or period is empty %}active{% endif %}">
|
|
||||||
24 часа
|
|
||||||
</a>
|
|
||||||
<a href="?tab=metrics&period=7d" class="btn btn-outline-primary w-100 {% if period == '7d' %}active{% endif %}">
|
|
||||||
7 дней
|
|
||||||
</a>
|
|
||||||
<a href="?tab=metrics&period=30d" class="btn btn-outline-primary w-100 {% if period == '30d' %}active{% endif %}">
|
|
||||||
30 дней
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-12 mt-1">
|
||||||
<div class="row mt-2">
|
<div class="btn-group d-flex" role="group">
|
||||||
<div class="col-md-12">
|
<a href="?tab=metrics&period=1h" class="btn btn-outline-primary w-100 {% if period == '1h' %}active{% endif %}">
|
||||||
<small class="text-muted">Масштаб:</small>
|
|
||||||
<div class="btn-group ms-2" role="group">
|
|
||||||
<a href="?tab=metrics&period={{ period }}&zoom=auto" class="btn btn-sm {% if not zoom or zoom == 'auto' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
|
||||||
авто
|
|
||||||
</a>
|
|
||||||
<a href="?tab=metrics&period={{ period }}&zoom=1h" class="btn btn-sm {% if zoom == '1h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
|
||||||
1ч
|
1ч
|
||||||
</a>
|
</a>
|
||||||
<a href="?tab=metrics&period={{ period }}&zoom=6h" class="btn btn-sm {% if zoom == '6h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
<a href="?tab=metrics&period=6h" class="btn btn-outline-primary w-100 {% if period == '6h' or period is empty %}active{% endif %}">
|
||||||
6ч
|
6ч
|
||||||
</a>
|
</a>
|
||||||
<a href="?tab=metrics&period={{ period }}&zoom=24h" class="btn btn-sm {% if zoom == '24h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
<a href="?tab=metrics&period=24h" class="btn btn-outline-primary w-100 {% if period == '24h' %}active{% endif %}">
|
||||||
24ч
|
24ч
|
||||||
</a>
|
</a>
|
||||||
<a href="?tab=metrics&period={{ period }}&zoom=7d" class="btn btn-sm {% if zoom == '7d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
<a href="?tab=metrics&period=7d" class="btn btn-outline-primary w-100 {% if period == '7d' %}active{% endif %}">
|
||||||
7д
|
7д
|
||||||
</a>
|
</a>
|
||||||
<a href="?tab=metrics&period={{ period }}&zoom=30d" class="btn btn-sm {% if zoom == '30d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
<a href="?tab=metrics&period=30d" class="btn btn-outline-primary w-100 {% if period == '30d' %}active{% endif %}">
|
||||||
30д
|
30д
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="btn-group d-flex mt-1" role="group">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary w-100" onclick="resetAllZoom()" title="Сбросить интерактивный зум">
|
||||||
|
<i class="fas fa-search-minus"></i> Сбросить зум
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">💡 Колёсико мыши = зум, перетаскивание = выделение области, Shift+колёсико = панорама</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -411,6 +399,8 @@
|
||||||
<!-- Chart.js -->
|
<!-- Chart.js -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script src="/chartjs-plugin-crosshair.min.js"></script>
|
<script src="/chartjs-plugin-crosshair.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8/hammer.min.js"></script>
|
||||||
|
<script src="/chartjs-plugin-zoom.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
// Функция для получения топ-процессов для указанного времени
|
// Функция для получения топ-процессов для указанного времени
|
||||||
|
|
@ -434,7 +424,7 @@ function fetchProcesses(serverId, time) {
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('🏆 Топ CPU:');
|
lines.push('🏆 Топ CPU:');
|
||||||
data.top_cpu.forEach(function(proc) {
|
data.top_cpu.forEach(function(proc) {
|
||||||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -443,7 +433,7 @@ function fetchProcesses(serverId, time) {
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('💾 Топ RAM:');
|
lines.push('💾 Топ RAM:');
|
||||||
data.top_ram.forEach(function(proc) {
|
data.top_ram.forEach(function(proc) {
|
||||||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -499,7 +489,7 @@ const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementByI
|
||||||
var labels{{ metricName }} = [];
|
var labels{{ metricName }} = [];
|
||||||
var data{{ metricName }} = [];
|
var data{{ metricName }} = [];
|
||||||
|
|
||||||
{% for metric in metricData|slice(0, 50000)|reverse %}
|
{% for metric in metricData|slice(0, 50000) %}
|
||||||
{% set time_val = metric.time_bucket|default(metric.created_at) %}
|
{% set time_val = metric.time_bucket|default(metric.created_at) %}
|
||||||
{% set time_format = metric.time_bucket ? 'd.m H:i' : 'H:i' %}
|
{% set time_format = metric.time_bucket ? 'd.m H:i' : 'H:i' %}
|
||||||
labels{{ metricName }}.push('{{ time_val|date(time_format) }}');
|
labels{{ metricName }}.push('{{ time_val|date(time_format) }}');
|
||||||
|
|
@ -596,7 +586,7 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('TOP CPU:');
|
lines.push('TOP CPU:');
|
||||||
data.top_cpu.forEach(function(proc) {
|
data.top_cpu.forEach(function(proc) {
|
||||||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
{% elseif metricName == 'ram_used' %}
|
{% elseif metricName == 'ram_used' %}
|
||||||
|
|
@ -605,7 +595,7 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('TOP RAM:');
|
lines.push('TOP RAM:');
|
||||||
data.top_ram.forEach(function(proc) {
|
data.top_ram.forEach(function(proc) {
|
||||||
lines.push(' ' + proc.name + ': ' + proc.value + '%');
|
lines.push(' ' + (proc.cmdline || proc.name) + ': ' + proc.value + '%');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -623,6 +613,24 @@ const chart{{ metricName|replace({'-': '_', '.': '_'}) }} = new Chart(ctx{{ metr
|
||||||
// }, 3000);
|
// }, 3000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
zoom: {
|
||||||
|
drag: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
pinch: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
wheel: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
mode: 'x'
|
||||||
|
},
|
||||||
|
pan: {
|
||||||
|
enabled: true,
|
||||||
|
mode: 'x'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -664,5 +672,17 @@ document.addEventListener('mousemove', function(e) {
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Сбросить зум на всех графиках
|
||||||
|
function resetAllZoom() {
|
||||||
|
{% for metricName, metricData in metrics %}
|
||||||
|
{% if metricName!="top_cpu_proc" and metricName!="top_ram_proc" %}
|
||||||
|
if (typeof chart{{ metricName|replace({'-': '_', '.': '_'}) }} !== 'undefined') {
|
||||||
|
chart{{ metricName|replace({'-': '_', '.': '_'}) }}.resetZoom();
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue