diff --git a/public/index.php b/public/index.php index 49c09d0..c5d2dce 100755 --- a/public/index.php +++ b/public/index.php @@ -6,6 +6,7 @@ use App\Controllers\AgentController; use App\Controllers\AdminController; use App\Controllers\AlertController; use App\Controllers\Api\MetricsController; +use App\Controllers\Api\MetricsApiController; use App\Controllers\GroupController; use App\Controllers\ServerController; use App\Controllers\ServerDetailController; @@ -187,6 +188,10 @@ $agentController = new AgentController(); $dashboardApiController = new DashboardController($twig); $app->get('/api/dashboard/stats', [$dashboardApiController, 'getDashboardData'])->add(AuthMiddleware::class); +// API для метрик сервера (динамическая загрузка) +$metricsApiController = new MetricsApiController(); +$app->get('/api/servers/{id}/metrics', [$metricsApiController, 'getServerMetrics'])->add(AuthMiddleware::class); + // Routes for groups (protected with auth middleware and csrf) $groupsGroup = $app->group('/groups', function ($group) use ($groupController) { $group->get('', [$groupController, 'index']); diff --git a/src/Controllers/Api/MetricsApiController.php b/src/Controllers/Api/MetricsApiController.php new file mode 100644 index 0000000..454c8e5 --- /dev/null +++ b/src/Controllers/Api/MetricsApiController.php @@ -0,0 +1,173 @@ +getQueryParams(); + $period = $queryParams['period'] ?? '7d'; + $startParam = $queryParams['start'] ?? null; + $endParam = $queryParams['end'] ?? null; + $zoom = $queryParams['zoom'] ?? null; + + // Вычисляем даты + $endDate = new DateTime(); + $startDate = clone $endDate; + + if ($startParam && $endParam) { + // Используем переданные даты + $startDate = new DateTime($startParam); + $endDate = new DateTime($endParam); + } else { + // Вычисляем по period + 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; + } + } + + // Вычисляем интервал агрегации + $totalMinutes = ($endDate->getTimestamp() - $startDate->getTimestamp()) / 60; + $aggregationMinutes = max(1, ceil($totalMinutes / self::MAX_POINTS)); + $aggregationSeconds = $aggregationMinutes * 60; + + // Получаем метрики для графиков (исключая uptime и top_*) + $stmt = $this->pdo->prepare(" + SELECT + mn.name, + mn.unit, + FLOOR(TIMESTAMPDIFF(SECOND, :start, sm.created_at) / :agg) as bucket, + AVG(sm.value) as value, + MAX(sm.created_at) as created_at + FROM server_metrics sm + JOIN metric_names mn ON sm.metric_name_id = mn.id + WHERE sm.server_id = :server_id + AND sm.created_at BETWEEN :start AND :end + AND mn.name NOT IN ('uptime') + AND mn.name NOT LIKE '%_proc' + 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_%' + ) + GROUP BY mn.name, bucket + ORDER BY mn.name, bucket + "); + + $stmt->execute([ + ':server_id' => $id, + ':start' => $startDate->format('Y-m-d H:i:s'), + ':end' => $endDate->format('Y-m-d H:i:s'), + ':agg' => $aggregationSeconds + ]); + + $rawData = $stmt->fetchAll(); + + // Группируем по метрикам + $datasets = []; + $labels = []; + $minTime = null; + $maxTime = null; + + foreach ($rawData as $row) { + $metricName = $row['name']; + $time = $row['created_at']; + + if (!isset($datasets[$metricName])) { + $datasets[$metricName] = []; + } + + $datasets[$metricName][] = (float)$row['value']; + + // Собираем уникальные метки времени + if (!in_array($time, $labels)) { + $labels[] = $time; + } + + if ($minTime === null || $time < $minTime) $minTime = $time; + if ($maxTime === null || $time > $maxTime) $maxTime = $time; + } + + // Форматируем labels + $formattedLabels = array_map(function($label) { + return (new DateTime($label))->format('d.m H:i'); + }, $labels); + + // Получаем TOP процессы для последней точки + $topCpu = []; + $topRam = []; + + $stmt = $this->pdo->prepare(" + SELECT mn.name, sm.value + FROM server_metrics sm + JOIN metric_names mn ON sm.metric_name_id = mn.id + WHERE sm.server_id = :server_id AND mn.name = 'top_cpu_proc' + ORDER BY sm.created_at DESC + LIMIT 5 + "); + $stmt->execute([':server_id' => $id]); + $topData = $stmt->fetchAll(); + if ($topData) { + $topCpu = json_decode($topData[0]['value'] ?? '[]', true) ?? []; + } + + $stmt = $this->pdo->prepare(" + SELECT sm.value + FROM server_metrics sm + JOIN metric_names mn ON sm.metric_name_id = mn.id + WHERE sm.server_id = :server_id AND mn.name = 'top_ram_proc' + ORDER BY sm.created_at DESC + LIMIT 1 + "); + $stmt->execute([':server_id' => $id]); + $topRamData = $stmt->fetch(); + if ($topRamData) { + $topRam = json_decode($topRamData['value'] ?? '[]', true) ?? []; + } + + // Формируем ответ + $result = [ + 'period' => $period, + 'start' => $startDate->format('Y-m-d H:i:s'), + 'end' => $endDate->format('Y-m-d H:i:s'), + 'aggregation_minutes' => $aggregationMinutes, + 'total_points' => count($labels), + 'labels' => $formattedLabels, + 'datasets' => $datasets, + 'top_cpu' => $topCpu, + 'top_ram' => $topRam + ]; + + $response->getBody()->write(json_encode($result, JSON_UNESCAPED_UNICODE)); + return $response->withHeader('Content-Type', 'application/json'); + } +} \ No newline at end of file diff --git a/src/Controllers/Api/MetricsController.php b/src/Controllers/Api/MetricsController.php index 252793b..0a56a18 100755 --- a/src/Controllers/Api/MetricsController.php +++ b/src/Controllers/Api/MetricsController.php @@ -436,60 +436,75 @@ class MetricsController extends Model public function getProcesses(Request $request, Response $response, $args) { - $serverId = $args['id']; - $timeParam = $request->getQueryParams()['time'] ?? null; + try { + $serverId = $args['id']; + $timeParam = $request->getQueryParams()['time'] ?? null; - if (!$timeParam) { - return $response->withStatus(400)->getBody()->write(json_encode(['error' => 'Time parameter required'])); + if (!$timeParam) { + $response->getBody()->write(json_encode(['error' => 'Time parameter required'])); + return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); + } + + $timestamp = strtotime($timeParam); + // Парсинг формата d.m H:i (12.04 07:48) + if ($timestamp === false && preg_match("/^(\d{1,2})\.(\d{2}) (\d{1,2}):(\d{2})$/", $timeParam, $m)) { + $timestamp = strtotime(date("Y") . "-" . $m[2] . "-" . $m[1] . " " . $m[3] . ":" . $m[4] . ":00"); + } + + if ($timestamp === false && preg_match('/^\d{1,2}:\d{2}$/', $timeParam)) { + $today = date('Y-m-d'); + $timestamp = strtotime($today . ' ' . $timeParam); + } + + // Если timestamp всё ещё false или равен 0 (1970) - возвращаем пустой результат + if ($timestamp === false || $timestamp < 0) { + $response->getBody()->write(json_encode([ + 'top_cpu' => [], + 'top_ram' => [], + 'time' => $timeParam, + 'error' => 'Invalid time format' + ])); + return $response->withHeader('Content-Type', 'application/json'); + } + + $time = date('Y-m-d H:i:s', $timestamp); + + $stmt = $this->pdo->prepare(" + SELECT value FROM server_metrics sm + JOIN metric_names mn ON sm.metric_name_id = mn.id + WHERE sm.server_id = :server_id AND mn.name = 'top_cpu_proc' + AND sm.created_at BETWEEN DATE_SUB(:time1, INTERVAL 30 SECOND) AND DATE_ADD(:time2, INTERVAL 30 SECOND) + ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3)) LIMIT 1 + "); + $stmt->execute([':server_id' => $serverId, ':time1' => $time, ':time2' => $time, ':time3' => $time]); + $topCpuResult = $stmt->fetch(); + + $stmt = $this->pdo->prepare(" + SELECT value FROM server_metrics sm + JOIN metric_names mn ON sm.metric_name_id = mn.id + WHERE sm.server_id = :server_id AND mn.name = 'top_ram_proc' + AND sm.created_at BETWEEN DATE_SUB(:time1, INTERVAL 30 SECOND) AND DATE_ADD(:time2, INTERVAL 30 SECOND) + ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3)) LIMIT 1 + "); + $stmt->execute([':server_id' => $serverId, ':time1' => $time, ':time2' => $time, ':time3' => $time]); + $topRamResult = $stmt->fetch(); + + $topCpu = $topCpuResult ? json_decode($topCpuResult['value'], true) : []; + $topRam = $topRamResult ? json_decode($topRamResult['value'], true) : []; + + $response->getBody()->write(json_encode([ + 'top_cpu' => $topCpu, + 'top_ram' => $topRam, + 'time' => $time + ])); + + return $response->withHeader('Content-Type', 'application/json'); + } catch (\Exception $e) { + $response->getBody()->write(json_encode([ + 'error' => $e->getMessage() + ])); + return $response->withStatus(500)->withHeader('Content-Type', 'application/json'); } - - $timestamp = strtotime($timeParam); - // Парсинг формата d.m H:i (12.04 07:48) - if ($timestamp === false && preg_match("/^(\d{1,2})\.(\d{2}) (\d{1,2}):(\d{2})$/", $timeParam, $m)) { - $timestamp = strtotime(date("Y") . "-" . $m[2] . "-" . $m[1] . " " . $m[3] . ":" . $m[4] . ":00"); - } - - if ($timestamp === false && preg_match('/^\d{1,2}:\d{2}$/', $timeParam)) { - $today = date('Y-m-d'); - $timestamp = strtotime($today . ' ' . $timeParam); - } - - if ($timestamp === false) { - return $response->withStatus(400)->getBody()->write(json_encode(['error' => 'Invalid time format'])); - } - - $time = date('Y-m-d H:i:s', $timestamp); - - $stmt = $this->pdo->prepare(" - SELECT value FROM server_metrics sm - JOIN metric_names mn ON sm.metric_name_id = mn.id - WHERE sm.server_id = :server_id AND mn.name = 'top_cpu_proc' - AND sm.created_at BETWEEN DATE_SUB(:time1, INTERVAL 30 SECOND) AND DATE_ADD(:time2, INTERVAL 30 SECOND) - ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3)) LIMIT 1 - "); - $stmt->execute([':server_id' => $serverId, ':time1' => $time, ':time2' => $time, ':time3' => $time]); - $topCpuResult = $stmt->fetch(); - - $stmt = $this->pdo->prepare(" - SELECT value FROM server_metrics sm - JOIN metric_names mn ON sm.metric_name_id = mn.id - WHERE sm.server_id = :server_id AND mn.name = 'top_ram_proc' - AND sm.created_at BETWEEN DATE_SUB(:time1, INTERVAL 30 SECOND) AND DATE_ADD(:time2, INTERVAL 30 SECOND) - ORDER BY ABS(TIMESTAMPDIFF(SECOND, sm.created_at, :time3)) LIMIT 1 - "); - $stmt->execute([':server_id' => $serverId, ':time1' => $time, ':time2' => $time, ':time3' => $time]); - $topRamResult = $stmt->fetch(); - - $topCpu = $topCpuResult ? json_decode($topCpuResult['value'], true) : []; - $topRam = $topRamResult ? json_decode($topRamResult['value'], true) : []; - - $response->getBody()->write(json_encode([ - 'top_cpu' => $topCpu, - 'top_ram' => $topRam, - 'time' => $time - ])); - - return $response->withHeader('Content-Type', 'application/json'); } public function getMetrics(Request $request, Response $response, $args) diff --git a/src/Controllers/ServerDetailController.php b/src/Controllers/ServerDetailController.php index 62fcdcc..02be308 100755 --- a/src/Controllers/ServerDetailController.php +++ b/src/Controllers/ServerDetailController.php @@ -25,7 +25,8 @@ class ServerDetailController extends Model // Получаем информацию о сервере $stmt = $this->pdo->prepare(" - SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color + SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color, + (SELECT MAX(sm.created_at) FROM server_metrics sm WHERE sm.server_id = s.id) as last_seen FROM servers s LEFT JOIN server_groups sg ON s.group_id = sg.id WHERE s.id = :id @@ -133,6 +134,7 @@ class ServerDetailController extends Model WHERE sm.server_id = :id AND sm.created_at >= :start_date AND sm.created_at <= :end_date + AND mn.name != 'uptime' {$groupBy} ORDER BY time_bucket ASC "; @@ -146,6 +148,7 @@ class ServerDetailController extends Model WHERE sm.server_id = :id AND sm.created_at >= :start_date AND sm.created_at <= :end_date + AND mn.name != 'uptime' ORDER BY sm.created_at ASC "; $stmt = $this->pdo->prepare($sql); @@ -246,6 +249,19 @@ class ServerDetailController extends Model $monitorServices = json_decode($agentConfig['monitor_services'], true) ?? []; } + // Получаем последние значения метрик (для виджета аптайма) + $stmt = $this->pdo->prepare(" + SELECT mn.name, sm.value, sm.created_at + FROM server_metrics sm + JOIN metric_names mn ON sm.metric_name_id = mn.id + WHERE sm.server_id = :id + AND mn.name = 'uptime' + ORDER BY sm.created_at DESC + LIMIT 1 + "); + $stmt->execute([':id' => $id]); + $latestUptime = $stmt->fetch(); + $templateData = [ 'title' => 'Сервер: ' . $server['name'], 'server' => $server, @@ -254,6 +270,7 @@ class ServerDetailController extends Model 'existingThresholds' => $existingThresholds, 'allServices' => $allServices, 'monitorServices' => $monitorServices, + 'latestUptime' => $latestUptime, 'startDate' => $startDate->format('Y-m-d\T H:i'), 'endDate' => $endDate->format('Y-m-d\T H:i'), 'aggregation' => $aggConfig, diff --git a/templates/servers/detail.twig b/templates/servers/detail.twig index c182fbd..40f3a0c 100755 --- a/templates/servers/detail.twig +++ b/templates/servers/detail.twig @@ -69,8 +69,8 @@