From 1cbec8220dff3394293391d9eebc016f3824711f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 10 Jul 2025 16:08:04 -0700 Subject: [PATCH] add diagnostic page --- .../Components/Charts/CustomGauge/index.jsx | 4 +- client/src/Hooks/logHooks.js | 77 ++++++++++++++++++- client/src/Hooks/queueHooks.js | 57 -------------- .../Diagnostics/components/gauges/index.jsx | 74 ++++++++++++++++++ .../Diagnostics/components/gauges/utils.js | 4 + client/src/Pages/Logs/Diagnostics/index.jsx | 30 ++++++++ client/src/Pages/Logs/Logs/index.jsx | 2 +- client/src/Pages/Logs/Queue/index.jsx | 2 +- client/src/Pages/Logs/index.jsx | 5 +- client/src/Utils/NetworkService.js | 8 ++ client/src/locales/en.json | 6 +- server/controllers/diagnosticController.js | 73 ++++++++++++++++++ server/routes/diagnosticRoute.js | 1 + 13 files changed, 279 insertions(+), 64 deletions(-) delete mode 100644 client/src/Hooks/queueHooks.js create mode 100644 client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx create mode 100644 client/src/Pages/Logs/Diagnostics/components/gauges/utils.js create mode 100644 client/src/Pages/Logs/Diagnostics/index.jsx diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/Charts/CustomGauge/index.jsx index 3c7e13610..359b5b328 100644 --- a/client/src/Components/Charts/CustomGauge/index.jsx +++ b/client/src/Components/Charts/CustomGauge/index.jsx @@ -54,8 +54,8 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = const fillColor = progressWithinRange > threshold - ? theme.palette.error.lowContrast // CAIO_REVIEW - : theme.palette.accent.main; // CAIO_REVIEW + ? theme.palette.error.lowContrast + : theme.palette.accent.main; return ( { return [logs, isLoading, error]; }; -export { useFetchLogs }; +const useFetchQueueData = (trigger) => { + const [jobs, setJobs] = useState(undefined); + const [metrics, setMetrics] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + useEffect(() => { + const fetchJobs = async () => { + try { + setIsLoading(true); + const response = await networkService.getQueueData(); + if (response.status === 200) { + setJobs(response.data.data.jobs); + setMetrics(response.data.data.metrics); + } + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }; + + fetchJobs(); + }, [trigger]); + + return [jobs, metrics, isLoading, error]; +}; + +const useFlushQueue = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + const flushQueue = async (trigger, setTrigger) => { + try { + setIsLoading(true); + await networkService.flushQueue(); + createToast({ + body: "Queue flushed", + }); + } catch (error) { + setError(error); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + setTrigger(!trigger); + } + }; + return [flushQueue, isLoading, error]; +}; + +const useFetchDiagnostics = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const [diagnostics, setDiagnostics] = useState(undefined); + + useEffect(() => { + const fetchDiagnostics = async () => { + try { + setIsLoading(true); + const response = await networkService.getDiagnostics(); + setDiagnostics(response.data.data); + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }; + fetchDiagnostics(); + }, []); + + return [diagnostics, isLoading, error]; +}; + +export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics }; diff --git a/client/src/Hooks/queueHooks.js b/client/src/Hooks/queueHooks.js deleted file mode 100644 index f7a25b3d3..000000000 --- a/client/src/Hooks/queueHooks.js +++ /dev/null @@ -1,57 +0,0 @@ -import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; - -const useFetchQueueData = (trigger) => { - const [jobs, setJobs] = useState(undefined); - const [metrics, setMetrics] = useState(undefined); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - useEffect(() => { - const fetchJobs = async () => { - try { - setIsLoading(true); - const response = await networkService.getQueueData(); - if (response.status === 200) { - setJobs(response.data.data.jobs); - setMetrics(response.data.data.metrics); - } - } catch (error) { - setError(error); - } finally { - setIsLoading(false); - } - }; - - fetchJobs(); - }, [trigger]); - - return [jobs, metrics, isLoading, error]; -}; - -const useFlushQueue = () => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - - const flushQueue = async (trigger, setTrigger) => { - try { - setIsLoading(true); - await networkService.flushQueue(); - createToast({ - body: "Queue flushed", - }); - } catch (error) { - setError(error); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - setTrigger(!trigger); - } - }; - return [flushQueue, isLoading, error]; -}; - -export { useFetchQueueData, useFlushQueue }; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx new file mode 100644 index 000000000..21d9a35d8 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -0,0 +1,74 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Gauge from "../../../../../Components/Charts/CustomGauge"; +import Typography from "@mui/material/Typography"; + +// Utils +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +import { getPercentage } from "./utils"; + +const GaugeBox = ({ title, subtitle, children }) => { + const theme = useTheme(); + return ( + + {children} + + {title} + {subtitle} + + ); +}; + +const Gauges = ({ diagnostics }) => { + const heapTotalSize = getPercentage( + diagnostics?.v8HeapStats?.totalHeapSizeMb, + diagnostics?.v8HeapStats?.heapSizeLimitMb + ); + + const heapUsedSize = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeMb, + diagnostics?.v8HeapStats?.heapSizeLimitMb + ); + + const actualHeapUsed = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeMb, + diagnostics?.v8HeapStats?.totalHeapSizeMb + ); + const theme = useTheme(); + + return ( + + + + + + + + + + + + ); +}; + +Gauges.propTypes = { + diagnostics: PropTypes.object.isRequired, +}; + +export default Gauges; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js b/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js new file mode 100644 index 000000000..bdc570a3b --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js @@ -0,0 +1,4 @@ +export const getPercentage = (value, total) => { + if (!value || !total) return 0; + return (value / total) * 100; +}; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx new file mode 100644 index 000000000..9d7ee4639 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -0,0 +1,30 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Gauges from "./components/gauges"; + +import { useTheme } from "@emotion/react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useFetchDiagnostics } from "../../../Hooks/logHooks"; + +const Diagnostics = () => { + // Local state + + // Hooks + const theme = useTheme(); + const { t } = useTranslation(); + const [diagnostics, isLoading, error] = useFetchDiagnostics(); + console.log(diagnostics); + // Setup + return ( + + + {t("diagnosticsPage.description")} + + + + ); +}; + +export default Diagnostics; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index 6c5e1a301..d8bd323a3 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -51,7 +51,7 @@ const Logs = () => { return ( - {t("logsPage.description")} + {t("logsPage.description")} { const theme = useTheme(); // Local state - const [value, setValue] = useState(0); + const [value, setValue] = useState(2); // Handlers const handleChange = (event, newValue) => { @@ -31,9 +32,11 @@ const Logs = () => { > + {value === 0 && } {value === 1 && } + {value === 2 && } ); }; diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 7fba2ce18..895d90d30 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -1091,6 +1091,14 @@ class NetworkService { }, }); } + + async getDiagnostics() { + return this.axiosInstance.get(`/diagnostic/system`, { + headers: { + "Content-Type": "application/json", + }, + }); + } } export default NetworkService; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ddc07f166..54f9600e2 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -451,13 +451,17 @@ }, "tabs": { "logs": "Server logs", - "queue": "Job queue" + "queue": "Job queue", + "diagnostics": "Diagnostics" }, "title": "Logs", "toast": { "fetchLogsSuccess": "Logs fetched successfully" } }, + "diagnosticsPage": { + "description": "System diagnostics" + }, "low": "low", "maintenance": "maintenance", "maintenanceRepeat": "Maintenance Repeat", diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js index 8fa5fa42b..c4223dc04 100755 --- a/server/controllers/diagnosticController.js +++ b/server/controllers/diagnosticController.js @@ -1,6 +1,14 @@ import { handleError } from "./controllerUtils.js"; +import v8 from "v8"; +import os from "os"; + const SERVICE_NAME = "diagnosticController"; +const obs = new PerformanceObserver((items) => { + const entry = items.getEntries()[0]; + performance.clearMarks(); +}); +obs.observe({ entryTypes: ["measure"] }); class DiagnosticController { constructor(db) { this.db = db; @@ -44,5 +52,70 @@ class DiagnosticController { next(handleError(error, SERVICE_NAME, "getDbStats")); } } + + async getSystemStats(req, res, next) { + try { + // Memory Usage + const totalMemory = os.totalmem(); + const freeMemory = os.freemem(); + + const osStats = { + freeMemoryMb: freeMemory / 1024 / 1024, // MB + totalMemoryMb: totalMemory / 1024 / 1024, // MB + }; + + const used = process.memoryUsage(); + const memoryUsage = {}; + for (let key in used) { + memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB + } + + // CPU Usage + const cpuUsage = process.cpuUsage(); + const cpuMetrics = { + userUsageMs: cpuUsage.user / 1000, // ms + systemUsageMs: cpuUsage.system / 1000, // ms + }; + + // V8 Heap Statistics + const heapStats = v8.getHeapStatistics(); + const v8Metrics = { + totalHeapSizeMb: heapStats.total_heap_size / 1024 / 1024, // MB + usedHeapSizeMb: heapStats.used_heap_size / 1024 / 1024, // MB + heapSizeLimitMb: heapStats.heap_size_limit / 1024 / 1024, // MB + }; + + // Event Loop Delay + let eventLoopDelay = 0; + performance.mark("start"); + await new Promise((resolve) => setTimeout(resolve, 0)); + performance.mark("end"); + performance.measure("eventLoopDelay", "start", "end"); + const entries = performance.getEntriesByName("eventLoopDelay"); + if (entries.length > 0) { + eventLoopDelay = entries[0].duration; + } + + // Uptime + const uptime = process.uptime(); // seconds + + // Combine Metrics + const diagnostics = { + osStats, + memoryUsage, + cpuUsage: cpuMetrics, + v8HeapStats: v8Metrics, + eventLoopDelayMs: eventLoopDelay, + uptimeSeconds: uptime, + }; + + return res.success({ + msg: "OK", + data: diagnostics, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMemoryUsage")); + } + } } export default DiagnosticController; diff --git a/server/routes/diagnosticRoute.js b/server/routes/diagnosticRoute.js index 616d01ccf..0d8f4df84 100755 --- a/server/routes/diagnosticRoute.js +++ b/server/routes/diagnosticRoute.js @@ -13,6 +13,7 @@ class DiagnosticRoutes { ); this.router.post("/db/stats", this.diagnosticController.getDbStats); + this.router.get("/system", this.diagnosticController.getSystemStats); } getRouter() {