Fix server metrics view and backfill trends

This commit is contained in:
mirivlad 2026-04-26 20:20:28 +08:00
parent 10e646a26a
commit a809b55b86
12 changed files with 879 additions and 1463 deletions

86
cron/backfill_trends.php Executable file → Normal file
View File

@ -1,51 +1,50 @@
#!/usr/bin/env php
<?php
// cron/backfill_trends.php - запустить один раз для заполнения трендов
// cron/backfill_trends.php
// Запуск:
// php /var/www/mon/cron/backfill_trends.php
// php /var/www/mon/cron/backfill_trends.php 30
// По умолчанию заполняет trends за последние 30 дней.
require __DIR__ . '/../vendor/autoload.php';
$pdo = new PDO("mysql:host=localhost;dbname=monitoring_system;charset=utf8mb4", "mon_user", "mon_password_123", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
use Config\DatabaseConfig;
// Заполняем за последние 48 часов (можно увеличить)
$hours = 48;
$processed = 0;
$errors = 0;
$days = isset($argv[1]) ? max(1, (int)$argv[1]) : 30;
$pdo = DatabaseConfig::getInstance();
for ($i = 1; $i <= $hours; $i++) {
$hourStart = new DateTime();
$hourStart->modify("-{$i} hour");
$hourStart->setTime((int)$hourStart->format('H'), 0, 0);
$startedAt = new DateTime();
$rangeStart = (clone $startedAt)->modify("-{$days} days")->format('Y-m-d H:i:s');
$hourEnd = clone $hourStart;
$hourEnd->modify('+59 minutes +59 seconds');
echo "Backfilling server_metrics_trends for the last {$days} days...\n";
echo "Range start: {$rangeStart}\n";
$periodStartStr = $hourStart->format('Y-m-d H:00:00');
$periodEndStr = $hourEnd->format('Y-m-d H:59:59');
// Получаем все серверы
$stmt = $pdo->query("SELECT id FROM servers");
$servers = $stmt->fetchAll(PDO::FETCH_COLUMN);
foreach ($servers as $serverId) {
// Исключаем метрики с JSON данными (top_cpu_proc, top_ram_proc)
$sql = "
INSERT INTO server_metrics_trends (server_id, metric_name_id, period_start, avg_value, min_value, max_value, count_samples)
INSERT INTO server_metrics_trends (
server_id,
metric_name_id,
period_start,
avg_value,
min_value,
max_value,
count_samples
)
SELECT
sm.server_id,
sm.metric_name_id,
:period_start,
AVG(CAST(sm.value AS DECIMAL(20,4))),
MIN(CAST(sm.value AS DECIMAL(20,4))),
MAX(CAST(sm.value AS DECIMAL(20,4))),
COUNT(*)
DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:00:00') AS period_start,
AVG(CAST(sm.value AS DECIMAL(20,4))) AS avg_value,
MIN(CAST(sm.value AS DECIMAL(20,4))) AS min_value,
MAX(CAST(sm.value AS DECIMAL(20,4))) AS max_value,
COUNT(*) AS count_samples
FROM server_metrics sm
INNER JOIN metric_names mn ON sm.metric_name_id = mn.id
WHERE sm.server_id = :server_id
AND sm.created_at >= :start_date
AND sm.created_at <= :end_date
INNER JOIN metric_names mn ON mn.id = sm.metric_name_id
WHERE sm.created_at >= :range_start
AND mn.name NOT IN ('top_cpu_proc', 'top_ram_proc')
GROUP BY sm.server_id, sm.metric_name_id
GROUP BY
sm.server_id,
sm.metric_name_id,
DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:00:00')
ON DUPLICATE KEY UPDATE
avg_value = VALUES(avg_value),
min_value = VALUES(min_value),
@ -54,21 +53,8 @@ for ($i = 1; $i <= $hours; $i++) {
created_at = NOW()
";
try {
$stmt = $pdo->prepare($sql);
$stmt->execute([
':server_id' => $serverId,
':period_start' => $periodStartStr,
':start_date' => $periodStartStr,
':end_date' => $periodEndStr
]);
$processed++;
} catch (Exception $e) {
$errors++;
}
}
$stmt->execute([':range_start' => $rangeStart]);
echo "Processed hour: $periodStartStr ($processed servers)\n";
}
echo "Done! Processed: $processed, Errors: $errors\n";
$duration = (new DateTime())->getTimestamp() - $startedAt->getTimestamp();
echo "Done in {$duration} sec.\n";

View File

@ -1,57 +0,0 @@
<?php
// Простое логирование POST запросов для отладки
ini_set("session.save_path", "/var/www/mon/sessions");
session_start();
// Логируем все входящие данные
$timestamp = date('Y-m-d H:i:s');
$method = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
$postData = file_get_contents("php://input");
$logLine = "$timestamp - $method $uri\n";
$logLine .= "POST data: $postData\n";
$logLine .= "POST params: " . json_encode($_POST, JSON_PRETTY_PRINT) . "\n";
$logLine .= "SESSION user_id: " . ($_SESSION['user_id'] ?? 'null') . "\n";
$logLine .= "-------------------\n";
file_put_contents('/tmp/login_debug.log', $logLine, FILE_APPEND);
// Продолжаем с обычным кодом
require_once '/var/www/mon/vendor/autoload.php';
use App\Models\User;
if ($method === 'POST' && $uri === '/login') {
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
echo "DEBUG: Processing login for user: $username\n";
echo "DEBUG: POST data: $postData\n";
$userModel = new User();
$user = $userModel->authenticate($username, $password);
if ($user) {
echo "DEBUG: Auth successful!\n";
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
header('Location: /');
exit;
} else {
echo "DEBUG: Auth failed!\n";
header('Location: /login');
exit;
}
}
?>
<html>
<body>
<h1>Debug Mode</h1>
<p>Check /tmp/login_debug.log for details</p>
</body>
</html>

View File

@ -1,202 +0,0 @@
<?php
// public/index.php - updated dashboard route
use App\Controllers\AgentController;
use App\Controllers\AdminController;
use App\Controllers\AlertController;
use App\Controllers\Api\MetricsController;
use App\Controllers\GroupController;
use App\Controllers\ServerController;
use App\Controllers\ServerDetailController;
use App\Controllers\DashboardController;
use App\Middlewares\AuthMiddleware;
use App\Middlewares\SessionMiddleware;
use App\Models\User;
use App\Models\Server as ServerModel;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Csrf\Guard;
use Slim\Factory\AppFactory;
use Slim\Views\Twig;
use Slim\Views\TwigMiddleware;
require_once __DIR__ . '/../vendor/autoload.php';
// Start session
session_start();
// Create Slim app
$app = AppFactory::create();
// Create CSRF Guard
$csrf = new Guard($app->getResponseFactory());
$csrf->setPersistentTokenMode(true);
// Create Twig view
$twig = Twig::create(__DIR__ . '/../templates', ['cache' => false]);
// Add CSRF middleware FIRST
$app->add($csrf);
// Add Twig middleware
$twigMiddleware = TwigMiddleware::create($app, $twig);
$app->add($twigMiddleware);
$sessionMiddleware = new AppMiddlewaresSessionMiddleware($twig);
$app->add($sessionMiddleware);
// Add session data to Twig$sessionMiddleware = new SessionMiddleware($twig);$app->add($sessionMiddleware);
// Add a route to get CSRF tokens via AJAX
$app->get('/csrf-token', function (Request $request, Response $response, $args) use ($csrf) {
$data = [
'name_key' => $csrf->getTokenNameKey(),
'value_key' => $csrf->getTokenValueKey(),
'name' => $csrf->getTokenName(),
'value' => $csrf->getTokenValue()
];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
});
// Define /test route
$app->get('/test', function (Request $request, Response $response, $args) use ($twig) {
$templateData = [
'title' => 'Тест системы',
'message' => 'Система мониторинга запущена'
];
return $twig->render($response, 'test.twig', $templateData);
});
// Login routes (without auth middleware, but with CSRF)
$app->get('/login', function (Request $request, Response $response, $args) use ($twig, $csrf) {
$templateData = [
'title' => 'Вход в систему',
'csrf' => [
'name_key' => $csrf->getTokenNameKey(),
'value_key' => $csrf->getTokenValueKey(),
'name' => $csrf->getTokenName(),
'value' => $csrf->getTokenValue()
]
];
return $twig->render($response, 'login.twig', $templateData);
});
$app->post('/login', function (Request $request, Response $response, $args) use ($csrf) {
$params = $request->getParsedBody();
// Validate CSRF token
$nameKey = $csrf->getTokenNameKey();
$valueKey = $csrf->getTokenValueKey();
if (!isset($params[$nameKey]) || !isset($params[$valueKey]) || !$csrf->validateToken($params[$nameKey], $params[$valueKey])) {
error_log('CSRF validation failed for /login');
return $response->withHeader('Location', '/login')->withStatus(302);
}
$username = $params['username'] ?? '';
$password = $params['password'] ?? '';
$userModel = new User();
$user = $userModel->authenticate($username, $password);
if ($user) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
return $response->withHeader('Location', '/')->withStatus(302);
} else {
return $response->withHeader('Location', '/login')->withStatus(302);
}
});
// Logout route (without auth middleware)
$app->get('/logout', function (Request $request, Response $response, $args) {
session_destroy();
return $response->withHeader('Location', '/login')->withStatus(302);
});
// Dashboard route (protected with auth middleware)
$app->get('/', function (Request $request, Response $response, $args) use ($twig) {
$serverModel = new ServerModel();
// Get statistics
$stats = $serverModel->getStats();
// Get servers with latest metrics
$servers = $serverModel->getAll();
$templateData = [
'title' => 'Дашборд мониторинга',
'stats' => $stats,
'servers' => $servers
];
return $twig->render($response, 'dashboard.twig', $templateData);
})->add(AuthMiddleware::class);
// Routes for groups (protected with auth middleware)
$groupController = new GroupController($twig);
$app->get('/groups', [$groupController, 'index'])->add(AuthMiddleware::class);
$app->get('/groups/create', [$groupController, 'create'])->add(AuthMiddleware::class);
$app->post('/groups', [$groupController, 'store'])->add(AuthMiddleware::class);
$app->get('/groups/{id}/edit', [$groupController, 'edit'])->add(AuthMiddleware::class);
$app->post('/groups/{id}', [$groupController, 'update'])->add(AuthMiddleware::class);
$app->delete('/groups/{id}', [$groupController, 'delete'])->add(AuthMiddleware::class);
// Routes for servers (protected with auth middleware)
$serverController = new ServerController($twig);
$app->get('/servers', [$serverController, 'index'])->add(AuthMiddleware::class);
$app->get('/servers/create', [$serverController, 'create'])->add(AuthMiddleware::class);
$app->post('/servers', [$serverController, 'store'])->add(AuthMiddleware::class);
$app->get('/servers/{id}/edit', [$serverController, 'edit'])->add(AuthMiddleware::class);
$app->post('/servers/{id}', [$serverController, 'update'])->add(AuthMiddleware::class);
$app->delete('/servers/{id}', [$serverController, 'delete'])->add(AuthMiddleware::class);
$app->get('/servers/{id}/regenerate-token', [$serverController, 'regenerateToken'])->add(AuthMiddleware::class);
// Server detail route (protected with auth middleware)
$serverDetailController = new ServerDetailController($twig);
$app->get('/servers/{id}', [$serverDetailController, 'show'])->add(AuthMiddleware::class);
// Alerts routes (protected with auth middleware)
$alertController = new AlertController($twig);
$app->get('/alerts', [$alertController, 'index'])->add(AuthMiddleware::class);
$app->get('/alerts/{id}/resolve', [$alertController, 'markAsResolved'])->add(AuthMiddleware::class);
// Admin routes (protected with auth middleware)
$adminController = new AdminController($twig);
$app->get('/admin/users', [$adminController, 'usersList'])->add(AuthMiddleware::class);
$app->get('/admin/notifications', [$adminController, 'notificationSettings'])->add(AuthMiddleware::class);
// API route for agents (public, no auth middleware)
$metricsController = new MetricsController();
$app->post('/api/v1/metrics', [$metricsController, 'collectMetrics']);
// API status endpoint (public, no auth middleware)
$app->get('/api/status', function (Request $request, Response $response, $args) {
$data = [
'status' => 'ok',
'timestamp' => date('Y-m-d H:i:s'),
'version' => '1.0.0'
];
$response->getBody()->write(json_encode($data));
return $response
->withHeader('Content-Type', 'application/json');
});
// Agent installation script route (public, no auth middleware)
$agentController = new AgentController();
$app->get('/agent/install.sh', [$agentController, 'generateInstallScript']);
// Run app
$app->run();

View File

@ -1,46 +0,0 @@
<?php
session_start();
// Простая проверка пароля (для теста)
if (isset($_POST['username']) && isset($_POST['password'])) {
$username = $_POST['username'];
$password = $_POST['password'];
// Хешированный пароль для admin_test_2026
$correctHash = '$2y$10$5PhDSHiF1J6yxcEldOsluOSmUYaO1bWa7swFmfmP/Slj.HJOh5t2O';
$inputHash = password_hash($password, PASSWORD_DEFAULT);
// Для теста используем прямое сравнение хешей
if ($username === 'admin' && password_verify($password, $correctHash)) {
$_SESSION['user_id'] = 1;
$_SESSION['username'] = 'admin';
$_SESSION['role'] = 'admin';
$_SESSION['logged_in'] = time();
header('Location: /');
exit;
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Тест входа</title>
<meta charset="utf-8">
</head>
<body>
<h1>Тестовый вход</h1>
<form method="post">
<p>
Логин: <input type="text" name="username" value="admin">
</p>
<p>
Пароль: <input type="password" name="password">
</p>
<p>
<button type="submit">Войти</button>
</p>
</form>
<p>Тестовые креды: admin / admin_test_2026</p>
</body>
</html>

View File

@ -1,12 +0,0 @@
<?php
session_start();
header('Content-Type: application/json');
echo json_encode([
'session_id' => session_id(),
'user_id' => $_SESSION['user_id'] ?? null,
'username' => $_SESSION['username'] ?? null,
'role' => $_SESSION['role'] ?? null,
'session_data' => $_SESSION
], JSON_PRETTY_PRINT);

View File

@ -1,23 +0,0 @@
<?php
session_start();
// Проверяем существующую сессию
echo "Текущая сессия:\n";
echo "Session ID: " . session_id() . "\n";
echo "Session data: " . json_encode($_SESSION, JSON_PRETTY_PRINT) . "\n";
// Если сессия пуста, создаем пользователя admin
if (!isset($_SESSION["user_id"])) {
$_SESSION["user_id"] = 1;
$_SESSION["username"] = "admin";
$_SESSION["role"] = "admin";
$_SESSION["created_at"] = time();
echo "\n✅ Сессия создана!\n";
} else {
echo "\n✅ Сессия уже существует!\n";
echo "Время создания: " . date("Y-m-d H:i:s", $_SESSION["created_at"]) . "\n";
}
// Информация о cookies
echo "\n🍪 Cookie информация:\n

View File

@ -1,19 +0,0 @@
<?php
session_start();
require_once '/var/www/mon/vendor/autoload.php';
use Config\DatabaseConfig;
use App\Models\User;
$pdo = DatabaseConfig::getInstance();
$userModel = new User();
// Устанавливаем пароль admin в сессии напрямую
$_SESSION['user_id'] = 1;
$_SESSION['username'] = 'admin';
$_SESSION['role'] = 'admin';
echo "✅ Сессия admin создана!\n";
echo "Session ID: " . session_id() . "\n";
echo "Session data: " . json_encode($_SESSION, JSON_PRETTY_PRINT) . "\n";

View File

@ -141,12 +141,20 @@ class ServerController extends Model
$tokenRow = $stmt->fetch();
$decryptedToken = $tokenRow ? \App\Utils\EncryptionHelper::decrypt($tokenRow['encrypted_token']) : null;
// Получаем все метрики которые есть у серве<D0B2><D0B5>а
// Получаем только метрики, которые можно показывать на странице сервера
$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 = :server_id
AND mn.name NOT LIKE '%_proc'
AND (
mn.name IN ('uptime', '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 mn.name
");
$stmt->execute([':server_id' => $id]);
@ -181,7 +189,8 @@ class ServerController extends Model
// Собираем выбранные метрики
$displayMetrics = $params['display_metrics'] ?? [];
$displayMetricsJson = json_encode(array_values($displayMetrics));
$displayMetrics = array_values(array_unique(array_filter($displayMetrics, fn ($metric) => is_string($metric) && $metric !== '')));
$displayMetricsJson = json_encode($displayMetrics);
$stmt = $this->pdo->prepare("
UPDATE servers

View File

@ -131,15 +131,17 @@ class ServerDetailController extends Model
$startStr = $startDate->format('Y-m-d H:i:s');
$endStr = $endDate->format('Y-m-d H:i:s');
// Формируем фильтр метрик из настроек сервера
// Формируем фильтр метрик из настроек сервера.
// Дополнительные метрики подгружаем автоматически, если они нужны для отображения.
$metricsFilter = '';
$metricParams = [];
if ($displayMetrics) {
$queryMetricNames = $displayMetrics ? $this->expandDisplayMetrics($displayMetrics) : [];
if ($queryMetricNames) {
$placeholders = [];
foreach ($displayMetrics as $i => $m) {
foreach (array_values($queryMetricNames) as $i => $metricName) {
$key = ':metric_' . $i;
$placeholders[] = $key;
$metricParams[$key] = $m;
$metricParams[$key] = $metricName;
}
$metricsFilter = 'AND mn.name IN (' . implode(', ', $placeholders) . ')';
}
@ -203,19 +205,6 @@ class ServerDetailController extends Model
AND 1=1 {$metricsFilter}
ORDER BY t.period_start ASC
";
// Конвертируем имена метрик в ID для filters
$metricIds = [];
if ($displayMetrics) {
$placeholders = [];
foreach ($displayMetrics as $i => $m) {
$key = ':metric_' . $i;
$placeholders[] = $key;
$metricParams[$key] = $m;
}
$metricsFilter = 'AND mn.name IN (' . implode(', ', $placeholders) . ')';
}
$stmt = $this->pdo->prepare($sql);
$executeParams = array_merge([':id' => $id, ':period_start' => $periodStartStr, ':period_end' => $periodEndStr], $metricParams);
$stmt->execute($executeParams);
@ -328,29 +317,26 @@ class ServerDetailController extends Model
$stmt->execute([':id' => $id]);
$latestUptime = $stmt->fetch();
$simpleMetricCharts = $this->buildSimpleMetricCharts($groupedMetrics, $displayMetrics);
$networkCharts = $this->buildNetworkCharts($groupedMetrics, $displayMetrics);
$temperatureChart = $this->buildTemperatureChart($groupedMetrics, $displayMetrics);
$diskCharts = $this->buildDiskCharts($groupedMetrics, $displayMetrics);
$templateData = [
'title' => 'Сервер: ' . $server['name'],
'server' => $server,
'metrics' => $groupedMetrics,
'displayMetrics' => $displayMetrics,
// Группировка метрик по категориям для отдельных секций
'diskMetrics' => array_filter($groupedMetrics, function($key) {
return str_starts_with($key, 'disk_used_');
}, ARRAY_FILTER_USE_KEY),
'tempMetrics' => array_filter($groupedMetrics, function($key) {
return str_starts_with($key, 'temp_');
}, ARRAY_FILTER_USE_KEY),
'netInMetrics' => array_filter($groupedMetrics, function($key) {
return str_starts_with($key, 'net_in_');
}, ARRAY_FILTER_USE_KEY),
'netOutMetrics' => array_filter($groupedMetrics, function($key) {
return str_starts_with($key, 'net_out_');
}, ARRAY_FILTER_USE_KEY),
'simpleMetricCharts' => $simpleMetricCharts,
'networkCharts' => $networkCharts,
'temperatureChart' => $temperatureChart,
'diskCharts' => $diskCharts,
'allMetricTypes' => $allMetricTypes,
'existingThresholds' => $existingThresholds,
'allServices' => $allServices,
'monitorServices' => $monitorServices,
'latestUptime' => $latestUptime,
'uptimeText' => $this->formatUptime($latestUptime['value'] ?? null),
'startDate' => $startDate->format('Y-m-d\TH:i'),
'endDate' => $endDate->format('Y-m-d\TH:i'),
'aggregation' => $aggConfig,
@ -402,6 +388,283 @@ class ServerDetailController extends Model
}
}
private function expandDisplayMetrics(array $displayMetrics): array
{
$expanded = [];
foreach ($displayMetrics as $metricName) {
if ($metricName === 'uptime') {
continue;
}
$expanded[$metricName] = $metricName;
if (str_starts_with($metricName, 'disk_used_')) {
$suffix = substr($metricName, strlen('disk_used_'));
$expanded['disk_total_gb_' . $suffix] = 'disk_total_gb_' . $suffix;
}
}
return array_values($expanded);
}
private function buildSimpleMetricCharts(array $groupedMetrics, ?array $displayMetrics): array
{
$charts = [];
$config = [
'cpu_load' => ['title' => 'Загрузка CPU', 'color' => '#0d6efd'],
'ram_used' => ['title' => 'Использование RAM', 'color' => '#198754'],
];
foreach ($config as $metricName => $meta) {
if (!$this->isMetricSelected($metricName, $displayMetrics) || empty($groupedMetrics[$metricName])) {
continue;
}
$charts[] = [
'id' => $metricName,
'title' => $meta['title'],
'unit' => $groupedMetrics[$metricName][0]['unit'] ?? '',
'color' => $meta['color'],
'labels' => $this->extractLabels($groupedMetrics[$metricName]),
'timestamps' => $this->extractTimestamps($groupedMetrics[$metricName]),
'values' => $this->extractValues($groupedMetrics[$metricName]),
'lastValue' => round((float)($groupedMetrics[$metricName][0]['value'] ?? 0), 2),
'lastTime' => $this->formatPointTime($groupedMetrics[$metricName][0] ?? []),
];
}
return $charts;
}
private function buildNetworkCharts(array $groupedMetrics, ?array $displayMetrics): array
{
$interfaces = [];
foreach (array_keys($groupedMetrics) as $metricName) {
if (str_starts_with($metricName, 'net_in_')) {
$interfaces[substr($metricName, strlen('net_in_'))] = true;
}
if (str_starts_with($metricName, 'net_out_')) {
$interfaces[substr($metricName, strlen('net_out_'))] = true;
}
}
$charts = [];
foreach (array_keys($interfaces) as $iface) {
$inMetric = 'net_in_' . $iface;
$outMetric = 'net_out_' . $iface;
$showIn = $this->isMetricSelected($inMetric, $displayMetrics) && !empty($groupedMetrics[$inMetric]);
$showOut = $this->isMetricSelected($outMetric, $displayMetrics) && !empty($groupedMetrics[$outMetric]);
if (!$showIn && !$showOut) {
continue;
}
$baseSeries = $showIn ? $groupedMetrics[$inMetric] : $groupedMetrics[$outMetric];
$datasets = [];
if ($showIn) {
$datasets[] = [
'label' => 'Входящий трафик',
'color' => '#198754',
'values' => $this->extractValues($groupedMetrics[$inMetric]),
];
}
if ($showOut) {
$datasets[] = [
'label' => 'Исходящий трафик',
'color' => '#dc3545',
'values' => $this->extractValues($groupedMetrics[$outMetric]),
];
}
$charts[] = [
'id' => $iface,
'title' => 'Сеть: ' . $iface,
'unit' => $baseSeries[0]['unit'] ?? '',
'labels' => $this->extractLabels($baseSeries),
'timestamps' => $this->extractTimestamps($baseSeries),
'datasets' => $datasets,
];
}
return $charts;
}
private function buildTemperatureChart(array $groupedMetrics, ?array $displayMetrics): array
{
$datasets = [];
$labels = [];
$colors = ['#dc3545', '#fd7e14', '#0dcaf0', '#6f42c1', '#20c997', '#ffc107', '#6610f2', '#198754'];
$colorIndex = 0;
foreach ($groupedMetrics as $metricName => $points) {
if (!str_starts_with($metricName, 'temp_') || !$this->isMetricSelected($metricName, $displayMetrics) || empty($points)) {
continue;
}
if (!$labels) {
$labels = $this->extractLabels($points);
}
$datasets[] = [
'label' => $this->formatMetricLabel($metricName),
'color' => $colors[$colorIndex % count($colors)],
'values' => $this->extractValues($points),
];
$colorIndex++;
}
return [
'unit' => '°C',
'labels' => $labels,
'timestamps' => $labels ? $this->extractTimestamps($points) : [],
'datasets' => $datasets,
];
}
private function buildDiskCharts(array $groupedMetrics, ?array $displayMetrics): array
{
$charts = [];
foreach ($groupedMetrics as $metricName => $points) {
if (
!str_starts_with($metricName, 'disk_used_')
|| $metricName === 'disk_used'
|| !$this->isMetricSelected($metricName, $displayMetrics)
|| empty($points)
) {
continue;
}
$suffix = substr($metricName, strlen('disk_used_'));
$totalMetric = 'disk_total_gb_' . $suffix;
$percent = (float)($points[0]['value'] ?? 0);
$totalGb = isset($groupedMetrics[$totalMetric][0]['value']) ? (float)$groupedMetrics[$totalMetric][0]['value'] : 0.0;
$usedGb = $totalGb > 0 ? round(($percent / 100) * $totalGb, 1) : null;
$freeGb = $totalGb > 0 ? round($totalGb - $usedGb, 1) : null;
$charts[] = [
'id' => $suffix,
'title' => $this->formatDiskTitle($suffix),
'percent' => round($percent, 1),
'totalGb' => $totalGb > 0 ? round($totalGb, 1) : null,
'usedGb' => $usedGb,
'freeGb' => $freeGb,
'updatedAt' => $this->formatPointTime($points[0]),
];
}
return $charts;
}
private function isMetricSelected(string $metricName, ?array $displayMetrics): bool
{
return is_array($displayMetrics) && in_array($metricName, $displayMetrics, true);
}
private function extractLabels(array $points): array
{
$labels = [];
foreach ($points as $point) {
$labels[] = $this->formatPointTime($point, 'd.m H:i');
}
return $labels;
}
private function extractValues(array $points): array
{
$values = [];
foreach ($points as $point) {
$values[] = round((float)($point['value'] ?? 0), 2);
}
return $values;
}
private function extractTimestamps(array $points): array
{
$timestamps = [];
foreach ($points as $point) {
$timestamps[] = $point['time_bucket'] ?? $point['created_at'] ?? null;
}
return $timestamps;
}
private function formatPointTime(array $point, string $format = 'd.m.Y H:i:s'): string
{
$raw = $point['time_bucket'] ?? $point['created_at'] ?? null;
if (!$raw) {
return '';
}
return (new DateTime($raw))->format($format);
}
private function formatMetricLabel(string $metricName): string
{
if ($metricName === 'cpu_load') {
return 'Загрузка CPU';
}
if ($metricName === 'ram_used') {
return 'Использование RAM';
}
if (str_starts_with($metricName, 'temp_')) {
return 'Температура ' . str_replace('_', ' ', substr($metricName, strlen('temp_')));
}
return str_replace('_', ' ', $metricName);
}
private function formatDiskTitle(string $suffix): string
{
return match ($suffix) {
'root' => '/ (корень)',
'home' => '/home',
'boot' => '/boot',
'mnt_data' => '/mnt/data',
default => '/' . str_replace('_', '/', $suffix),
};
}
private function formatUptime($value): ?string
{
if ($value === null || $value === '') {
return null;
}
$seconds = (int)round((float)$value);
if ($seconds < 0) {
return null;
}
$days = intdiv($seconds, 86400);
$seconds %= 86400;
$hours = intdiv($seconds, 3600);
$seconds %= 3600;
$minutes = intdiv($seconds, 60);
$parts = [];
if ($days > 0) {
$parts[] = $days . ' д';
}
if ($hours > 0 || $days > 0) {
$parts[] = $hours . ' ч';
}
$parts[] = $minutes . ' мин';
return implode(' ', $parts);
}
public function saveThresholds(Request $request, Response $response, $args)
{
$id = $args['id'];

View File

@ -1,198 +0,0 @@
<?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;
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);
}
// Получаем период для выборки метрик
$period = $request->getQueryParams()['period'] ?? '24h';
// Определяем интервал времени в зависимости от периода
$interval = match($period) {
'7d' => 'INTERVAL 7 DAY',
'30d' => 'INTERVAL 30 DAY',
default => 'INTERVAL 24 HOUR'
};
// Получаем последние метрики для этого сервера
$stmt = $this->pdo->prepare("
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 >= DATE_SUB(NOW(), {$interval})
ORDER BY sm.created_at DESC
");
$stmt->execute([':id' => $id]);
$metrics = $stmt->fetchAll();
// Группируем метрики по типу
$groupedMetrics = [];
foreach ($metrics as $metric) {
$name = $metric['name'];
if (!isset($groupedMetrics[$name])) {
$groupedMetrics[$name] = [];
}
$groupedMetrics[$name][] = $metric;
}
// Получаем все доступные типы метрик для настройки порогов
$stmt = $this->pdo->query("SELECT name, unit FROM metric_names ORDER BY name");
$allMetricTypes = $stmt->fetchAll();
// Получаем текущие пороговые значения для сервера
$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 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,
'period' => $period,
'request' => $request->getQueryParams()
'monitorServices' => $monitorServices
];
return $this->twig->render($response, 'servers/detail.twig', $templateData);
}
public function saveThresholds(Request $request, Response $response, $args)
{
$id = $args['id'];
$params = $request->getParsedBody();
// Получаем все типы метрик
$stmt = $this->pdo->query("SELECT id, name FROM metric_names ORDER BY name");
$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)
");
foreach ($metricTypes as $metricType) {
$warning = $params[$metricType['name'] . '_warning'] ?? null;
$critical = $params[$metricType['name'] . '_critical'] ?? null;
$duration = $params[$metricType['name'] . '_duration'] ?? 0;
// Сохраняем только если указан хотя бы один порог
if ($warning !== null || $critical !== null) {
$insertStmt->execute([
':server_id' => $id,
':metric_name_id' => $metricType['id'],
':warning_threshold' => $warning,
':critical_threshold' => $critical,
':duration' => (int)$duration
]);
}
}
// Возвращаемся на страницу сервера
return $response->withHeader('Location', "/servers/{$id}")->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);
}
}

1013
templates/servers/detail.twig Executable file → Normal file

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,7 @@
{% if allMetrics|length > 0 %}
<hr>
<h5 class="mb-3"><i class="fas fa-chart-line"></i> Отображаемые метрики</h5>
<p class="text-muted small">Выберите метрики, которые будут показываться на графиках. Пустой выбор = все метрики.</p>
<p class="text-muted small">Выберите метрики, которые будут показываться на странице сервера. Аптайм выводится текстом, диски показываются donut-графиками, температуры собираются в один общий график.</p>
<div class="row mb-3">
{% set savedMetrics = server_display_metrics ?: [] %}
@ -53,7 +53,23 @@
id="metric_{{ metric.name }}" value="{{ metric.name }}"
{% if metric.name in savedMetrics %}checked{% endif %}>
<label class="form-check-label" for="metric_{{ metric.name }}">
{% if metric.name == 'uptime' %}
Аптайм
{% elseif metric.name == 'cpu_load' %}
Загрузка CPU
{% elseif metric.name == 'ram_used' %}
Использование RAM
{% elseif metric.name starts with 'disk_used_' %}
Диск: {{ metric.name|replace({'disk_used_': '', '_': '/'}) }}
{% elseif metric.name starts with 'net_in_' %}
Сеть входящая: {{ metric.name|replace({'net_in_': ''}) }}
{% elseif metric.name starts with 'net_out_' %}
Сеть исходящая: {{ metric.name|replace({'net_out_': ''}) }}
{% elseif metric.name starts with 'temp_' %}
Температура: {{ metric.name|replace({'temp_': '', '_': ' '})|title }}
{% else %}
{{ metric.name }}
{% endif %}
{% if metric.unit %}({{ metric.unit }}){% endif %}
</label>
</div>