mirvmon/src/Controllers/ServerDetailController.php

430 lines
17 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
// src/Controllers/ServerDetailController.php
namespace App\Controllers;
use App\Models\Model;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Views\Twig;
use DateTime;
class ServerDetailController extends Model
{
private $twig;
public function __construct(Twig $twig)
{
parent::__construct();
$this->twig = $twig;
}
public function show(Request $request, Response $response, $args)
{
$id = $args['id'];
// Получаем информацию о сервере
$stmt = $this->pdo->prepare("
SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color
FROM servers s
LEFT JOIN server_groups sg ON s.group_id = sg.id
WHERE s.id = :id
");
$stmt->execute([':id' => $id]);
$server = $stmt->fetch();
if (!$server) {
return $response->withHeader('Location', '/servers')->withStatus(302);
}
// Получаем параметры
$queryParams = $request->getQueryParams();
$startDate = $queryParams['start'] ?? null;
$endDate = $queryParams['end'] ?? null;
$period = $queryParams['period'] ?? '24h';
$zoom = $queryParams['zoom'] ?? null;
// Если даты не указаны, вычисляем по period
if (!$startDate || !$endDate) {
$endDate = new DateTime();
$startDate = clone $endDate;
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 {
$startDate = new DateTime($startDate);
$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
if ($endDate <= $startDate) {
$endDate = clone $startDate;
$endDate->modify('+24 hours');
}
// Вычисляем длительность периода для агрегации
$interval = $startDate->diff($endDate);
$totalMinutes = ($interval->days * 24 * 60) + ($interval->h * 60) + $interval->i;
// Конфигурация агрегации на основе дат
$aggConfig = $this->getAggregationConfigFromDates($startDate, $endDate, $totalMinutes);
$groupBy = $aggConfig['groupBy'];
$bucketFormat = $aggConfig['format'];
// Форматируем даты для SQL
$startStr = $startDate->format('Y-m-d H:i:s');
$endStr = $endDate->format('Y-m-d H:i:s');
// Запрос с агрегацией если нужно
if ($groupBy) {
$sql = "
SELECT
AVG(sm.value) as value,
mn.name,
mn.unit,
DATE_FORMAT(sm.created_at, '{$bucketFormat}') as time_bucket
FROM server_metrics sm
JOIN metric_names mn ON sm.metric_name_id = mn.id
WHERE sm.server_id = :id
AND sm.created_at >= :start_date
AND sm.created_at <= :end_date
{$groupBy}
ORDER BY time_bucket ASC
";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id, ':start_date' => $startStr, ':end_date' => $endStr]);
} else {
$sql = "
SELECT sm.value, mn.name, mn.unit, sm.created_at
FROM server_metrics sm
JOIN metric_names mn ON sm.metric_name_id = mn.id
WHERE sm.server_id = :id
AND sm.created_at >= :start_date
AND sm.created_at <= :end_date
ORDER BY sm.created_at ASC
";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id, ':start_date' => $startStr, ':end_date' => $endStr]);
}
$metrics = $stmt->fetchAll();
// Группируем метрики
$groupedMetrics = [];
foreach ($metrics as $metric) {
$metricName = $metric['name'];
if (!isset($groupedMetrics[$metricName])) {
$groupedMetrics[$metricName] = [];
}
$groupedMetrics[$metricName][] = $metric;
}
// Сортируем метрики в фиксированном порядке: cpu_load → ram_used → disk_used → остальные
$priorityOrder = ['cpu_load', 'ram_used', 'disk_used'];
$sortedMetrics = [];
// Сначала добавляем приоритетные метрики в нужном порядке
foreach ($priorityOrder as $metricName) {
if (isset($groupedMetrics[$metricName])) {
$sortedMetrics[$metricName] = $groupedMetrics[$metricName];
unset($groupedMetrics[$metricName]);
}
}
// Затем добавляем остальные метрики (например, top_cpu_proc, top_ram_proc)
foreach ($groupedMetrics as $metricName => $metricData) {
$sortedMetrics[$metricName] = $metricData;
}
$groupedMetrics = $sortedMetrics;
// Пороги
$stmt = $this->pdo->prepare("
SELECT mt.warning_threshold, mt.critical_threshold, mt.duration, mn.name
FROM metric_thresholds mt
JOIN metric_names mn ON mt.metric_name_id = mn.id
WHERE mt.server_id = :id
");
$stmt->execute([':id' => $id]);
$existingThresholds = [];
foreach ($stmt->fetchAll() as $threshold) {
$existingThresholds[$threshold['name']] = [
'warning' => $threshold['warning_threshold'],
'critical' => $threshold['critical_threshold'],
'duration' => $threshold['duration']
];
}
// Типы метрик — только те что отображаются на графиках и есть у сервера
$stmt = $this->pdo->prepare("
SELECT DISTINCT mn.id, mn.name, mn.unit
FROM metric_names mn
JOIN server_metrics sm ON sm.metric_name_id = mn.id
WHERE sm.server_id = :id
AND mn.name != 'uptime'
AND (
mn.name IN ('cpu_load', 'ram_used')
OR mn.name LIKE 'disk_used_%'
OR mn.name LIKE 'net_in_%'
OR mn.name LIKE 'net_out_%'
OR mn.name LIKE 'temp_%'
)
ORDER BY
CASE
WHEN mn.name = 'cpu_load' THEN 1
WHEN mn.name = 'ram_used' THEN 2
WHEN mn.name LIKE 'disk_used_%' THEN 3
WHEN mn.name LIKE 'net_in_%' THEN 4
WHEN mn.name LIKE 'net_out_%' THEN 5
WHEN mn.name LIKE 'temp_%' THEN 6
END,
mn.name
");
$stmt->execute([':id' => $id]);
$allMetricTypes = $stmt->fetchAll();
// Сервисы
$stmt = $this->pdo->prepare("
SELECT service_name, status, load_state, active_state, sub_state
FROM service_status WHERE server_id = :server_id ORDER BY service_name
");
$stmt->execute([':server_id' => $id]);
$allServices = $stmt->fetchAll();
// Мониторинг сервисов
$stmt = $this->pdo->prepare("SELECT monitor_services FROM agent_configs WHERE server_id = :server_id");
$stmt->execute([':server_id' => $id]);
$agentConfig = $stmt->fetch();
$monitorServices = [];
if ($agentConfig && !empty($agentConfig['monitor_services'])) {
$monitorServices = json_decode($agentConfig['monitor_services'], true) ?? [];
}
$templateData = [
'title' => 'Сервер: ' . $server['name'],
'server' => $server,
'metrics' => $groupedMetrics,
'allMetricTypes' => $allMetricTypes,
'existingThresholds' => $existingThresholds,
'allServices' => $allServices,
'monitorServices' => $monitorServices,
'startDate' => $startDate->format('Y-m-d\T H:i'),
'endDate' => $endDate->format('Y-m-d\T H:i'),
'aggregation' => $aggConfig,
'totalMinutes' => $totalMinutes,
'period' => $period,
'zoom' => $zoom
];
return $this->twig->render($response, 'servers/detail.twig', $templateData);
}
private function getAggregationConfigFromDates(DateTime $startDate, DateTime $endDate, int $totalMinutes): array
{
// Target: ~400 points on chart for optimal performance
// Formula: aggregate_minutes = total_minutes / 400
$targetPoints = 400;
$aggregateMinutes = ceil($totalMinutes / $targetPoints);
// Определяем формат группировки на основе длительности агрегации
if ($aggregateMinutes <= 1) {
// Менее 1 минуты — без агрегации
return [
'groupBy' => null,
'format' => '%Y-%m-%d %H:%i:%s',
'aggregate_minutes' => 0
];
} elseif ($aggregateMinutes < 60) {
// Минуты — группировка по минутам
return [
'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:%i')",
'format' => '%Y-%m-%d %H:%i',
'aggregate_minutes' => $aggregateMinutes
];
} elseif ($aggregateMinutes < 1440) {
// Часы — группировка по часам
return [
'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:00')",
'format' => '%Y-%m-%d %H:00',
'aggregate_minutes' => $aggregateMinutes
];
} else {
// Дни — группировка по дням
return [
'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d')",
'format' => '%Y-%m-%d',
'aggregate_minutes' => $aggregateMinutes
];
}
}
public function saveThresholds(Request $request, Response $response, $args)
{
$id = $args['id'];
$params = $request->getParsedBody();
// Получаем только метрики которые есть у сервера и отображаются на графиках
$stmt = $this->pdo->prepare("
SELECT DISTINCT mn.id, mn.name, mn.unit
FROM metric_names mn
JOIN server_metrics sm ON sm.metric_name_id = mn.id
WHERE sm.server_id = :id
AND mn.name != 'uptime'
AND (
mn.name IN ('cpu_load', 'ram_used')
OR mn.name LIKE 'disk_used_%'
OR mn.name LIKE 'net_in_%'
OR mn.name LIKE 'net_out_%'
OR mn.name LIKE 'temp_%'
)
ORDER BY
CASE
WHEN mn.name = 'cpu_load' THEN 1
WHEN mn.name = 'ram_used' THEN 2
WHEN mn.name LIKE 'disk_used_%' THEN 3
WHEN mn.name LIKE 'net_in_%' THEN 4
WHEN mn.name LIKE 'net_out_%' THEN 5
WHEN mn.name LIKE 'temp_%' THEN 6
END,
mn.name
");
$stmt->execute([':id' => $id]);
$metricTypes = $stmt->fetchAll();
$stmt = $this->pdo->prepare("DELETE FROM metric_thresholds WHERE server_id = :server_id");
$stmt->execute([':server_id' => $id]);
$insertStmt = $this->pdo->prepare("
INSERT INTO metric_thresholds (server_id, metric_name_id, warning_threshold, critical_threshold, duration)
VALUES (:server_id, :metric_name_id, :warning_threshold, :critical_threshold, :duration)
");
// Дефолтные значения порогов (пока хардкод, потом из настроек)
$defaultWarning = 80;
$defaultCritical = 90;
$defaultDuration = 0;
$saved = [];
$usedDefaults = [];
foreach ($metricTypes as $metricType) {
$warning = $params[$metricType['name'] . '_warning'] ?? '';
$critical = $params[$metricType['name'] . '_critical'] ?? '';
$duration = $params[$metricType['name'] . '_duration'] ?? '';
// Сохраняем если хотя бы один порог заполнен
if ($warning !== '' || $critical !== '') {
// Если не указано - используем дефолт
$warningVal = $warning !== '' ? (float)$warning : $defaultWarning;
$criticalVal = $critical !== '' ? (float)$critical : $defaultCritical;
$durationVal = $duration !== '' ? (int)$duration : $defaultDuration;
$insertStmt->execute([
':server_id' => $id,
':metric_name_id' => $metricType['id'],
':warning_threshold' => $warningVal,
':critical_threshold' => $criticalVal,
':duration' => $durationVal
]);
$name = $metricType['name'];
$used = [];
if ($warning === '') $used[] = 'warning=' . $defaultWarning;
if ($critical === '') $used[] = 'critical=' . $defaultCritical;
if ($duration === '') $used[] = 'duration=' . $defaultDuration;
if (count($used) > 0) {
$usedDefaults[] = $name . ' (' . implode(', ', $used) . ')';
} else {
$saved[] = $name;
}
}
}
// Формируем flash сообщение
$messages = [];
if (count($saved) > 0) {
$messages[] = 'Сохранено: ' . implode(', ', $saved);
}
if (count($usedDefaults) > 0) {
$messages[] = 'Для остальных подставлены значения по умолчанию: ' . implode(', ', $usedDefaults);
}
if (count($messages) === 0) {
$messages[] = 'Все пороги удалены';
}
$_SESSION['flash_message'] = implode('. ', $messages);
$_SESSION['flash_type'] = count($usedDefaults) > 0 ? 'warning' : 'success';
return $response->withHeader('Location', "/servers/{$id}?tab=thresholds")->withStatus(302);
}
public function saveServices(Request $request, Response $response, $args)
{
$id = $args['id'];
$params = $request->getParsedBody();
$services = $params['services'] ?? [];
if (is_string($services)) {
$services = json_decode($services, true) ?? [];
}
$stmt = $this->pdo->prepare("
INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled)
VALUES (:server_id, 60, :services, TRUE)
ON DUPLICATE KEY UPDATE monitor_services = VALUES(monitor_services), updated_at = CURRENT_TIMESTAMP
");
$stmt->execute([':server_id' => $id, ':services' => json_encode($services)]);
return $response->withHeader('Location', "/servers/{$id}?tab=services")->withStatus(302);
}
}