feat(metrics): replace period/zoom buttons with datetime range selector
This commit is contained in:
parent
6ca570ec1d
commit
9b64cee32c
|
|
@ -36,21 +36,43 @@ class ServerDetailController extends Model
|
|||
return $response->withHeader('Location', '/servers')->withStatus(302);
|
||||
}
|
||||
|
||||
// Получаем период и зум
|
||||
$period = $request->getQueryParams()['period'] ?? '24h';
|
||||
$zoom = $request->getQueryParams()['zoom'] ?? null;
|
||||
// Получаем даты начала и окончания
|
||||
$queryParams = $request->getQueryParams();
|
||||
$startDate = $queryParams['start'] ?? null;
|
||||
$endDate = $queryParams['end'] ?? null;
|
||||
|
||||
// Конфигурация агрегации
|
||||
$aggConfig = $this->getAggregationConfig($period, $zoom);
|
||||
// Если даты не указаны, используем последние 24 часа по умолчанию
|
||||
if (!$startDate || !$endDate) {
|
||||
$endDate = new DateTime();
|
||||
$startDate = clone $endDate;
|
||||
$startDate->modify('-24 hours');
|
||||
} else {
|
||||
$startDate = new DateTime($startDate);
|
||||
$endDate = new DateTime($endDate);
|
||||
}
|
||||
|
||||
// Валидация: 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);
|
||||
|
||||
$interval = $aggConfig['interval'];
|
||||
$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) {
|
||||
// Используем format из конфига
|
||||
$bucketFormat = $aggConfig['format'] ?? '%Y-%m-%d %H:%i';
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
AVG(sm.value) as value,
|
||||
|
|
@ -60,23 +82,27 @@ class ServerDetailController extends Model
|
|||
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})
|
||||
AND sm.created_at >= :start_date
|
||||
AND sm.created_at <= :end_date
|
||||
{$groupBy}
|
||||
ORDER BY time_bucket DESC
|
||||
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 >= DATE_SUB(NOW(), {$interval})
|
||||
ORDER BY sm.created_at DESC
|
||||
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]);
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->execute([':id' => $id]);
|
||||
$metrics = $stmt->fetchAll();
|
||||
|
||||
// Группируем метрики
|
||||
|
|
@ -89,6 +115,25 @@ class ServerDetailController extends Model
|
|||
$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
|
||||
|
|
@ -136,46 +181,53 @@ class ServerDetailController extends Model
|
|||
'existingThresholds' => $existingThresholds,
|
||||
'allServices' => $allServices,
|
||||
'monitorServices' => $monitorServices,
|
||||
'period' => $period,
|
||||
'zoom' => $zoom,
|
||||
'startDate' => $startDate->format('Y-m-d\T H:i'),
|
||||
'endDate' => $endDate->format('Y-m-d\T H:i'),
|
||||
'aggregation' => $aggConfig,
|
||||
'request' => $request->getQueryParams()
|
||||
'totalMinutes' => $totalMinutes
|
||||
];
|
||||
|
||||
return $this->twig->render($response, 'servers/detail.twig', $templateData);
|
||||
}
|
||||
|
||||
private function getAggregationConfig(string $period, ?string $zoom): array
|
||||
private function getAggregationConfigFromDates(DateTime $startDate, DateTime $endDate, int $totalMinutes): array
|
||||
{
|
||||
// Target: ~360 points on chart for any period/zoom
|
||||
// Formula: aggregate_minutes = period_minutes / 360
|
||||
// Target: ~400 points on chart for optimal performance
|
||||
// Formula: aggregate_minutes = total_minutes / 400
|
||||
|
||||
if ($zoom) {
|
||||
return match($zoom) {
|
||||
// 1h = 60 min / 360 = 0.17 → no aggregation (~360 points)
|
||||
'1h' => ['interval' => 'INTERVAL 1 HOUR', 'groupBy' => null, 'aggregate_minutes' => 0, 'format' => '%Y-%m-%d %H:%i:%s'],
|
||||
// 6h = 360 min / 360 = 1 min aggregation (~360 points)
|
||||
'6h' => ['interval' => 'INTERVAL 6 HOUR', 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:%i')", 'aggregate_minutes' => 1, 'format' => '%Y-%m-%d %H:%i'],
|
||||
// 24h = 1440 min / 360 = 4 min aggregation (~360 points)
|
||||
'24h' => ['interval' => 'INTERVAL 24 HOUR', 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:%i')", 'aggregate_minutes' => 4, 'format' => '%Y-%m-%d %H:%i'],
|
||||
// 7d = 10080 min / 360 = 28 min aggregation (~360 points)
|
||||
'7d' => ['interval' => 'INTERVAL 7 DAY', 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:%i')", 'aggregate_minutes' => 28, 'format' => '%Y-%m-%d %H:%i'],
|
||||
// 30d = 43200 min / 360 = 120 min (2 hours) aggregation (~360 points)
|
||||
'30d' => ['interval' => 'INTERVAL 30 DAY', 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:00')", 'aggregate_minutes' => 120, 'format' => '%Y-%m-%d %H:00'],
|
||||
default => ['interval' => 'INTERVAL 24 HOUR', 'groupBy' => null, 'aggregate_minutes' => 0, 'format' => '%Y-%m-%d %H:%i:%s']
|
||||
};
|
||||
$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
|
||||
];
|
||||
}
|
||||
|
||||
// Default: base period aggregation (~360 points)
|
||||
return match($period) {
|
||||
// 24h = 1440 min / 360 = 4 min
|
||||
'24h' => ['interval' => 'INTERVAL 24 HOUR', 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:%i')", 'aggregate_minutes' => 4, 'format' => '%Y-%m-%d %H:%i'],
|
||||
// 7d = 10080 min / 360 = 28 min
|
||||
'7d' => ['interval' => 'INTERVAL 7 DAY', 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:%i')", 'aggregate_minutes' => 28, 'format' => '%Y-%m-%d %H:%i'],
|
||||
// 30d = 43200 min / 360 = 120 min
|
||||
'30d' => ['interval' => 'INTERVAL 30 DAY', 'groupBy' => "GROUP BY mn.id, DATE_FORMAT(sm.created_at, '%Y-%m-%d %H:00')", 'aggregate_minutes' => 120, 'format' => '%Y-%m-%d %H:00'],
|
||||
default => ['interval' => 'INTERVAL 24 HOUR', 'groupBy' => null, 'aggregate_minutes' => 0, 'format' => '%Y-%m-%d %H:%i:%s']
|
||||
};
|
||||
}
|
||||
|
||||
public function saveThresholds(Request $request, Response $response, $args)
|
||||
|
|
|
|||
|
|
@ -90,44 +90,50 @@
|
|||
<div class="tab-pane fade show active" id="metrics" role="tabpanel">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<!-- Отладка: period = {{ request.query.period }} -->
|
||||
<!-- Отладка: period = {{ period }}, request = {{ request.period }} -->
|
||||
<div class="btn-group d-flex" role="group">
|
||||
<a href="?tab=metrics&period=24h" class="btn btn-outline-primary w-100 {% if period == '24h' or period is empty %}active{% endif %}">
|
||||
24 часа
|
||||
</a>
|
||||
<a href="?tab=metrics&period=7d" class="btn btn-outline-primary w-100 {% if period == '7d' %}active{% endif %}">
|
||||
7 дней
|
||||
</a>
|
||||
<a href="?tab=metrics&period=30d" class="btn btn-outline-primary w-100 {% if period == '30d' %}active{% endif %}">
|
||||
30 дней
|
||||
</a>
|
||||
</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>
|
||||
<form method="get" class="row g-2 align-items-end" id="periodForm">
|
||||
<input type="hidden" name="tab" value="metrics">
|
||||
|
||||
<!-- Пресеты -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">
|
||||
<i class="fas fa-clock"></i> Быстрый выбор
|
||||
</label>
|
||||
<select class="form-select" id="presetSelect" onchange="applyPreset()">
|
||||
<option value="">-- Выбрать --</option>
|
||||
<option value="30">Последние 30 минут</option>
|
||||
<option value="60">Последние 1 час</option>
|
||||
<option value="120">Последние 2 часа</option>
|
||||
<option value="360">Последние 6 часов</option>
|
||||
<option value="720">Последние 12 часов</option>
|
||||
<option value="1440">Последние 24 часа</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Дата начала -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">
|
||||
<i class="fas fa-calendar"></i> С
|
||||
</label>
|
||||
<input type="datetime-local" class="form-control" name="start" id="startDate"
|
||||
value="{{ startDate }}" required>
|
||||
</div>
|
||||
|
||||
<!-- Дата окончания -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label mb-1">
|
||||
<i class="fas fa-calendar"></i> По
|
||||
</label>
|
||||
<input type="datetime-local" class="form-control" name="end" id="endDate"
|
||||
value="{{ endDate }}" required>
|
||||
</div>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<div class="col-md-3">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-search"></i> Применить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -634,4 +640,37 @@ if (canvas{{ metricName|replace({'-': '_', '.': '_'}) }}) {
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
</script>
|
||||
<script>
|
||||
// Применение пресета
|
||||
function applyPreset() {
|
||||
var select = document.getElementById("presetSelect");
|
||||
var minutes = parseInt(select.value);
|
||||
if (!minutes) return;
|
||||
|
||||
var now = new Date();
|
||||
var startDate = new Date(now.getTime() - (minutes * 60 * 1000));
|
||||
|
||||
// Форматируем для datetime-local (YYYY-MM-DDTHH:MM)
|
||||
function formatDateTimeLocal(date) {
|
||||
var year = date.getFullYear();
|
||||
var month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
var day = String(date.getDate()).padStart(2, "0");
|
||||
var hours = String(date.getHours()).padStart(2, "0");
|
||||
var mins = String(date.getMinutes()).padStart(2, "0");
|
||||
return year + "-" + month + "-" + day + "T" + hours + ":" + mins;
|
||||
}
|
||||
|
||||
document.getElementById("startDate").value = formatDateTimeLocal(startDate);
|
||||
document.getElementById("endDate").value = formatDateTimeLocal(now);
|
||||
}
|
||||
|
||||
// Автоотправка формы при изменении дат (опционально)
|
||||
// document.getElementById("startDate").addEventListener("change", function() {
|
||||
// document.getElementById("periodForm").submit();
|
||||
// });
|
||||
// document.getElementById("endDate").addEventListener("change", function() {
|
||||
// document.getElementById("periodForm").submit();
|
||||
// });
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Reference in New Issue