*/ public array $diskSnapshot = []; public function mount(): void { $this->diskSnapshot = $this->fetchDiskSnapshot(); } public function refreshData(): void { $this->refreshKey++; $data = $this->getData(); $this->storeTenSecondMetrics( (float) ($data['load']['1min'] ?? 0), (float) ($data['cpu']['iowait'] ?? 0), (float) ($data['memory']['usage'] ?? 0), (float) ($data['memory']['swap_usage'] ?? 0), [], ); $config = $this->rangeConfig($this->range); $label = now()->format($config['label_format']); $history = $this->getHistory( (float) ($data['load']['1min'] ?? 0), (float) ($data['cpu']['iowait'] ?? 0), (float) ($data['memory']['usage'] ?? 0), (float) ($data['memory']['swap_usage'] ?? 0), $data['disk']['partitions'] ?? [], ); $this->dispatch('server-charts-updated', [ 'cpu' => $data['cpu']['usage'] ?? 0, 'load' => $data['load']['1min'] ?? 0, 'iowait' => $data['cpu']['iowait'] ?? 0, 'memory' => $data['memory']['usage'] ?? 0, 'swap' => $data['memory']['swap_usage'] ?? 0, 'label' => $label, 'history' => $history, 'history_points' => $config['points'], 'label_format' => $config['label_format'], 'interval_seconds' => $config['interval_seconds'], ]); } public function getData(): array { try { $agent = new AgentClient; $overview = $agent->metricsOverview(); $disk = $this->diskSnapshot; if (empty($disk)) { $disk = $this->fetchDiskSnapshot($agent); $this->diskSnapshot = $disk; } $cpu = $overview['cpu'] ?? []; $memory = $overview['memory'] ?? []; $swap = $memory['swap'] ?? []; $memUsage = $memory['usage_percent'] ?? $memory['usage'] ?? 0; if ($memUsage == 0 && ($memory['total'] ?? 0) > 0) { $memUsage = (($memory['used'] ?? 0) / $memory['total']) * 100; } $history = $this->getHistory( (float) ($overview['load']['1min'] ?? 0), (float) ($cpu['iowait'] ?? 0), (float) $memUsage, (float) ($swap['usage_percent'] ?? 0), $disk['partitions'] ?? [], ); return [ 'cpu' => [ 'usage' => round($cpu['usage'] ?? 0, 1), 'iowait' => round($cpu['iowait'] ?? 0, 1), 'cores' => $cpu['cores'] ?? 0, 'model' => $cpu['model'] ?? 'Unknown', ], 'memory' => [ 'usage' => round($memUsage, 1), 'used' => $memory['used'] ?? 0, 'total' => $memory['total'] ?? 0, 'free' => $memory['free'] ?? 0, 'cached' => $memory['cached'] ?? 0, 'swap_used' => $swap['used'] ?? 0, 'swap_total' => $swap['total'] ?? 0, 'swap_usage' => round((float) ($swap['usage_percent'] ?? 0), 1), ], 'disk' => [ 'partitions' => $disk['partitions'] ?? [], ], 'load' => $overview['load'] ?? [], 'uptime' => $overview['uptime']['human'] ?? 'N/A', 'history' => $history, 'source' => 'agent', ]; } catch (\Exception $e) { $fallback = $this->getLocalMetrics(); if ($fallback !== null) { if (empty($this->diskSnapshot)) { $this->diskSnapshot = $fallback['disk'] ?? []; } $history = $this->getHistory( (float) ($fallback['load']['1min'] ?? 0), (float) ($fallback['cpu']['iowait'] ?? 0), (float) ($fallback['memory']['usage'] ?? 0), (float) ($fallback['memory']['swap_usage'] ?? 0), $fallback['disk']['partitions'] ?? [], ); return [ 'cpu' => [ 'usage' => round($fallback['cpu']['usage'] ?? 0, 1), 'iowait' => round($fallback['cpu']['iowait'] ?? 0, 1), 'cores' => $fallback['cpu']['cores'] ?? 0, 'model' => $fallback['cpu']['model'] ?? 'Local', ], 'memory' => $fallback['memory'], 'disk' => $fallback['disk'], 'load' => $fallback['load'], 'uptime' => $fallback['uptime'] ?? 'N/A', 'history' => $history, 'source' => 'local', 'warning' => $e->getMessage(), ]; } return [ 'error' => $e->getMessage(), 'cpu' => ['usage' => 0, 'iowait' => 0, 'cores' => 0, 'model' => 'Error'], 'memory' => ['usage' => 0, 'used' => 0, 'total' => 0, 'free' => 0, 'cached' => 0], 'disk' => $this->diskSnapshot, 'load' => [], 'uptime' => 'N/A', 'history' => $this->getHistory(0.0, 0.0, 0.0, 0.0, []), 'source' => 'error', ]; } } public function setRange(string $range): void { $this->range = $this->normalizeRange($range); $config = $this->rangeConfig($this->range); $history = $this->getHistory(); $this->dispatch('server-charts-range-changed', [ 'history' => $history, 'history_points' => $config['points'], 'label_format' => $config['label_format'], 'interval_seconds' => $config['interval_seconds'], 'range' => $this->range, ]); } public function getHistory( float $load = 0.0, float $iowait = 0.0, float $memory = 0.0, float $swap = 0.0, array $diskPartitions = [] ): array { if (! Schema::hasTable('server_metrics')) { $config = $this->rangeConfig($this->range); return $this->seedHistory( $config['points'], $config['label_format'], $config['interval_seconds'], $load, $iowait, $memory, $swap, $diskPartitions, ); } $config = $this->rangeConfig($this->range); $since = now()->subMinutes($config['minutes']); $records = ServerMetric::query() ->where('resolution', $config['resolution']) ->where('captured_at', '>=', $since) ->orderBy('captured_at') ->get(); if ($records->isEmpty()) { return $this->seedHistory( $config['points'], $config['label_format'], $config['interval_seconds'], $load, $iowait, $memory, $swap, $diskPartitions, ); } $grouped = $records->groupBy(fn (ServerMetric $metric) => $metric->captured_at->format('Y-m-d H:i:s')); $diskLabels = $records ->where('metric', 'disk_gb') ->pluck('label') ->filter() ->unique() ->values() ->all(); $labels = []; $loadSeries = []; $ioWaitSeries = []; $memorySeries = []; $swapSeries = []; $diskSeries = []; foreach ($diskLabels as $label) { $diskSeries[$label] = []; } foreach ($grouped as $timestamp => $rows) { $labels[] = Carbon::createFromFormat('Y-m-d H:i:s', $timestamp)->format($config['label_format']); $loadSeries[] = (float) ($rows->firstWhere('metric', 'load')->value ?? 0); $ioWaitSeries[] = (float) ($rows->firstWhere('metric', 'iowait')->value ?? 0); $memorySeries[] = (float) ($rows->firstWhere('metric', 'memory')->value ?? 0); $swapSeries[] = (float) ($rows->firstWhere('metric', 'swap')->value ?? 0); foreach ($diskLabels as $label) { $diskSeries[$label][] = (float) ($rows->firstWhere(fn (ServerMetric $metric) => $metric->metric === 'disk_gb' && $metric->label === $label)->value ?? 0); } } if (count($labels) < $config['points']) { $seed = $this->seedHistory( $config['points'], $config['label_format'], $config['interval_seconds'], $load, $iowait, $memory, $swap, $diskPartitions, ); $missing = $config['points'] - count($labels); $labels = array_merge(array_slice($seed['labels'], 0, $missing), $labels); $loadSeries = array_merge(array_fill(0, $missing, null), $loadSeries); $ioWaitSeries = array_merge(array_fill(0, $missing, null), $ioWaitSeries); $memorySeries = array_merge(array_fill(0, $missing, null), $memorySeries); $swapSeries = array_merge(array_fill(0, $missing, null), $swapSeries); $seedDisk = $seed['disk'] ?? []; foreach ($diskSeries as $label => $series) { $prefix = array_fill(0, $missing, null); $diskSeries[$label] = array_merge($prefix, $series); } } if (count($labels) > $config['points']) { $labels = array_slice($labels, -$config['points']); $loadSeries = array_slice($loadSeries, -$config['points']); $ioWaitSeries = array_slice($ioWaitSeries, -$config['points']); $memorySeries = array_slice($memorySeries, -$config['points']); $swapSeries = array_slice($swapSeries, -$config['points']); foreach ($diskSeries as $label => $series) { $diskSeries[$label] = array_slice($series, -$config['points']); } } return [ 'labels' => $labels, 'load' => $loadSeries, 'iowait' => $ioWaitSeries, 'memory' => $memorySeries, 'swap' => $swapSeries, 'disk' => $diskSeries, ]; } private function rangeConfig(string $range): array { return match ($range) { '5m' => ['minutes' => 5, 'points' => 30, 'resolution' => '10s', 'label_format' => 'H:i:s', 'interval_seconds' => 10], '30m' => ['minutes' => 30, 'points' => 180, 'resolution' => '10s', 'label_format' => 'H:i:s', 'interval_seconds' => 10], 'day' => ['minutes' => 1440, 'points' => 24, 'resolution' => '1h', 'label_format' => 'H:00', 'interval_seconds' => 3600], 'week' => ['minutes' => 10080, 'points' => 28, 'resolution' => '6h', 'label_format' => 'M d H:00', 'interval_seconds' => 21600], 'month' => ['minutes' => 43200, 'points' => 30, 'resolution' => '1d', 'label_format' => 'M d', 'interval_seconds' => 86400], default => ['minutes' => 30, 'points' => 180, 'resolution' => '10s', 'label_format' => 'H:i:s', 'interval_seconds' => 10], }; } private function normalizeRange(string $range): string { return in_array($range, ['5m', '30m', 'day', 'week', 'month'], true) ? $range : '30m'; } private function seedHistory( int $points, string $labelFormat, int $intervalSeconds, float $load, float $iowait, float $memory, float $swap, array $diskPartitions ): array { $labels = []; $loadSeries = []; $ioWaitSeries = []; $memorySeries = []; $swapSeries = []; $diskSeries = []; $diskSeeds = []; foreach ($diskPartitions as $partition) { $mount = $partition['mount'] ?? null; $usedGb = $this->bytesToGb((int) ($partition['used'] ?? 0)); if ($mount !== null) { $diskSeeds[$mount] = $usedGb; } } foreach ($diskSeeds as $mount => $value) { $diskSeries[$mount] = []; } $now = now(); for ($i = $points - 1; $i >= 0; $i--) { $labels[] = $now->copy()->subSeconds($i * $intervalSeconds)->format($labelFormat); $loadSeries[] = round($load, 3); $ioWaitSeries[] = round($iowait, 2); $memorySeries[] = round($memory, 1); $swapSeries[] = round($swap, 1); foreach ($diskSeeds as $mount => $value) { $diskSeries[$mount][] = round($value, 2); } } return [ 'labels' => $labels, 'load' => $loadSeries, 'iowait' => $ioWaitSeries, 'memory' => $memorySeries, 'swap' => $swapSeries, 'disk' => $diskSeries, ]; } private function storeTenSecondMetrics(float $load, float $iowait, float $memory, float $swap, array $partitions): void { if (! Schema::hasTable('server_metrics')) { return; } $capturedAt = now()->setSecond(intdiv(now()->second, self::HISTORY_INTERVAL_SECONDS) * self::HISTORY_INTERVAL_SECONDS); $this->storeMetric('load', '10s', $load, $capturedAt); $this->storeMetric('iowait', '10s', $iowait, $capturedAt); $this->storeMetric('memory', '10s', $memory, $capturedAt); $this->storeMetric('swap', '10s', $swap, $capturedAt); foreach ($partitions as $partition) { $mount = $partition['mount'] ?? null; if ($mount === null) { continue; } $used = (int) ($partition['used'] ?? 0); $this->storeMetric('disk_gb', '10s', $this->bytesToGb($used), $capturedAt, $mount); } $this->pruneResolution('10s', now()->subMinutes(30)); $this->storeAggregateMetric('load', $load); $this->storeAggregateMetric('iowait', $iowait); $this->storeAggregateMetric('memory', $memory); $this->storeAggregateMetric('swap', $swap); } private function storeMetric(string $metric, string $resolution, float $value, Carbon $capturedAt, ?string $label = null): void { ServerMetric::updateOrCreate( [ 'metric' => $metric, 'label' => $label, 'resolution' => $resolution, 'captured_at' => $capturedAt, ], [ 'value' => round($value, 3), ], ); } private function storeAggregateMetric(string $metric, float $value): void { $now = now(); $this->storeMetric($metric, '1h', $value, $this->bucketTimestamp($now, 3600)); $this->storeMetric($metric, '6h', $value, $this->bucketTimestamp($now, 21600)); $this->storeMetric($metric, '1d', $value, $this->bucketTimestamp($now, 86400)); $this->pruneResolution('1h', $now->copy()->subHours(24)); $this->pruneResolution('6h', $now->copy()->subDays(7)); $this->pruneResolution('1d', $now->copy()->subDays(31)); } private function bucketTimestamp(Carbon $now, int $bucketSeconds): Carbon { return $now->copy()->setTimestamp(intdiv($now->timestamp, $bucketSeconds) * $bucketSeconds); } private function pruneResolution(string $resolution, Carbon $before): void { ServerMetric::query() ->where('resolution', $resolution) ->where('captured_at', '<', $before) ->delete(); } private function getLocalMetrics(): ?array { $load = sys_getloadavg(); if (! is_array($load) || count($load) < 3) { return null; } $loadData = [ '1min' => round((float) $load[0], 2), '5min' => round((float) $load[1], 2), '15min' => round((float) $load[2], 2), ]; $cpuCores = 0; if (is_readable('/proc/cpuinfo')) { $lines = file('/proc/cpuinfo', FILE_IGNORE_NEW_LINES); foreach ($lines as $line) { if (str_starts_with($line, 'processor')) { $cpuCores++; } } } $cpuCores = $cpuCores > 0 ? $cpuCores : 1; $memory = $this->readMeminfo(); $disk = $this->readDiskUsage(); return [ 'cpu' => [ 'usage' => 0, 'iowait' => 0, 'cores' => $cpuCores, 'model' => 'Local', ], 'memory' => $memory, 'disk' => $disk, 'load' => $loadData, ]; } private function fetchDiskSnapshot(?AgentClient $agent = null): array { try { $client = $agent ?? new AgentClient; $disk = $client->metricsDisk()['data'] ?? []; if (! empty($disk)) { return $disk; } } catch (\Exception) { // Ignore and fall back to local disk stats below. } return $this->readDiskUsage(); } private function readMeminfo(): array { $memInfo = []; if (is_readable('/proc/meminfo')) { $lines = file('/proc/meminfo', FILE_IGNORE_NEW_LINES); foreach ($lines as $line) { if (preg_match('/^(\w+):\s+(\d+)/', $line, $matches)) { $memInfo[$matches[1]] = (int) $matches[2]; } } } $totalKb = $memInfo['MemTotal'] ?? 0; $freeKb = $memInfo['MemFree'] ?? 0; $availableKb = $memInfo['MemAvailable'] ?? $freeKb; $buffersKb = $memInfo['Buffers'] ?? 0; $cachedKb = $memInfo['Cached'] ?? 0; $swapTotalKb = $memInfo['SwapTotal'] ?? 0; $swapFreeKb = $memInfo['SwapFree'] ?? 0; $swapUsedKb = max(0, $swapTotalKb - $swapFreeKb); $usedKb = max(0, $totalKb - $availableKb); return [ 'usage' => $totalKb > 0 ? round(($usedKb / $totalKb) * 100, 1) : 0, 'used' => round($usedKb / 1024, 0), 'total' => round($totalKb / 1024, 0), 'free' => round($freeKb / 1024, 0), 'cached' => round($cachedKb / 1024, 0), 'swap_used' => round($swapUsedKb / 1024, 0), 'swap_total' => round($swapTotalKb / 1024, 0), 'swap_usage' => $swapTotalKb > 0 ? round(($swapUsedKb / $swapTotalKb) * 100, 1) : 0, ]; } private function readDiskUsage(): array { $total = @disk_total_space('/') ?: 0; $free = @disk_free_space('/') ?: 0; $used = max(0, $total - $free); $usagePercent = $total > 0 ? round(($used / $total) * 100, 1) : 0; return [ 'partitions' => [ [ 'filesystem' => '/', 'mount' => '/', 'used' => $used, 'total' => $total, 'usage_percent' => $usagePercent, 'used_human' => $this->formatBytes($used), 'total_human' => $this->formatBytes($total), ], ], ]; } private function formatBytes(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB', 'TB']; $size = max(0, $bytes); $unit = 0; while ($size >= 1024 && $unit < count($units) - 1) { $size /= 1024; $unit++; } return round($size, 1).' '.$units[$unit]; } private function bytesToGb(int $bytes): float { if ($bytes <= 0) { return 0.0; } return round($bytes / 1024 / 1024 / 1024, 2); } }