Add aggregation for metrics: auto/1h/6h/24h/7d/30d zoom levels
This commit is contained in:
parent
36c8856d38
commit
6073ec348a
|
|
@ -36,29 +36,47 @@ class ServerDetailController extends Model
|
||||||
return $response->withHeader('Location', '/servers')->withStatus(302);
|
return $response->withHeader('Location', '/servers')->withStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем период для выборки метрик
|
// Получаем период и зум
|
||||||
$period = $request->getQueryParams()['period'] ?? '24h';
|
$period = $request->getQueryParams()['period'] ?? '24h';
|
||||||
|
$zoom = $request->getQueryParams()['zoom'] ?? null;
|
||||||
|
|
||||||
// Определяем интервал времени в зависимости от периода
|
// Конфигурация агрегации
|
||||||
$interval = match($period) {
|
$aggConfig = $this->getAggregationConfig($period, $zoom);
|
||||||
'7d' => 'INTERVAL 7 DAY',
|
|
||||||
'30d' => 'INTERVAL 30 DAY',
|
$interval = $aggConfig['interval'];
|
||||||
default => 'INTERVAL 24 HOUR'
|
$groupBy = $aggConfig['groupBy'];
|
||||||
};
|
|
||||||
|
// Запрос с агрегацией если нужно
|
||||||
// Получаем последние метрики для этого сервера
|
if ($groupBy) {
|
||||||
$stmt = $this->pdo->prepare("
|
$sql = "
|
||||||
SELECT sm.value, mn.name, mn.unit, sm.created_at
|
SELECT
|
||||||
FROM server_metrics sm
|
AVG(sm.value) as value,
|
||||||
JOIN metric_names mn ON sm.metric_name_id = mn.id
|
mn.name,
|
||||||
WHERE sm.server_id = :id
|
mn.unit,
|
||||||
AND sm.created_at >= DATE_SUB(NOW(), {$interval})
|
DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:%i:00') as time_bucket
|
||||||
ORDER BY sm.created_at DESC
|
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]);
|
$stmt->execute([':id' => $id]);
|
||||||
$metrics = $stmt->fetchAll();
|
$metrics = $stmt->fetchAll();
|
||||||
|
|
||||||
// Группируем метрики по типу (CPU, RAM, Disk)
|
// Группируем метрики
|
||||||
$groupedMetrics = [];
|
$groupedMetrics = [];
|
||||||
foreach ($metrics as $metric) {
|
foreach ($metrics as $metric) {
|
||||||
$metricName = $metric['name'];
|
$metricName = $metric['name'];
|
||||||
|
|
@ -68,7 +86,7 @@ class ServerDetailController extends Model
|
||||||
$groupedMetrics[$metricName][] = $metric;
|
$groupedMetrics[$metricName][] = $metric;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем текущие пороговые значения для сервера
|
// Пороги
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
SELECT mt.warning_threshold, mt.critical_threshold, mt.duration, mn.name
|
SELECT mt.warning_threshold, mt.critical_threshold, mt.duration, mn.name
|
||||||
FROM metric_thresholds mt
|
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");
|
$stmt = $this->pdo->query("SELECT id, name, unit FROM metric_names WHERE name NOT LIKE '%\_proc' ORDER BY name");
|
||||||
$allMetricTypes = $stmt->fetchAll();
|
$allMetricTypes = $stmt->fetchAll();
|
||||||
|
|
||||||
// Получаем список сервисов
|
// Сервисы
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
SELECT service_name, status, load_state, active_state, sub_state
|
SELECT service_name, status, load_state, active_state, sub_state
|
||||||
FROM service_status
|
FROM service_status WHERE server_id = :server_id ORDER BY service_name
|
||||||
WHERE server_id = :server_id
|
|
||||||
ORDER BY service_name
|
|
||||||
");
|
");
|
||||||
$stmt->execute([':server_id' => $id]);
|
$stmt->execute([':server_id' => $id]);
|
||||||
$allServices = $stmt->fetchAll();
|
$allServices = $stmt->fetchAll();
|
||||||
|
|
||||||
// Получаем список сервисов для мониторинга из конфигурации агента
|
// Мониторинг сервисов
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("SELECT monitor_services FROM agent_configs WHERE server_id = :server_id");
|
||||||
SELECT monitor_services FROM agent_configs WHERE server_id = :server_id
|
|
||||||
");
|
|
||||||
$stmt->execute([':server_id' => $id]);
|
$stmt->execute([':server_id' => $id]);
|
||||||
$agentConfig = $stmt->fetch();
|
$agentConfig = $stmt->fetch();
|
||||||
|
|
||||||
|
|
@ -120,26 +134,45 @@ class ServerDetailController extends Model
|
||||||
'allServices' => $allServices,
|
'allServices' => $allServices,
|
||||||
'monitorServices' => $monitorServices,
|
'monitorServices' => $monitorServices,
|
||||||
'period' => $period,
|
'period' => $period,
|
||||||
|
'zoom' => $zoom,
|
||||||
|
'aggregation' => $aggConfig,
|
||||||
'request' => $request->getQueryParams()
|
'request' => $request->getQueryParams()
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->twig->render($response, 'servers/detail.twig', $templateData);
|
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)
|
public function saveThresholds(Request $request, Response $response, $args)
|
||||||
{
|
{
|
||||||
$id = $args['id'];
|
$id = $args['id'];
|
||||||
$params = $request->getParsedBody();
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
// Получаем все типы метрик
|
|
||||||
$stmt = $this->pdo->query("SELECT id, name FROM metric_names WHERE name NOT LIKE '%\_proc' ORDER BY name");
|
$stmt = $this->pdo->query("SELECT id, name FROM metric_names WHERE name NOT LIKE '%\_proc' ORDER BY name");
|
||||||
$metricTypes = $stmt->fetchAll();
|
$metricTypes = $stmt->fetchAll();
|
||||||
|
|
||||||
// Удаляем старые пороги для этого сервера
|
|
||||||
$stmt = $this->pdo->prepare("DELETE FROM metric_thresholds WHERE server_id = :server_id");
|
$stmt = $this->pdo->prepare("DELETE FROM metric_thresholds WHERE server_id = :server_id");
|
||||||
$stmt->execute([':server_id' => $id]);
|
$stmt->execute([':server_id' => $id]);
|
||||||
|
|
||||||
// Добавляем новые пороги
|
|
||||||
$insertStmt = $this->pdo->prepare("
|
$insertStmt = $this->pdo->prepare("
|
||||||
INSERT INTO metric_thresholds (server_id, metric_name_id, warning_threshold, critical_threshold, duration)
|
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)
|
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);
|
return $response->withHeader('Location', "/servers/{$id}")->withStatus(302);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,29 +201,20 @@ class ServerDetailController extends Model
|
||||||
{
|
{
|
||||||
$id = $args['id'];
|
$id = $args['id'];
|
||||||
$params = $request->getParsedBody();
|
$params = $request->getParsedBody();
|
||||||
|
|
||||||
// Получаем список сервисов для мониторинга
|
|
||||||
$services = $params['services'] ?? [];
|
$services = $params['services'] ?? [];
|
||||||
|
|
||||||
if (is_string($services)) {
|
if (is_string($services)) {
|
||||||
$services = json_decode($services, true) ?? [];
|
$services = json_decode($services, true) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем конфигурацию агента
|
|
||||||
$stmt = $this->pdo->prepare("
|
$stmt = $this->pdo->prepare("
|
||||||
INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled)
|
INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled)
|
||||||
VALUES (:server_id, 60, :services, TRUE)
|
VALUES (:server_id, 60, :services, TRUE)
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE monitor_services = VALUES(monitor_services), updated_at = CURRENT_TIMESTAMP
|
||||||
monitor_services = VALUES(monitor_services),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
");
|
");
|
||||||
|
|
||||||
$stmt->execute([
|
$stmt->execute([':server_id' => $id, ':services' => json_encode($services)]);
|
||||||
':server_id' => $id,
|
|
||||||
':services' => json_encode($services)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Возвращаемся на страницу сервера
|
|
||||||
return $response->withHeader('Location', "/servers/{$id}?tab=services")->withStatus(302);
|
return $response->withHeader('Location', "/servers/{$id}?tab=services")->withStatus(302);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,31 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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 %}">
|
||||||
|
1ч
|
||||||
|
</a>
|
||||||
|
<a href="?tab=metrics&period={{ period }}&zoom=6h" class="btn btn-sm {% if zoom == '6h' %}btn-secondary{% else %}btn-outline-secondary{% endif %}">
|
||||||
|
6ч
|
||||||
|
</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 %}">
|
||||||
|
7д
|
||||||
|
</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">
|
<div class="row">
|
||||||
{% for metricName, metricData in metrics %}
|
{% for metricName, metricData in metrics %}
|
||||||
|
|
@ -473,8 +498,10 @@ const ctx{{ metricName|replace({'-': '_', '.': '_'}) }} = document.getElementByI
|
||||||
var labels{{ metricName }} = [];
|
var labels{{ metricName }} = [];
|
||||||
var data{{ metricName }} = [];
|
var data{{ metricName }} = [];
|
||||||
|
|
||||||
{% for metric in metricData|slice(0, 1000)|reverse %}
|
{% for metric in metricData|slice(0, 50000)|reverse %}
|
||||||
labels{{ metricName }}.push('{{ metric.created_at|date('H:i') }}');
|
{% 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 }});
|
data{{ metricName }}.push({{ metric.value|raw }});
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue