Merge pull request #2595 from bluewave-labs/feat/diagnostics
feat: diagnostics tab
This commit is contained in:
16 files changed
+561
-114
No files matched your search
@@ -1,8 +1,10 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import PropTypes from "prop-types";
|
||||
import "./index.css";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
const MINIMUM_VALUE = 0;
|
||||
const MAXIMUM_VALUE = 100;
|
||||
@@ -16,6 +18,8 @@ const MAXIMUM_VALUE = 100;
|
||||
* @param {number} [props.radius=60] - Radius of the gauge circle
|
||||
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
|
||||
* @param {number} [props.threshold=50] - Threshold for color change
|
||||
* @param {number} [props.precision=1] - Precision of the progress percentage
|
||||
* @param {string} [props.unit="%"] - Unit of progress
|
||||
*
|
||||
* @example
|
||||
* <CustomGauge
|
||||
@@ -27,7 +31,15 @@ const MAXIMUM_VALUE = 100;
|
||||
*
|
||||
* @returns {React.ReactElement} Rendered CustomGauge component
|
||||
*/
|
||||
const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => {
|
||||
const CustomGauge = ({
|
||||
isLoading = false,
|
||||
progress = 0,
|
||||
radius = 70,
|
||||
strokeWidth = 15,
|
||||
threshold = 50,
|
||||
precision = 1,
|
||||
unit = "%",
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
// Calculate the length of the stroke for the circle
|
||||
const { circumference, totalSize, strokeLength } = useMemo(
|
||||
@@ -54,8 +66,22 @@ 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;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack
|
||||
className="radial-chart"
|
||||
width={radius}
|
||||
height={radius}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
<CircularProgress color="accent" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -71,7 +97,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
>
|
||||
<circle
|
||||
className="radial-chart-base"
|
||||
stroke={theme.palette.secondary.light} // CAIO_REVIEW
|
||||
stroke={theme.palette.secondary.light}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
cx={totalSize / 2} // Center the circle
|
||||
@@ -103,7 +129,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
fill: theme.typography.h2.color,
|
||||
}}
|
||||
>
|
||||
{`${progressWithinRange.toFixed(1)}%`}
|
||||
{`${progressWithinRange.toFixed(precision)}${unit}`}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
@@ -112,8 +138,11 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
|
||||
export default CustomGauge;
|
||||
|
||||
CustomGauge.propTypes = {
|
||||
isLoading: PropTypes.bool,
|
||||
progress: PropTypes.number,
|
||||
radius: PropTypes.number,
|
||||
strokeWidth: PropTypes.number,
|
||||
threshold: PropTypes.number,
|
||||
precision: PropTypes.number,
|
||||
unit: PropTypes.string,
|
||||
};
|
||||
@@ -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,109 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
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/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const GaugeBox = ({ title, subtitle, children }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
p={theme.spacing(2)}
|
||||
maxWidth={150}
|
||||
width={150}
|
||||
>
|
||||
{children}
|
||||
|
||||
<Typography variant="h2">{title}</Typography>
|
||||
<Typography variant="body2">{subtitle}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
GaugeBox.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
const Gauges = ({ diagnostics, isLoading }) => {
|
||||
const heapTotalSize = getPercentage(
|
||||
diagnostics?.v8HeapStats?.totalHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.heapSizeLimitBytes
|
||||
);
|
||||
|
||||
const heapUsedSize = getPercentage(
|
||||
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.heapSizeLimitBytes
|
||||
);
|
||||
|
||||
const actualHeapUsed = getPercentage(
|
||||
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
|
||||
diagnostics?.v8HeapStats?.totalHeapSizeBytes
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={theme.spacing(4)}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.heapAllocationTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.heapAllocationSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={heapTotalSize}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.heapUsageTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.heapUsageSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={heapUsedSize}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.heapUtilizationTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.heapUtilizationSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={actualHeapUsed}
|
||||
/>
|
||||
</GaugeBox>
|
||||
<GaugeBox
|
||||
title={t("diagnosticsPage.gauges.instantCpuUsageTitle")}
|
||||
subtitle={t("diagnosticsPage.gauges.instantCpuUsageSubtitle")}
|
||||
>
|
||||
<Gauge
|
||||
isLoading={isLoading}
|
||||
radius={100}
|
||||
progress={diagnostics?.cpuUsage?.usagePercentage}
|
||||
precision={2}
|
||||
/>
|
||||
</GaugeBox>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Gauges.propTypes = {
|
||||
diagnostics: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Gauges;
|
||||
@@ -0,0 +1,97 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Card from "@mui/material/Card";
|
||||
import CardContent from "@mui/material/CardContent";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
|
||||
import { formatBytes } from "../../utils/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StatsCard = ({ title, value, unit = "", isLoading }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Card sx={{ width: 150, maxWidth: 150, height: 80, maxHeight: 80 }}>
|
||||
{isLoading ? (
|
||||
<Stack
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
height={80}
|
||||
maxHeight={80}
|
||||
>
|
||||
<CircularProgress color="accent" />
|
||||
</Stack>
|
||||
) : (
|
||||
<CardContent>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color={theme.palette.primary.contrastText}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{value} {unit}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
StatsCard.propTypes = {
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
unit: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
const Stats = ({ diagnostics, isLoading }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(4)}
|
||||
flexWrap="wrap"
|
||||
>
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.eventLoopDelayTitle")}
|
||||
value={getHumanReadableDuration(diagnostics?.eventLoopDelayMs)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.uptimeTitle")}
|
||||
value={getHumanReadableDuration(diagnostics?.uptimeMs)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.usedHeapSizeTitle")}
|
||||
value={formatBytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.totalHeapSizeTitle")}
|
||||
value={formatBytes(diagnostics?.v8HeapStats?.totalHeapSizeBytes)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<StatsCard
|
||||
title={t("diagnosticsPage.stats.osMemoryLimitTitle")}
|
||||
value={formatBytes(diagnostics?.osStats?.totalMemoryBytes)}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Stats.propTypes = {
|
||||
diagnostics: PropTypes.object,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Stats;
|
||||
@@ -0,0 +1,44 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Gauges from "./components/gauges";
|
||||
import Stats from "./components/stats";
|
||||
import Divider from "@mui/material/Divider";
|
||||
|
||||
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();
|
||||
// Setup
|
||||
return (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Box>
|
||||
<Typography variant="h2">{t("diagnosticsPage.diagnosticDescription")}</Typography>
|
||||
</Box>
|
||||
<Divider color={theme.palette.accent.main} />
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<Gauges
|
||||
diagnostics={diagnostics}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<Stats
|
||||
diagnostics={diagnostics}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Diagnostics;
|
||||
@@ -0,0 +1,16 @@
|
||||
export const getPercentage = (value, total) => {
|
||||
if (!value || !total) return 0;
|
||||
return (value / total) * 100;
|
||||
};
|
||||
|
||||
export const formatBytes = (bytes) => {
|
||||
if (!bytes) return "N/A";
|
||||
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from "@mui/material/Divider";
|
||||
|
||||
import { useFetchLogs } from "../../../Hooks/logHooks";
|
||||
import { useTheme } from "@emotion/react";
|
||||
@@ -51,12 +52,15 @@ const Logs = () => {
|
||||
return (
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Box>
|
||||
<Typography variant="body">{t("logsPage.description")}</Typography>
|
||||
<Typography variant="h2">{t("logsPage.description")}</Typography>
|
||||
</Box>
|
||||
<Divider color={theme.palette.accent.main} />
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<Typography>{t("logsPage.logLevelSelect.title")}</Typography>
|
||||
<Select
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import StatBox from "../../../../../Components/StatBox";
|
||||
import StatusBoxes from "../../../../../Components/StatusBoxes";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
|
||||
@@ -24,20 +22,17 @@ const Metrics = ({ metrics = {} }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(2)}>
|
||||
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
|
||||
<StatusBoxes flexWrap="wrap">
|
||||
{data.map((metric) => {
|
||||
return (
|
||||
<StatBox
|
||||
key={metric.key}
|
||||
heading={metric.title}
|
||||
subHeading={metric.value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StatusBoxes>
|
||||
</Stack>
|
||||
<StatusBoxes flexWrap="wrap">
|
||||
{data.map((metric) => {
|
||||
return (
|
||||
<StatBox
|
||||
key={metric.key}
|
||||
heading={metric.title}
|
||||
subHeading={metric.value}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StatusBoxes>
|
||||
);
|
||||
};
|
||||
export default Metrics;
|
||||
@@ -5,10 +5,12 @@ import Metrics from "./components/Metrics";
|
||||
import FailedJobTable from "./components/FailedJobTable";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import Button from "@mui/material/Button";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Divider from "@mui/material/Divider";
|
||||
|
||||
// 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";
|
||||
|
||||
@@ -26,39 +28,46 @@ const QueueDetails = () => {
|
||||
if (error || flushError) return <div>Error: {error.message}</div>;
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Metrics metrics={metrics} />
|
||||
<JobTable jobs={jobs} />
|
||||
<FailedJobTable metrics={metrics} />
|
||||
|
||||
<ButtonGroup
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{
|
||||
position: "sticky",
|
||||
bottom: 0,
|
||||
zIndex: 1000,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
p: theme.spacing(4),
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderRadius: theme.spacing(2),
|
||||
}}
|
||||
<Stack gap={theme.spacing(4)}>
|
||||
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
|
||||
<Divider color={theme.palette.accent.main} />
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
mt={theme.spacing(10)}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrigger(!trigger);
|
||||
<Metrics metrics={metrics} />
|
||||
<JobTable jobs={jobs} />
|
||||
<FailedJobTable metrics={metrics} />
|
||||
|
||||
<ButtonGroup
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{
|
||||
position: "sticky",
|
||||
bottom: 0,
|
||||
zIndex: 1000,
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
p: theme.spacing(4),
|
||||
border: `1px solid ${theme.palette.primary.lowContrast}`,
|
||||
borderRadius: theme.spacing(2),
|
||||
}}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("queuePage.refreshButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => flushQueue(trigger, setTrigger)}
|
||||
loading={isFlushing}
|
||||
>
|
||||
{t("queuePage.flushButton")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTrigger(!trigger);
|
||||
}}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t("queuePage.refreshButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => flushQueue(trigger, setTrigger)}
|
||||
loading={isFlushing}
|
||||
>
|
||||
{t("queuePage.flushButton")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -85,6 +85,7 @@ export const getHumanReadableDuration = (ms) => {
|
||||
hours: durationObj.hours(),
|
||||
minutes: durationObj.minutes(),
|
||||
seconds: durationObj.seconds(),
|
||||
milliseconds: durationObj.milliseconds(),
|
||||
};
|
||||
|
||||
const result = [];
|
||||
@@ -101,6 +102,9 @@ export const getHumanReadableDuration = (ms) => {
|
||||
if (result.length < 2 && parts.seconds > 0) {
|
||||
result.push(`${parts.seconds}s`);
|
||||
}
|
||||
if (result.length < 2 && parts.milliseconds > 0 && parts.seconds < 1) {
|
||||
result.push(`${parts.milliseconds.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// fallback for durations < 1s
|
||||
|
||||
@@ -443,7 +443,7 @@
|
||||
"inviteNoTokenFound": "No invite token found",
|
||||
"loginHere": "Login here",
|
||||
"logsPage": {
|
||||
"description": "This page shows the latest 1000 lines of logs from the Checkmate server",
|
||||
"description": "System logs - last 1000 lines",
|
||||
"logLevelSelect": {
|
||||
"title": "Log level",
|
||||
"values": {
|
||||
@@ -456,13 +456,35 @@
|
||||
},
|
||||
"tabs": {
|
||||
"logs": "Server logs",
|
||||
"queue": "Job queue"
|
||||
"queue": "Job queue",
|
||||
"diagnostics": "Diagnostics"
|
||||
},
|
||||
"title": "Logs",
|
||||
"toast": {
|
||||
"fetchLogsSuccess": "Logs fetched successfully"
|
||||
}
|
||||
},
|
||||
"diagnosticsPage": {
|
||||
"diagnosticDescription": "System diagnostics",
|
||||
"statsDescription": "System statistics",
|
||||
"gauges": {
|
||||
"heapAllocationTitle": "Heap allocation",
|
||||
"heapAllocationSubtitle": "% of available memory",
|
||||
"heapUsageTitle": "Heap usage",
|
||||
"heapUsageSubtitle": "% of available memory",
|
||||
"heapUtilizationTitle": "Heap utilization",
|
||||
"heapUtilizationSubtitle": "% of allocated",
|
||||
"instantCpuUsageTitle": "Instant CPU usage",
|
||||
"instantCpuUsageSubtitle": "% of 1s used by CPU"
|
||||
},
|
||||
"stats": {
|
||||
"eventLoopDelayTitle": "Event loop delay",
|
||||
"uptimeTitle": "Uptime",
|
||||
"usedHeapSizeTitle": "Used heap size",
|
||||
"totalHeapSizeTitle": "Total heap size",
|
||||
"osMemoryLimitTitle": "OS Memory Limit"
|
||||
}
|
||||
},
|
||||
"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,85 @@ class DiagnosticController {
|
||||
next(handleError(error, SERVICE_NAME, "getDbStats"));
|
||||
}
|
||||
}
|
||||
|
||||
async getCPUUsage() {
|
||||
try {
|
||||
const startUsage = process.cpuUsage();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
const endUsage = process.cpuUsage(startUsage);
|
||||
const cpuUsage = {
|
||||
userUsageMs: endUsage.user / 1000,
|
||||
systemUsageMs: endUsage.system / 1000,
|
||||
usagePercentage: ((endUsage.user + endUsage.system) / 1000 / 1000) * 100,
|
||||
};
|
||||
return cpuUsage;
|
||||
} catch (error) {
|
||||
return {
|
||||
userUsageMs: 0,
|
||||
systemUsageMs: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getSystemStats = async (req, res, next) => {
|
||||
try {
|
||||
// Memory Usage
|
||||
const totalMemory = os.totalmem();
|
||||
const freeMemory = os.freemem();
|
||||
|
||||
const osStats = {
|
||||
freeMemoryBytes: freeMemory, // bytes
|
||||
totalMemoryBytes: totalMemory, // bytes
|
||||
};
|
||||
|
||||
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 cpuMetrics = await this.getCPUUsage();
|
||||
|
||||
// V8 Heap Statistics
|
||||
const heapStats = v8.getHeapStatistics();
|
||||
const v8Metrics = {
|
||||
totalHeapSizeBytes: heapStats.total_heap_size, // bytes
|
||||
usedHeapSizeBytes: heapStats.used_heap_size, // bytes
|
||||
heapSizeLimitBytes: heapStats.heap_size_limit, // bytes
|
||||
};
|
||||
|
||||
// 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 uptimeMs = process.uptime() * 1000; // ms
|
||||
|
||||
// Combine Metrics
|
||||
const diagnostics = {
|
||||
osStats,
|
||||
memoryUsage,
|
||||
cpuUsage: cpuMetrics,
|
||||
v8HeapStats: v8Metrics,
|
||||
eventLoopDelayMs: eventLoopDelay,
|
||||
uptimeMs,
|
||||
};
|
||||
|
||||
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