Add aggregation for metrics: auto/1h/6h/24h/7d/30d zoom levels

This commit is contained in:
mirivlad 2026-02-15 09:49:16 +00:00
parent 36c8856d38
commit 6073ec348a
2 changed files with 95 additions and 45 deletions

View File

@ -36,29 +36,47 @@ class ServerDetailController extends Model
return $response->withHeader('Location', '/servers')->withStatus(302);
}
// Получаем период для выборки метрик
// Получаем период и зум
$period = $request->getQueryParams()['period'] ?? '24h';
$zoom = $request->getQueryParams()['zoom'] ?? null;
// Определяем интервал времени в зависимости от периода
$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
");
// Конфигурация агрегации
$aggConfig = $this->getAggregationConfig($period, $zoom);
$interval = $aggConfig['interval'];
$groupBy = $aggConfig['groupBy'];
// Запрос с агрегацией если нужно
if ($groupBy) {
$sql = "
SELECT
AVG(sm.value) as value,
mn.name,
mn.unit,
DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:%i:00') 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 >= DATE_SUB(NOW(), {$interval})
{$groupBy}
ORDER BY time_bucket DESC
";
} 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 >= DATE_SUB(NOW(), {$interval})
ORDER BY sm.created_at DESC
";
}
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
$metrics = $stmt->fetchAll();
// Группируем метрики по типу (CPU, RAM, Disk)
// Группируем метрики
$groupedMetrics = [];
foreach ($metrics as $metric) {
$metricName = $metric['name'];
@ -68,7 +86,7 @@ class ServerDetailController extends Model
$groupedMetrics[$metricName][] = $metric;
}
// Получаем текущие пороговые значения для сервера
// Пороги
$stmt = $this->pdo->prepare("
SELECT mt.warning_threshold, mt.critical_threshold, mt.duration, mn.name
FROM metric_thresholds mt
@ -85,24 +103,20 @@ class ServerDetailController extends Model
];
}
// Получаем все типы метрик
// Типы метрик
$stmt = $this->pdo->query("SELECT id, name, unit FROM metric_names WHERE name NOT LIKE '%\_proc' ORDER BY name");
$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
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 = $this->pdo->prepare("SELECT monitor_services FROM agent_configs WHERE server_id = :server_id");
$stmt->execute([':server_id' => $id]);
$agentConfig = $stmt->fetch();
@ -120,26 +134,45 @@ class ServerDetailController extends Model
'allServices' => $allServices,
'monitorServices' => $monitorServices,
'period' => $period,
'zoom' => $zoom,
'aggregation' => $aggConfig,
'request' => $request->getQueryParams()
];
return $this->twig->render($response, 'servers/detail.twig', $templateData);
}
private function getAggregationConfig(string $period, ?string $zoom): array
{
if ($zoom) {
return match($zoom) {
'1h' => ['interval' => 'INTERVAL 1 HOUR', 'groupBy' => null],
'6h' => ['interval' => 'INTERVAL 6 HOUR', 'groupBy' => null],
'24h' => ['interval' => 'INTERVAL 24 HOUR', 'groupBy' => null],
'7d' => ['interval' => 'INTERVAL 7 DAY', 'groupBy' => "GROUP BY mn.id, time_bucket"],
'30d' => ['interval' => 'INTERVAL 30 DAY', 'groupBy' => "GROUP BY mn.id, time_bucket"],
default => ['interval' => 'INTERVAL 24 HOUR', 'groupBy' => null]
};
}
return match($period) {
'7d' => ['interval' => 'INTERVAL 7 DAY', 'groupBy' => "GROUP BY mn.id, time_bucket"],
'30d' => ['interval' => 'INTERVAL 30 DAY', 'groupBy' => "GROUP BY mn.id, time_bucket"],
default => ['interval' => 'INTERVAL 24 HOUR', 'groupBy' => null]
};
}
public function saveThresholds(Request $request, Response $response, $args)
{
$id = $args['id'];
$params = $request->getParsedBody();
// Получаем все типы метрик
$stmt = $this->pdo->query("SELECT id, name FROM metric_names WHERE name NOT LIKE '%\_proc' 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)
@ -161,7 +194,6 @@ class ServerDetailController extends Model
}
}
// Возвращаемся на страницу сервера
return $response->withHeader('Location', "/servers/{$id}")->withStatus(302);
}
@ -169,29 +201,20 @@ class ServerDetailController extends Model
{
$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
ON DUPLICATE KEY UPDATE monitor_services = VALUES(monitor_services), updated_at = CURRENT_TIMESTAMP
");
$stmt->execute([
':server_id' => $id,
':services' => json_encode($services)
]);
$stmt->execute([':server_id' => $id, ':services' => json_encode($services)]);
// Возвращаемся на страницу сервера
return $response->withHeader('Location', "/servers/{$id}?tab=services")->withStatus(302);
}
}

View File

@ -105,6 +105,31 @@
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-md-12">
<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 %}">
</a>
<a href="?tab=metrics&period={{ period }}&zoom=6h" class="btn btn-sm {% if zoom == '6h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
</a>
<a href="?tab=metrics&period={{ period }}&zoom=24h" class="btn btn-sm {% if zoom == '24h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
24ч
</a>
<a href="?tab=metrics&period={{ period }}&zoom=7d" class="btn btn-sm {% if zoom == '7d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
</a>
<a href="?tab=metrics&period={{ period }}&zoom=30d" class="btn btn-sm {% if zoom == '30d' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
30д
</a>
</div>
</div>
</div>
<div class="row">
{% for metricName, metricData in metrics %}
@ -473,8 +498,10 @@ const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementByI
var labels{{ metricName }} = [];
var data{{ metricName }} = [];
{% for metric in metricData|slice(0, 1000)|reverse %}
labels{{ metricName }}.push('{{ metric.created_at|date('H:i') }}');
{% for metric in metricData|slice(0, 50000)|reverse %}
{% set time_val = metric.time_bucket|default(metric.created_at) %}
{% set time_format = metric.time_bucket ? 'd.m H:i' : 'H:i' %}
labels{{ metricName }}.push('{{ time_val|date(time_format) }}');
data{{ metricName }}.push({{ metric.value|raw }});
{% endfor %}