mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-24 02:29:35 -06:00
add diagnostic page
This commit is contained in:
@@ -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 (
|
||||
<Box
|
||||
|
||||
@@ -32,4 +32,79 @@ const useFetchLogs = () => {
|
||||
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 };
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
p={theme.spacing(2)}
|
||||
>
|
||||
{children}
|
||||
|
||||
<Typography variant="h2">{title}</Typography>
|
||||
<Typography variant="body2">{subtitle}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(4)}
|
||||
>
|
||||
<GaugeBox
|
||||
title="Heap Allocation"
|
||||
subtitle="% of available memory"
|
||||
>
|
||||
<Gauge progress={heapTotalSize} />
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title="Heap Usage"
|
||||
subtitle="% of available memory"
|
||||
>
|
||||
<Gauge progress={heapUsedSize} />
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title="Heap Utilization"
|
||||
subtitle="% of Allocated"
|
||||
>
|
||||
<Gauge progress={actualHeapUsed} />
|
||||
</GaugeBox>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Gauges.propTypes = {
|
||||
diagnostics: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Gauges;
|
||||
@@ -0,0 +1,4 @@
|
||||
export const getPercentage = (value, total) => {
|
||||
if (!value || !total) return 0;
|
||||
return (value / total) * 100;
|
||||
};
|
||||
30
client/src/Pages/Logs/Diagnostics/index.jsx
Normal file
30
client/src/Pages/Logs/Diagnostics/index.jsx
Normal file
@@ -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 (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Box>
|
||||
<Typography variant="h2">{t("diagnosticsPage.description")}</Typography>
|
||||
</Box>
|
||||
<Gauges diagnostics={diagnostics} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Diagnostics;
|
||||
@@ -51,7 +51,7 @@ const Logs = () => {
|
||||
return (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Box>
|
||||
<Typography variant="body">{t("logsPage.description")}</Typography>
|
||||
<Typography variant="h2">{t("logsPage.description")}</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
direction="row"
|
||||
|
||||
@@ -8,7 +8,7 @@ import Button from "@mui/material/Button";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useFetchQueueData, useFlushQueue } from "../../../Hooks/queueHooks";
|
||||
import { useFetchQueueData, useFlushQueue } from "../../../Hooks/logHooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import Tabs from "@mui/material/Tabs";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Queue from "./Queue";
|
||||
import LogsComponent from "./Logs";
|
||||
import Diagnostics from "./Diagnostics";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,7 +15,7 @@ const Logs = () => {
|
||||
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 = () => {
|
||||
>
|
||||
<Tab label={t("logsPage.tabs.logs")} />
|
||||
<Tab label={t("logsPage.tabs.queue")} />
|
||||
<Tab label={t("logsPage.tabs.diagnostics")} />
|
||||
</Tabs>
|
||||
{value === 0 && <LogsComponent />}
|
||||
{value === 1 && <Queue />}
|
||||
{value === 2 && <Diagnostics />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1091,6 +1091,14 @@ class NetworkService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getDiagnostics() {
|
||||
return this.axiosInstance.get(`/diagnostic/system`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -13,6 +13,7 @@ class DiagnosticRoutes {
|
||||
);
|
||||
|
||||
this.router.post("/db/stats", this.diagnosticController.getDbStats);
|
||||
this.router.get("/system", this.diagnosticController.getSystemStats);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
Reference in New Issue
Block a user