, load: array, iowait: array, memory: array, swap: array} */ public function history(int $points, int $intervalSeconds, string $labelFormat): array { if ($points <= 0 || $intervalSeconds <= 0) { return []; } $timezone = $this->systemTimezone(); $end = CarbonImmutable::now($timezone)->second(0); if ($intervalSeconds >= 3600) { $end = $end->minute(0); } if ($intervalSeconds >= 86400) { $end = $end->hour(0)->minute(0); } $start = $end->subSeconds(($points - 1) * $intervalSeconds); $samples = $this->readSamples($start, $end); if (empty($samples)) { return []; } return $this->resample($samples, $start, $points, $intervalSeconds, $labelFormat); } /** * @return array{load1: float, load5: float, load15: float, iowait: float, memory: float, swap: float}|null */ public function latest(): ?array { $timezone = $this->systemTimezone(); $end = CarbonImmutable::now($timezone); $start = $end->subMinutes(15); $samples = $this->readSamples($start, $end); if (empty($samples)) { return null; } $last = end($samples); if (! is_array($last)) { return null; } return [ 'load1' => (float) ($last['load1'] ?? 0), 'load5' => (float) ($last['load5'] ?? 0), 'load15' => (float) ($last['load15'] ?? 0), 'iowait' => (float) ($last['iowait'] ?? 0), 'memory' => (float) ($last['memory'] ?? 0), 'swap' => (float) ($last['swap'] ?? 0), ]; } public function timezoneName(): string { return $this->systemTimezone()->getName(); } /** * @return array */ private function readSamples(CarbonImmutable $start, CarbonImmutable $end): array { $samples = []; $current = $start->startOfDay(); $lastDay = $end->startOfDay(); while ($current <= $lastDay) { $file = sprintf('/var/log/sysstat/sa%s', $current->format('d')); if (! is_readable($file)) { $current = $current->addDay(); continue; } $dayStart = $current->isSameDay($start) ? $start : $current->startOfDay(); $dayEnd = $current->isSameDay($end) ? $end : $current->endOfDay(); $statistics = $this->readSadf($file, $dayStart, $dayEnd); foreach ($statistics as $stat) { $parsed = $this->parseSample($stat); if ($parsed === null) { continue; } $samples[] = $parsed; } $current = $current->addDay(); } usort($samples, static fn (array $a, array $b): int => $a['timestamp'] <=> $b['timestamp']); return $samples; } /** * @return array> */ private function readSadf(string $file, CarbonImmutable $start, CarbonImmutable $end): array { $process = new Process([ 'sadf', '-j', '-T', $file, '--', '-A', '-s', $start->format('H:i:s'), '-e', $end->format('H:i:s'), ]); $process->run(); if (! $process->isSuccessful()) { return []; } $payload = json_decode($process->getOutput(), true); if (! is_array($payload)) { return []; } $stats = $payload['sysstat']['hosts'][0]['statistics'] ?? []; if (! is_array($stats)) { return []; } return $stats; } /** * @param array $stat * @return array{timestamp: int, load1: float, load5: float, load15: float, iowait: float, memory: float, swap: float}|null */ private function parseSample(array $stat): ?array { $timestamp = $this->parseTimestamp($stat['timestamp'] ?? []); if ($timestamp === null) { return null; } $queue = $stat['queue'] ?? []; $load1 = $this->getFloat($queue, ['ldavg-1', 'ldavg_1']) ?? 0.0; $load5 = $this->getFloat($queue, ['ldavg-5', 'ldavg_5']) ?? 0.0; $load15 = $this->getFloat($queue, ['ldavg-15', 'ldavg_15']) ?? 0.0; $cpuLoad = $stat['cpu-load'] ?? $stat['cpu-load-all'] ?? []; $iowait = $this->extractCpuMetric($cpuLoad, 'iowait'); $memory = $stat['memory'] ?? []; $memPercent = $this->getFloat($memory, ['memused-percent', 'memused_percent', 'memused']); if ($memPercent === null) { $memPercent = $this->percentFromTotals($memory, 'kbmemused', 'kbmemfree', 'kbmemtotal'); } $swap = $stat['swap'] ?? $stat['memory'] ?? []; $swapPercent = $this->getFloat($swap, ['swpused-percent', 'swpused_percent', 'swpused']); if ($swapPercent === null) { $swapPercent = $this->percentFromTotals($swap, 'kbswpused', 'kbswpfree', 'kbswptotal'); } return [ 'timestamp' => $timestamp->getTimestamp(), 'load1' => (float) $load1, 'load5' => (float) $load5, 'load15' => (float) $load15, 'iowait' => (float) ($iowait ?? 0.0), 'memory' => (float) ($memPercent ?? 0.0), 'swap' => (float) ($swapPercent ?? 0.0), ]; } /** * @return array{labels: array, load: array, iowait: array, memory: array, swap: array} */ private function resample(array $samples, CarbonImmutable $start, int $points, int $intervalSeconds, string $labelFormat): array { $labels = []; $loadSeries = []; $ioWaitSeries = []; $memorySeries = []; $swapSeries = []; $index = 0; $current = null; $count = count($samples); for ($i = 0; $i < $points; $i++) { $bucketTime = $start->addSeconds($i * $intervalSeconds); while ($index < $count && $samples[$index]['timestamp'] <= $bucketTime->getTimestamp()) { $current = $samples[$index]; $index++; } $labels[] = $bucketTime->format($labelFormat); $loadSeries[] = $current ? round((float) $current['load1'], 3) : 0.0; $ioWaitSeries[] = $current ? round((float) $current['iowait'], 2) : 0.0; $memorySeries[] = $current ? round((float) $current['memory'], 1) : 0.0; $swapSeries[] = $current ? round((float) $current['swap'], 1) : 0.0; } return [ 'labels' => $labels, 'load' => $loadSeries, 'iowait' => $ioWaitSeries, 'memory' => $memorySeries, 'swap' => $swapSeries, ]; } /** * @param array $timestamp */ private function parseTimestamp(array $timestamp): ?CarbonImmutable { $date = $timestamp['date'] ?? null; $time = $timestamp['time'] ?? null; if (! is_string($date) || ! is_string($time)) { return null; } $value = CarbonImmutable::createFromFormat('Y-m-d H:i:s', $date.' '.$time, $this->systemTimezone()); if ($value === false) { return null; } return $value; } private function systemTimezone(): DateTimeZone { static $timezone = null; if ($timezone instanceof DateTimeZone) { return $timezone; } $name = getenv('TZ') ?: null; if (! $name && is_file('/etc/timezone')) { $name = trim((string) file_get_contents('/etc/timezone')); } if (! $name && is_link('/etc/localtime')) { $target = readlink('/etc/localtime'); if (is_string($target) && str_contains($target, '/zoneinfo/')) { $name = substr($target, strpos($target, '/zoneinfo/') + 10); } } if (! $name) { $name = config('app.timezone') ?: date_default_timezone_get(); } try { $timezone = new DateTimeZone($name); } catch (\Exception $e) { $timezone = new DateTimeZone('UTC'); } return $timezone; } /** * @param array $source */ private function getFloat(array $source, array $keys): ?float { foreach ($keys as $key) { if (array_key_exists($key, $source) && is_numeric($source[$key])) { return (float) $source[$key]; } } return null; } /** * @param array $source */ private function extractCpuMetric(mixed $source, string $metric): ?float { if (is_array($source) && array_key_exists($metric, $source) && is_numeric($source[$metric])) { return (float) $source[$metric]; } if (is_array($source)) { $entries = $source; if (array_values($entries) === $entries) { foreach ($entries as $entry) { if (! is_array($entry)) { continue; } $cpu = $entry['cpu'] ?? $entry['cpu-load'] ?? null; if ($cpu === 'all' || $cpu === 0 || $cpu === '0') { if (isset($entry[$metric]) && is_numeric($entry[$metric])) { return (float) $entry[$metric]; } } } foreach ($entries as $entry) { if (is_array($entry) && isset($entry[$metric]) && is_numeric($entry[$metric])) { return (float) $entry[$metric]; } } } } return null; } /** * @param array $source */ private function percentFromTotals(array $source, string $usedKey, string $freeKey, string $totalKey): ?float { $used = $source[$usedKey] ?? null; $free = $source[$freeKey] ?? null; $total = $source[$totalKey] ?? null; if (is_numeric($total) && (float) $total > 0) { $usedValue = is_numeric($used) ? (float) $used : null; if ($usedValue === null && is_numeric($free)) { $usedValue = (float) $total - (float) $free; } if ($usedValue !== null) { return round(($usedValue / (float) $total) * 100, 2); } } return null; } }