447 lines
14 KiB
PHP
447 lines
14 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Admin\Widgets;
|
|
|
|
use App\Services\SysstatMetrics;
|
|
use Filament\Widgets\Widget;
|
|
|
|
class ServerChartsWidget extends Widget
|
|
{
|
|
protected string $view = 'filament.admin.widgets.server-charts';
|
|
|
|
protected int|string|array $columnSpan = 'full';
|
|
|
|
protected ?string $pollingInterval = '60s';
|
|
|
|
public int $refreshKey = 0;
|
|
|
|
public string $range = '5m';
|
|
|
|
/**
|
|
* @var array<string, mixed>
|
|
*/
|
|
public array $diskSnapshot = [];
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->diskSnapshot = $this->fetchDiskSnapshot();
|
|
}
|
|
|
|
public function refreshData(): void
|
|
{
|
|
$this->refreshKey++;
|
|
$data = $this->getData();
|
|
$config = $this->rangeConfig($this->range);
|
|
$sysstat = new SysstatMetrics;
|
|
$label = now($sysstat->timezoneName())->format($config['label_format']);
|
|
|
|
$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_points' => $config['points'],
|
|
'label_format' => $config['label_format'],
|
|
'interval_seconds' => $config['interval_seconds'],
|
|
]);
|
|
}
|
|
|
|
public function getData(): array
|
|
{
|
|
$sysstat = new SysstatMetrics;
|
|
$latest = $sysstat->latest();
|
|
|
|
try {
|
|
$disk = $this->diskSnapshot;
|
|
if (empty($disk)) {
|
|
$disk = $this->fetchDiskSnapshot();
|
|
$this->diskSnapshot = $disk;
|
|
}
|
|
|
|
$cpu = $this->getLocalCpuInfo();
|
|
$memory = $this->readMeminfo();
|
|
|
|
if ($latest !== null) {
|
|
$memory['usage'] = round((float) $latest['memory'], 1);
|
|
$memory['swap_usage'] = round((float) $latest['swap'], 1);
|
|
}
|
|
|
|
return [
|
|
'cpu' => [
|
|
'usage' => 0,
|
|
'iowait' => round((float) ($latest['iowait'] ?? 0), 1),
|
|
'cores' => $cpu['cores'] ?? 0,
|
|
'model' => $cpu['model'] ?? 'Unknown',
|
|
],
|
|
'memory' => [
|
|
'usage' => $memory['usage'] ?? 0,
|
|
'used' => $memory['used'] ?? 0,
|
|
'total' => $memory['total'] ?? 0,
|
|
'free' => $memory['free'] ?? 0,
|
|
'cached' => $memory['cached'] ?? 0,
|
|
'swap_used' => $memory['swap_used'] ?? 0,
|
|
'swap_total' => $memory['swap_total'] ?? 0,
|
|
'swap_usage' => $memory['swap_usage'] ?? 0,
|
|
],
|
|
'disk' => [
|
|
'partitions' => $disk['partitions'] ?? [],
|
|
],
|
|
'load' => [
|
|
'1min' => $latest['load1'] ?? 0,
|
|
'5min' => $latest['load5'] ?? 0,
|
|
'15min' => $latest['load15'] ?? 0,
|
|
],
|
|
'uptime' => $this->getLocalUptime(),
|
|
'source' => $latest !== null ? 'sysstat' : 'local',
|
|
];
|
|
} catch (\Exception $e) {
|
|
$fallback = $this->getLocalMetrics();
|
|
if ($fallback !== null) {
|
|
if (empty($this->diskSnapshot)) {
|
|
$this->diskSnapshot = $fallback['disk'] ?? [];
|
|
}
|
|
|
|
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',
|
|
'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',
|
|
'source' => 'error',
|
|
];
|
|
}
|
|
}
|
|
|
|
public function setRange(string $range): void
|
|
{
|
|
$this->range = $this->normalizeRange($range);
|
|
|
|
$config = $this->rangeConfig($this->range);
|
|
$data = $this->getData();
|
|
$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-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 {
|
|
$config = $this->rangeConfig($this->range);
|
|
$sysstat = new SysstatMetrics;
|
|
$history = $sysstat->history($config['points'], $config['interval_seconds'], $config['label_format']);
|
|
|
|
if (! empty($history)) {
|
|
$history['disk'] = $this->seedDiskHistory($config['points'], $diskPartitions);
|
|
|
|
return $history;
|
|
}
|
|
|
|
return $this->seedHistory(
|
|
$config['points'],
|
|
$config['label_format'],
|
|
$config['interval_seconds'],
|
|
$load,
|
|
$iowait,
|
|
$memory,
|
|
$swap,
|
|
$diskPartitions,
|
|
);
|
|
}
|
|
|
|
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', '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' => 30, 'resolution' => '1m', 'label_format' => 'H:i', 'interval_seconds' => 60],
|
|
};
|
|
}
|
|
|
|
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 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),
|
|
];
|
|
|
|
$cpuInfo = $this->getLocalCpuInfo();
|
|
|
|
$memory = $this->readMeminfo();
|
|
$disk = $this->readDiskUsage();
|
|
|
|
return [
|
|
'cpu' => [
|
|
'usage' => 0,
|
|
'iowait' => 0,
|
|
'cores' => $cpuInfo['cores'],
|
|
'model' => $cpuInfo['model'],
|
|
],
|
|
'memory' => $memory,
|
|
'disk' => $disk,
|
|
'load' => $loadData,
|
|
'uptime' => $this->getLocalUptime(),
|
|
];
|
|
}
|
|
|
|
private function fetchDiskSnapshot(): array
|
|
{
|
|
return $this->readDiskUsage();
|
|
}
|
|
|
|
/**
|
|
* @return array{cores: int, model: string}
|
|
*/
|
|
private function getLocalCpuInfo(): array
|
|
{
|
|
$cpuCores = 0;
|
|
$cpuModel = 'Unknown';
|
|
|
|
if (is_readable('/proc/cpuinfo')) {
|
|
$lines = file('/proc/cpuinfo', FILE_IGNORE_NEW_LINES);
|
|
foreach ($lines as $line) {
|
|
if (str_starts_with($line, 'processor')) {
|
|
$cpuCores++;
|
|
}
|
|
if (str_starts_with($line, 'model name')) {
|
|
$cpuModel = trim(explode(':', $line, 2)[1] ?? $cpuModel);
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'cores' => $cpuCores > 0 ? $cpuCores : 1,
|
|
'model' => $cpuModel,
|
|
];
|
|
}
|
|
|
|
private function getLocalUptime(): string
|
|
{
|
|
if (! is_readable('/proc/uptime')) {
|
|
return 'N/A';
|
|
}
|
|
|
|
$contents = trim((string) file_get_contents('/proc/uptime'));
|
|
$parts = preg_split('/\s+/', $contents);
|
|
$seconds = isset($parts[0]) ? (int) floor((float) $parts[0]) : 0;
|
|
|
|
if ($seconds <= 0) {
|
|
return 'N/A';
|
|
}
|
|
|
|
$days = intdiv($seconds, 86400);
|
|
$hours = intdiv($seconds % 86400, 3600);
|
|
$minutes = intdiv($seconds % 3600, 60);
|
|
|
|
if ($days > 0) {
|
|
return sprintf('%dd %dh %dm', $days, $hours, $minutes);
|
|
}
|
|
|
|
if ($hours > 0) {
|
|
return sprintf('%dh %dm', $hours, $minutes);
|
|
}
|
|
|
|
return sprintf('%dm', $minutes);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $diskPartitions
|
|
* @return array<string, array<int, float>>
|
|
*/
|
|
private function seedDiskHistory(int $points, array $diskPartitions): array
|
|
{
|
|
$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] = array_fill(0, $points, round($value, 2));
|
|
}
|
|
|
|
return $diskSeries;
|
|
}
|
|
|
|
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|float $bytes): string
|
|
{
|
|
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
$size = max(0, (float) $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);
|
|
}
|
|
}
|