From 80dc7cdc9d0c6c6a01ff87e495c17ac7d5bd288f Mon Sep 17 00:00:00 2001 From: Alex Crivion Date: Fri, 31 Jan 2025 17:18:54 +0000 Subject: [PATCH] cpu,memory,disk stats via websockets + top 20 processes --- app/Http/Controllers/DashboardController.php | 13 +- app/Services/SystemStatsService.php | 55 ++++--- resources/js/Components/StatCard.jsx | 18 --- resources/js/Pages/Dashboard.jsx | 136 ------------------ .../Pages/Dashboard/Admin/AdminDashboard.jsx | 66 +++++++++ .../Dashboard/Admin/Components/CPULive.jsx | 65 +++++++++ .../Dashboard/Admin/Components/DiskLive.jsx | 57 ++++++++ .../Dashboard/Admin/Components/MemoryLive.jsx | 66 +++++++++ .../Admin/Components/TopProcesses.jsx | 92 ++++++++++++ resources/js/Pages/Profile/Edit.jsx | 2 +- routes/web.php | 14 +- 11 files changed, 400 insertions(+), 184 deletions(-) delete mode 100644 resources/js/Components/StatCard.jsx delete mode 100644 resources/js/Pages/Dashboard.jsx create mode 100644 resources/js/Pages/Dashboard/Admin/AdminDashboard.jsx create mode 100644 resources/js/Pages/Dashboard/Admin/Components/CPULive.jsx create mode 100644 resources/js/Pages/Dashboard/Admin/Components/DiskLive.jsx create mode 100644 resources/js/Pages/Dashboard/Admin/Components/MemoryLive.jsx create mode 100644 resources/js/Pages/Dashboard/Admin/Components/TopProcesses.jsx diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 1819152..2b1aaee 100755 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -6,6 +6,7 @@ use App\Services\SystemStatsService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Cache; +use Inertia\Inertia; class DashboardController extends Controller { @@ -21,10 +22,12 @@ class DashboardController extends Controller public function admin() { - // Get Top Sorting Option - $topSort = Cache::get('ps_aux_sort_by', 'cpu'); + return Inertia::render('Dashboard/Admin/AdminDashboard'); + } - return view('dashboard/admin', ['topSort' => $topSort]); + public function getTopSort() + { + return ['sortBy' => Cache::get('ps_aux_sort_by', 'cpu')]; } public function setTopSort(Request $r) @@ -33,11 +36,11 @@ class DashboardController extends Controller Cache::put('ps_aux_sort_by', $r->sortBy); - return response()->noContent(); + return ['sortBy' => $r->sortBy]; } public function user() { - return view('dashboard/user'); + return Inertia::render('Dashboard/User/UserDashboard'); } } diff --git a/app/Services/SystemStatsService.php b/app/Services/SystemStatsService.php index c2a19df..6c6daa0 100755 --- a/app/Services/SystemStatsService.php +++ b/app/Services/SystemStatsService.php @@ -19,38 +19,47 @@ class SystemStatsService /** * Fetch memory usage. */ - public function getMemoryUsage(): string + public function getMemoryUsage(): array { $memory = Process::pipe([ 'free -m', - "awk '/Mem:/ {print $3 \"MB / \"$2\"\" }'" + "awk '/Mem:/ {print $4,$3,$6,$2}'" ]); if ($memory->failed()) { - return $memory->errorOutput(); + return ['error' => $memory->errorOutput()]; } - return $memory->output(); + $stats = $memory->output(); + $stats = explode(" ", $stats); + + return [ + 'free' => $stats[0], + 'used' => $stats[1], + 'buffcache' => $stats[2], + 'total' => $stats[3] + ]; } /** * Fetch disk usage. */ - public function getDiskUsage(): string + public function getDiskUsage(): array { // Fetch disk usage details - $diskUsage = Process::run('df -h / | awk \'/\\// {print $3 " " $4 " " $5}\'')->output(); + $diskUsage = Process::run('df -h / | awk \'/\\// {print $2, $3, $4, $5}\'')->output(); $diskUsageParts = preg_split('/\s+/', trim($diskUsage)); - if (count($diskUsageParts) >= 3) { - $usedSpace = $diskUsageParts[0]; - $availableSpace = $diskUsageParts[1]; - $usagePercentage = $diskUsageParts[2]; - - return "{$usedSpace} / {$availableSpace} ({$usagePercentage})"; + if (count($diskUsageParts) >= 4) { + return [ + 'size' => $diskUsageParts[0], + 'used' => $diskUsageParts[1], + 'free' => $diskUsageParts[2], + 'percent' => $diskUsageParts[3] + ]; } - return 'N/A'; + return []; } /** @@ -228,19 +237,23 @@ class SystemStatsService { $stats = [ 'whoami' => $this->getWhoami(), - 'cpuUsage' => $this->getCpuUsage(), - 'memoryUsage' => $this->getMemoryUsage(), - 'diskUsage' => $this->getDiskUsage(), - 'loadTimes' => $this->getLoadTimes(), - 'uptime' => $this->getUptime(), - 'processCount' => $this->getProcessCount(), - 'userCount' => $this->getUserCount(), + 'cpuStats' => [ + 'usage' => $this->getCpuUsage(), + 'loadTimes' => $this->getLoadTimes(), + 'uptime' => $this->getUptime(), + 'processCount' => $this->getProcessCount(), + ], + 'diskStats' => $this->getDiskUsage(), + 'memoryStats' => $this->getMemoryUsage(), 'nginxStatus' => $this->getNginxStatus(), 'phpStatus' => $this->getPhpFpmStatus(), 'sslStatus' => $this->getSslStatus(), 'nginxPort' => $this->getNginxPort(), 'apache' => $this->getApacheStatus(), - 'mysql' => $this->getMysqlStatus() + 'mysql' => $this->getMysqlStatus(), + + 'domainCount' => rand(1, 100), + 'userCount' => $this->getUserCount(), ]; return $stats; diff --git a/resources/js/Components/StatCard.jsx b/resources/js/Components/StatCard.jsx deleted file mode 100644 index d3e3e63..0000000 --- a/resources/js/Components/StatCard.jsx +++ /dev/null @@ -1,18 +0,0 @@ - -const StatCard = ({ icon, label, value, status, subtext }) => { - return ( -
-
- {icon} -
-

{label}

-

{value}

- {status &&

Status: {status}

} - {subtext &&

{subtext}

} -
-
-
- ); -} - -export default StatCard diff --git a/resources/js/Pages/Dashboard.jsx b/resources/js/Pages/Dashboard.jsx deleted file mode 100644 index 79a742f..0000000 --- a/resources/js/Pages/Dashboard.jsx +++ /dev/null @@ -1,136 +0,0 @@ -import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; -import { Head } from '@inertiajs/react'; -import { RiDashboard3Fill } from "react-icons/ri"; -import { useEffect, useState } from "react"; -import ShowError from '@/Components/ShowError'; -import StatCard from '@/Components/StatCard'; -import { FaMicrochip, FaMemory, FaHardDrive, FaBarsProgress, FaArrowUpShortWide, FaSitemap, FaUsers, FaLock } from "react-icons/fa6"; -import { SiApache, SiNginx, SiMysql, SiPhp } from "react-icons/si"; - - - -export default function Dashboard() { - - const [liveStats, setLiveStats] = useState([]); - const [topStats, setTopStats] = useState([]); - - const echo = window.Echo; - const dashboardChannel = echo.private("systemstats"); - const topStatsChannel = echo.private("topstats"); - - - useEffect(() => { - - dashboardChannel.listen("SystemStatsEvent", (data) => { - console.log(data); - console.log(typeof data.phpStatus); - setLiveStats(data); - }); - - topStatsChannel.listen("TopStatsEvent", (data) => { - // console.log(data); - setTopStats(data); - }); - - // Set interval to "whisper" every 2 seconds - // Makes it so we get stats via sockets - const whisperInterval = setInterval(() => { - dashboardChannel.whisper("typing", { requesting: "dashboard-realtime-stats" }); - topStatsChannel.whisper("typing", { requesting: "dashboard-top-stats" }); - }, 2000); - - return () => { - clearInterval(whisperInterval); - echo.leave("systemstats"); - echo.leave("topstats"); - }; - }, []); - - return ( - - - Dashboard - - } - > - - -
- } label="CPU Usage" value={`${liveStats?.cpuUsage}%`} /> - } label="Memory Usage" value={`${liveStats?.memoryUsage} MB`} /> - } label="Disk Usage" value={liveStats?.diskUsage} /> - } label="Load Times" value={liveStats?.loadTimes} /> - } label="Uptime" value={liveStats?.uptime} /> - } label="Processes" value={liveStats?.processCount} /> - } label="Accounts" value={`${liveStats?.userCount} users, ${liveStats?.domainCount} domains`} /> - } label="Apache Server" value={liveStats?.apache?.memory} status={liveStats?.apache?.status} /> - } label="Nginx Status" value={liveStats?.nginxStatus} subtext={`Port: ${liveStats?.nginxPort || 'N/A'}`} /> - } label="MySQL Server" value={liveStats?.mysql?.memory} status={liveStats?.mysql?.status} /> - } label="SSL Status" value={liveStats?.sslStatus} /> -
- - -
-
-

Top 20 Processes

-
- TOGGLE CPu | MEm -
-
- -
- - {topStats?.error && } - - - - - - - - - - - - - {topStats?.processes?.length > 0 ? ( - topStats?.processes?.map((process, index) => ( - - - - - - - - )) - ) : ( - - - - )} - -
PID%CPU%MEMUSERCOMMAND
- {process.pid} - - {process.cpu}% - - {process.mem}% - - {process.user} - - -
- No processes found. -
-
-
-
- ); -} diff --git a/resources/js/Pages/Dashboard/Admin/AdminDashboard.jsx b/resources/js/Pages/Dashboard/Admin/AdminDashboard.jsx new file mode 100644 index 0000000..32bb073 --- /dev/null +++ b/resources/js/Pages/Dashboard/Admin/AdminDashboard.jsx @@ -0,0 +1,66 @@ +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; +import { Head, Link } from '@inertiajs/react'; +import { RiDashboard3Fill } from "react-icons/ri"; +import { useEffect, useState } from "react"; +import TopProcesses from './Components/TopProcesses'; +import CPULive from './Components/CPULive'; +import MemoryLive from './Components/MemoryLive'; +import DiskLive from './Components/DiskLive'; + + +export default function Dashboard() { + + const [liveStats, setLiveStats] = useState([]); + + const echo = window.Echo; + const dashboardChannel = echo.private("systemstats"); + + useEffect(() => { + + dashboardChannel.listen("SystemStatsEvent", (data) => { + console.log(data); + setLiveStats(data); + }); + + const whisperInterval = setInterval(() => { + dashboardChannel.whisper("typing", { requesting: "dashboard-realtime-stats" }); + }, 2000); + + return () => { + clearInterval(whisperInterval); + echo.leave("systemstats"); + }; + }, []); + + return ( + + + Dashboard + + } + > + + +
+ + {/* CPU Usage*/} + + + {/* Memory Usage*/} + + + {/* Disk Usage */} + + +
+ + +
+ +
+ +
+ ); +} diff --git a/resources/js/Pages/Dashboard/Admin/Components/CPULive.jsx b/resources/js/Pages/Dashboard/Admin/Components/CPULive.jsx new file mode 100644 index 0000000..fda7e18 --- /dev/null +++ b/resources/js/Pages/Dashboard/Admin/Components/CPULive.jsx @@ -0,0 +1,65 @@ +import { Link } from '@inertiajs/react'; +import { FaMicrochip, FaBarsProgress, FaArrowUpShortWide, FaSitemap } from "react-icons/fa6"; +import { FaAngleDoubleRight } from "react-icons/fa"; +import { TbAntennaBars5 } from "react-icons/tb"; +import { ImSpinner9 } from "react-icons/im"; + +const CPULive = ({ cpuStats }) => { + + return ( +
+ +
+
+ +
+ +
CPU Stats
+ +
+ + + +
+
+ +
+

+ + Live Load: {cpuStats?.usage ? ( + cpuStats.usage + "%" + ) : ( + + )} +

+

+ + Average: {cpuStats?.loadTimes ? ( + cpuStats.loadTimes + ) : ( + + )} +

+

+ + Processes: {cpuStats?.processCount ? ( + cpuStats.processCount + ) : ( + + )} +

+

+ + Uptime: {cpuStats?.uptime ? ( + cpuStats.uptime + ) : ( + + )} +

+ +
+
+ ); +} + +export default CPULive diff --git a/resources/js/Pages/Dashboard/Admin/Components/DiskLive.jsx b/resources/js/Pages/Dashboard/Admin/Components/DiskLive.jsx new file mode 100644 index 0000000..e67fe91 --- /dev/null +++ b/resources/js/Pages/Dashboard/Admin/Components/DiskLive.jsx @@ -0,0 +1,57 @@ +import { FaBuffer, FaHardDrive } from "react-icons/fa6"; +import { ImSpinner9 } from "react-icons/im"; +import { GiPenguin, GiProgression } from "react-icons/gi"; +import { MdOutlineSummarize } from "react-icons/md"; + +const DiskLive = ({ diskStats }) => { + + return ( +
+ +
+
+ +
+ +
Disk Usage at /
+
+ +
+

+ + Used: {diskStats?.used ? ( + diskStats.used + ) : ( + + )} +

+

+ + Free: {diskStats?.free ? ( + diskStats.free + ) : ( + + )} +

+

+ + Size: {diskStats?.size ? ( + diskStats.size + ) : ( + + )} +

+

+ + Percent Used : {diskStats?.percent ? ( + diskStats.percent + ) : ( + + )} +

+
+
+ ); +} + +export default DiskLive diff --git a/resources/js/Pages/Dashboard/Admin/Components/MemoryLive.jsx b/resources/js/Pages/Dashboard/Admin/Components/MemoryLive.jsx new file mode 100644 index 0000000..7197881 --- /dev/null +++ b/resources/js/Pages/Dashboard/Admin/Components/MemoryLive.jsx @@ -0,0 +1,66 @@ +import { Link } from '@inertiajs/react'; + +import { FaMemory, FaBuffer, } from "react-icons/fa6"; +import { FaAngleDoubleRight } from "react-icons/fa"; +import { ImSpinner9 } from "react-icons/im"; +import { GiPenguin, GiProgression } from "react-icons/gi"; +import { MdOutlineSummarize } from "react-icons/md"; + +const MemoryLive = ({ memoryStats }) => { + + return ( +
+ +
+
+ +
+ +
Memory Usage
+ +
+ + + +
+
+ +
+

+ + Used: {memoryStats?.used ? ( + memoryStats.used + "MB" + ) : ( + + )} +

+

+ + Free: {memoryStats?.free ? ( + memoryStats.free + "MB" + ) : ( + + )} +

+

+ + Buff/Cache: {memoryStats?.buffcache ? ( + memoryStats.buffcache + "MB" + ) : ( + + )} +

+

+ + Total: {memoryStats?.total ? ( + memoryStats.total + "MB" + ) : ( + + )} +

+
+
+ ); +} + +export default MemoryLive diff --git a/resources/js/Pages/Dashboard/Admin/Components/TopProcesses.jsx b/resources/js/Pages/Dashboard/Admin/Components/TopProcesses.jsx new file mode 100644 index 0000000..c59c938 --- /dev/null +++ b/resources/js/Pages/Dashboard/Admin/Components/TopProcesses.jsx @@ -0,0 +1,92 @@ +import { Tooltip } from 'react-tooltip' +import { useEffect, useState } from "react"; + + +const TopProcesses = () => { + const [topStats, setTopStats] = useState([]); + + const echo = window.Echo; + const topStatsChannel = echo.private("topstats"); + + useEffect(() => { + + topStatsChannel.listen("TopStatsEvent", (data) => { + setTopStats(data); + }); + + // Set interval to "whisper" every 2 seconds + // Makes it so we get stats via sockets + const whisperInterval = setInterval(() => { + topStatsChannel.whisper("typing", { requesting: "dashboard-top-stats" }); + }, 2000); + + return () => { + clearInterval(whisperInterval); + echo.leave("topstats"); + }; + }, []); + + + { topStats?.error && } + + return ( +
+
+

Top 20 Processes

+
+ TOGGLE CPu | MEm +
+
+ + + + + + + + + + + + {topStats?.length > 0 ? ( + topStats?.map((process, index) => ( + + + + + + + + )) + ) : ( + + + + )} + +
PID%CPU%MEMUSERCOMMAND
+ {process.pid} + + {process.cpu}% + + {process.mem}% + + {process.user} + + + +
+ No processes found. +
+
); +} + +export default TopProcesses diff --git a/resources/js/Pages/Profile/Edit.jsx b/resources/js/Pages/Profile/Edit.jsx index b0fb272..53296e0 100644 --- a/resources/js/Pages/Profile/Edit.jsx +++ b/resources/js/Pages/Profile/Edit.jsx @@ -18,7 +18,7 @@ export default function Edit({ mustVerifyEmail, status }) {
-
+
middleware(['auth', 'verified'])->name('dashboard'); +Route::get('/dashboard', [DashboardController::class, 'index'])->middleware(['auth'])->name('dashboard'); +Route::get('/dashboard/admin', [DashboardController::class, 'admin'])->middleware(['auth', AdminMiddleware::class])->name('dashboard.admin'); +Route::patch('/dashboard/admin/set-top-sort', [DashboardController::class, 'setTopSort'])->middleware(['auth', AdminMiddleware::class])->name('dashboard.admin.set-top-sort'); +Route::get('/dashboard/user', [DashboardController::class, 'user'])->middleware(['auth'])->name('dashboard.user'); + + +Route::get('/stats/history', [StatsHistoryController::class, 'cpuAndMemory'])->middleware(['auth', AdminMiddleware::class])->name('stats.history'); + Route::middleware('auth')->group(function () { Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');