mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-14 05:39:50 -06:00
@@ -1,7 +1,6 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Background from "../../assets/Images/background-grid.svg?react";
|
||||
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
|
||||
import OutputAnimation from "../../assets/Animations/output.gif";
|
||||
import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif";
|
||||
import { useSelector } from "react-redux";
|
||||
@@ -13,7 +12,7 @@ const FallbackBackground = () => {
|
||||
<Box
|
||||
component="img"
|
||||
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
|
||||
Background="transparent"
|
||||
background="transparent"
|
||||
alt="Loading animation"
|
||||
sx={{
|
||||
zIndex: 1,
|
||||
|
||||
@@ -74,7 +74,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
color: theme.palette.text.secondary,
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
fontSize: "0.938rem",
|
||||
lineHeight: 1.5,
|
||||
mb: 1,
|
||||
|
||||
@@ -369,6 +369,8 @@ const useUpdateMonitor = () => {
|
||||
setIsLoading(true);
|
||||
const updatedFields = {
|
||||
name: monitor.name,
|
||||
statusWindowSize: monitor.statusWindowSize,
|
||||
statusWindowThreshold: monitor.statusWindowThreshold,
|
||||
description: monitor.description,
|
||||
interval: monitor.interval,
|
||||
notifications: monitor.notifications,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import ConfigBox from "../../../../Components/ConfigBox";
|
||||
import { Box, Stack, Typography } from "@mui/material";
|
||||
import { CustomThreshold } from "../Components/CustomThreshold";
|
||||
import { capitalizeFirstLetter } from "../../../../Utils/stringUtils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
const CustomAlertsSection = ({
|
||||
errors,
|
||||
onChange,
|
||||
infrastructureMonitor,
|
||||
handleCheckboxChange,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const METRICS = ["cpu", "memory", "disk", "temperature"];
|
||||
const METRIC_PREFIX = "usage_";
|
||||
const hasAlertError = (errors) => {
|
||||
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
|
||||
};
|
||||
const getAlertError = (errors) => {
|
||||
const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX));
|
||||
return errorKey ? errors[errorKey] : null;
|
||||
};
|
||||
return (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("infrastructureCustomizeAlerts")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("infrastructureAlertNotificationDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
{METRICS.map((metric) => {
|
||||
return (
|
||||
<CustomThreshold
|
||||
key={metric}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
errors={errors}
|
||||
checkboxId={metric}
|
||||
checkboxName={metric}
|
||||
checkboxLabel={
|
||||
metric !== "cpu" ? capitalizeFirstLetter(metric) : metric.toUpperCase()
|
||||
}
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
isChecked={infrastructureMonitor[metric]}
|
||||
fieldId={METRIC_PREFIX + metric}
|
||||
fieldName={METRIC_PREFIX + metric}
|
||||
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
|
||||
onFieldChange={onChange}
|
||||
alertUnit={metric == "temperature" ? "°C" : "%"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{hasAlertError(errors) && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{getAlertError(errors)}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
);
|
||||
};
|
||||
|
||||
CustomAlertsSection.propTypes = {
|
||||
errors: PropTypes.object.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
infrastructureMonitor: PropTypes.object.isRequired,
|
||||
handleCheckboxChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CustomAlertsSection;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useState } from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
|
||||
import Dialog from "../../../../Components/Dialog";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
alignSelf="flex-end"
|
||||
ml="auto"
|
||||
>
|
||||
<Button
|
||||
onClick={handlePause}
|
||||
loading={isBusy}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
strokeWidth: 0.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
{t("pause")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
{t("resume")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isBusy}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{ ml: theme.spacing(6) }}
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
theme={theme}
|
||||
title={t("deleteDialogTitle")}
|
||||
description={t("deleteDialogDescription")}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
confirmationButtonLabel={t("delete")}
|
||||
onConfirm={handleRemove}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorActionButtons.propTypes = {
|
||||
monitor: PropTypes.object.isRequired,
|
||||
isBusy: PropTypes.bool.isRequired,
|
||||
handlePause: PropTypes.func.isRequired,
|
||||
handleRemove: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default MonitorActionButtons;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Box, Stack, Tooltip, Typography } from "@mui/material";
|
||||
import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PulseDot from "../../../../Components/Animated/PulseDot";
|
||||
import PropTypes from "prop-types";
|
||||
const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={pagespeedStatusMsg[determineState(monitor)]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: { offset: [0, -8] },
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="monitorUrl"
|
||||
>
|
||||
{infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: theme.spacing(2),
|
||||
height: theme.spacing(2),
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.primary.contrastTextTertiary,
|
||||
opacity: 0.8,
|
||||
left: theme.spacing(-5),
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("editing")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
MonitorStatusHeader.propTypes = {
|
||||
monitor: PropTypes.object.isRequired,
|
||||
infrastructureMonitor: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default MonitorStatusHeader;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useState } from "react";
|
||||
const useInfrastructureMonitorForm = () => {
|
||||
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
notify_email: false,
|
||||
interval: 0.25,
|
||||
cpu: false,
|
||||
usage_cpu: "",
|
||||
memory: false,
|
||||
usage_memory: "",
|
||||
disk: false,
|
||||
usage_disk: "",
|
||||
temperature: false,
|
||||
usage_temperature: "",
|
||||
secret: "",
|
||||
});
|
||||
|
||||
const onChangeForm = (name, value) => {
|
||||
setInfrastructureMonitor({
|
||||
...infrastructureMonitor,
|
||||
[name]: value,
|
||||
});
|
||||
};
|
||||
const handleCheckboxChange = (event) => {
|
||||
setInfrastructureMonitor({
|
||||
...infrastructureMonitor,
|
||||
[event.target.name]: event.target.checked,
|
||||
});
|
||||
};
|
||||
const initializeInfrastructureMonitorForCreate = (globalSettings) => {
|
||||
const gt = globalSettings?.data?.settings?.globalThresholds || {};
|
||||
setInfrastructureMonitor({
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
interval: 0.25,
|
||||
cpu: gt.cpu !== undefined,
|
||||
usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "",
|
||||
memory: gt.memory !== undefined,
|
||||
usage_memory: gt.memory !== undefined ? gt.memory.toString() : "",
|
||||
disk: gt.disk !== undefined,
|
||||
usage_disk: gt.disk !== undefined ? gt.disk.toString() : "",
|
||||
temperature: gt.temperature !== undefined,
|
||||
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
|
||||
secret: "",
|
||||
});
|
||||
};
|
||||
|
||||
const initializeInfrastructureMonitorForUpdate = (monitor) => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { thresholds = {} } = monitor;
|
||||
setInfrastructureMonitor({
|
||||
url: monitor.url.replace(/^https?:\/\//, ""),
|
||||
name: monitor.name || "",
|
||||
notifications: monitor.notifications || [],
|
||||
interval: monitor.interval / MS_PER_MINUTE,
|
||||
cpu: thresholds.usage_cpu !== undefined,
|
||||
usage_cpu:
|
||||
thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "",
|
||||
memory: thresholds.usage_memory !== undefined,
|
||||
usage_memory:
|
||||
thresholds.usage_memory !== undefined
|
||||
? (thresholds.usage_memory * 100).toString()
|
||||
: "",
|
||||
disk: thresholds.usage_disk !== undefined,
|
||||
usage_disk:
|
||||
thresholds.usage_disk !== undefined
|
||||
? (thresholds.usage_disk * 100).toString()
|
||||
: "",
|
||||
temperature: thresholds.usage_temperature !== undefined,
|
||||
usage_temperature:
|
||||
thresholds.usage_temperature !== undefined
|
||||
? (thresholds.usage_temperature * 100).toString()
|
||||
: "",
|
||||
secret: monitor.secret || "",
|
||||
});
|
||||
};
|
||||
return {
|
||||
infrastructureMonitor,
|
||||
setInfrastructureMonitor,
|
||||
onChangeForm,
|
||||
handleCheckboxChange,
|
||||
initializeInfrastructureMonitorForCreate,
|
||||
initializeInfrastructureMonitorForUpdate,
|
||||
};
|
||||
};
|
||||
|
||||
export default useInfrastructureMonitorForm;
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks";
|
||||
const useInfrastructureSubmit = () => {
|
||||
const [createMonitor, isCreating] = useCreateMonitor();
|
||||
const [updateMonitor, isUpdating] = useUpdateMonitor();
|
||||
const buildForm = (infrastructureMonitor, https) => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
|
||||
let form = {
|
||||
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
|
||||
name:
|
||||
infrastructureMonitor.name === ""
|
||||
? infrastructureMonitor.url
|
||||
: infrastructureMonitor.name,
|
||||
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
|
||||
cpu: infrastructureMonitor.cpu,
|
||||
...(infrastructureMonitor.cpu
|
||||
? { usage_cpu: infrastructureMonitor.usage_cpu }
|
||||
: {}),
|
||||
memory: infrastructureMonitor.memory,
|
||||
...(infrastructureMonitor.memory
|
||||
? { usage_memory: infrastructureMonitor.usage_memory }
|
||||
: {}),
|
||||
disk: infrastructureMonitor.disk,
|
||||
...(infrastructureMonitor.disk
|
||||
? { usage_disk: infrastructureMonitor.usage_disk }
|
||||
: {}),
|
||||
temperature: infrastructureMonitor.temperature,
|
||||
...(infrastructureMonitor.temperature
|
||||
? { usage_temperature: infrastructureMonitor.usage_temperature }
|
||||
: {}),
|
||||
secret: infrastructureMonitor.secret,
|
||||
};
|
||||
return form;
|
||||
};
|
||||
const submitInfrastructureForm = async (
|
||||
infrastructureMonitor,
|
||||
form,
|
||||
isCreate,
|
||||
monitorId
|
||||
) => {
|
||||
const {
|
||||
cpu,
|
||||
usage_cpu,
|
||||
memory,
|
||||
usage_memory,
|
||||
disk,
|
||||
usage_disk,
|
||||
temperature,
|
||||
usage_temperature,
|
||||
...rest
|
||||
} = form;
|
||||
|
||||
const thresholds = {
|
||||
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
|
||||
...(memory ? { usage_memory: usage_memory / 100 } : {}),
|
||||
...(disk ? { usage_disk: usage_disk / 100 } : {}),
|
||||
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
|
||||
};
|
||||
|
||||
const finalForm = {
|
||||
...(isCreate ? {} : { _id: monitorId }),
|
||||
...rest,
|
||||
description: form.name,
|
||||
type: "hardware",
|
||||
notifications: infrastructureMonitor.notifications,
|
||||
thresholds,
|
||||
};
|
||||
// Handle create or update
|
||||
isCreate
|
||||
? await createMonitor({ monitor: finalForm, redirect: "/infrastructure" })
|
||||
: await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" });
|
||||
};
|
||||
return {
|
||||
buildForm,
|
||||
submitInfrastructureForm,
|
||||
isCreating,
|
||||
isUpdating,
|
||||
};
|
||||
};
|
||||
export default useInfrastructureSubmit;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState } from "react";
|
||||
import { infrastructureMonitorValidation } from "../../../../Validation/validation";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
const useValidateInfrastructureForm = () => {
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const validateField = (name, value) => {
|
||||
const { error } = infrastructureMonitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
|
||||
}));
|
||||
};
|
||||
|
||||
const validateForm = (form) => {
|
||||
const { error } = infrastructureMonitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
console.log(newErrors);
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Please check the form for errors." });
|
||||
return error;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return { errors, validateField, validateForm };
|
||||
};
|
||||
export default useValidateInfrastructureForm;
|
||||
@@ -1,40 +1,33 @@
|
||||
//Components
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
|
||||
import Link from "../../../Components/Link";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
import { CustomThreshold } from "./Components/CustomThreshold";
|
||||
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import MonitorStatusHeader from "./Components/MonitorStatusHeader";
|
||||
import MonitorActionButtons from "./Components/MonitorActionButtons";
|
||||
import CustomAlertsSection from "./Components/CustomAlertsSection";
|
||||
// Utils
|
||||
import NotificationsConfig from "../../../Components/NotificationConfig";
|
||||
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
|
||||
import { infrastructureMonitorValidation } from "../../../Validation/validation";
|
||||
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
|
||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useCreateMonitor,
|
||||
useDeleteMonitor,
|
||||
useFetchGlobalSettings,
|
||||
useFetchHardwareMonitorById,
|
||||
usePauseMonitor,
|
||||
useUpdateMonitor,
|
||||
} from "../../../Hooks/monitorHooks";
|
||||
import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm";
|
||||
import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm";
|
||||
import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit";
|
||||
|
||||
const CreateInfrastructureMonitor = () => {
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const { monitorId } = useParams();
|
||||
const isCreate = typeof monitorId === "undefined";
|
||||
|
||||
@@ -42,39 +35,29 @@ const CreateInfrastructureMonitor = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// State
|
||||
const [errors, setErrors] = useState({});
|
||||
const [https, setHttps] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
notify_email: false,
|
||||
interval: 0.25,
|
||||
cpu: false,
|
||||
usage_cpu: "",
|
||||
memory: false,
|
||||
usage_memory: "",
|
||||
disk: false,
|
||||
usage_disk: "",
|
||||
temperature: false,
|
||||
usage_temperature: "",
|
||||
secret: "",
|
||||
});
|
||||
|
||||
// Fetch monitor details if editing
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
|
||||
const [monitor, isLoading] = useFetchHardwareMonitorById({
|
||||
monitorId,
|
||||
updateTrigger,
|
||||
});
|
||||
const [createMonitor, isCreating] = useCreateMonitor();
|
||||
const [deleteMonitor, isDeleting] = useDeleteMonitor();
|
||||
const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
|
||||
const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId();
|
||||
const [pauseMonitor, isPausing] = usePauseMonitor();
|
||||
const [updateMonitor, isUpdating] = useUpdateMonitor();
|
||||
const {
|
||||
infrastructureMonitor,
|
||||
setInfrastructureMonitor,
|
||||
onChangeForm,
|
||||
handleCheckboxChange,
|
||||
initializeInfrastructureMonitorForCreate,
|
||||
initializeInfrastructureMonitorForUpdate,
|
||||
} = useInfrastructureMonitorForm();
|
||||
const { errors, validateField, validateForm } = useValidateInfrastructureForm();
|
||||
const { buildForm, submitInfrastructureForm, isCreating, isUpdating } =
|
||||
useInfrastructureSubmit();
|
||||
|
||||
const FREQUENCIES = [
|
||||
{ _id: 0.25, name: t("time.fifteenSeconds") },
|
||||
@@ -93,157 +76,27 @@ const CreateInfrastructureMonitor = () => {
|
||||
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
|
||||
]),
|
||||
];
|
||||
const METRICS = ["cpu", "memory", "disk", "temperature"];
|
||||
const METRIC_PREFIX = "usage_";
|
||||
const MS_PER_MINUTE = 60000;
|
||||
|
||||
const hasAlertError = (errors) => {
|
||||
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
|
||||
};
|
||||
|
||||
const getAlertError = (errors) => {
|
||||
const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX));
|
||||
return errorKey ? errors[errorKey] : null;
|
||||
};
|
||||
|
||||
// Populate form fields if editing
|
||||
useEffect(() => {
|
||||
if (isCreate) {
|
||||
if (globalSettingsLoading) return;
|
||||
|
||||
const gt = globalSettings?.data?.settings?.globalThresholds || {};
|
||||
|
||||
setHttps(false);
|
||||
|
||||
setInfrastructureMonitor({
|
||||
url: "",
|
||||
name: "",
|
||||
notifications: [],
|
||||
interval: 0.25,
|
||||
cpu: gt.cpu !== undefined,
|
||||
usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "",
|
||||
memory: gt.memory !== undefined,
|
||||
usage_memory: gt.memory !== undefined ? gt.memory.toString() : "",
|
||||
disk: gt.disk !== undefined,
|
||||
usage_disk: gt.disk !== undefined ? gt.disk.toString() : "",
|
||||
temperature: gt.temperature !== undefined,
|
||||
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
|
||||
secret: "",
|
||||
});
|
||||
initializeInfrastructureMonitorForCreate(globalSettings);
|
||||
} else if (monitor) {
|
||||
const { thresholds = {} } = monitor;
|
||||
|
||||
setHttps(monitor.url.startsWith("https"));
|
||||
|
||||
setInfrastructureMonitor({
|
||||
url: monitor.url.replace(/^https?:\/\//, ""),
|
||||
name: monitor.name || "",
|
||||
notifications: monitor.notifications || [],
|
||||
interval: monitor.interval / MS_PER_MINUTE,
|
||||
cpu: thresholds.usage_cpu !== undefined,
|
||||
usage_cpu:
|
||||
thresholds.usage_cpu !== undefined
|
||||
? (thresholds.usage_cpu * 100).toString()
|
||||
: "",
|
||||
memory: thresholds.usage_memory !== undefined,
|
||||
usage_memory:
|
||||
thresholds.usage_memory !== undefined
|
||||
? (thresholds.usage_memory * 100).toString()
|
||||
: "",
|
||||
disk: thresholds.usage_disk !== undefined,
|
||||
usage_disk:
|
||||
thresholds.usage_disk !== undefined
|
||||
? (thresholds.usage_disk * 100).toString()
|
||||
: "",
|
||||
temperature: thresholds.usage_temperature !== undefined,
|
||||
usage_temperature:
|
||||
thresholds.usage_temperature !== undefined
|
||||
? (thresholds.usage_temperature * 100).toString()
|
||||
: "",
|
||||
secret: monitor.secret || "",
|
||||
});
|
||||
initializeInfrastructureMonitorForUpdate(monitor);
|
||||
}
|
||||
}, [isCreate, monitor, globalSettings, globalSettingsLoading]);
|
||||
|
||||
// Handlers
|
||||
const onSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// Build the form
|
||||
let form = {
|
||||
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
|
||||
name:
|
||||
infrastructureMonitor.name === ""
|
||||
? infrastructureMonitor.url
|
||||
: infrastructureMonitor.name,
|
||||
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
|
||||
cpu: infrastructureMonitor.cpu,
|
||||
...(infrastructureMonitor.cpu
|
||||
? { usage_cpu: infrastructureMonitor.usage_cpu }
|
||||
: {}),
|
||||
memory: infrastructureMonitor.memory,
|
||||
...(infrastructureMonitor.memory
|
||||
? { usage_memory: infrastructureMonitor.usage_memory }
|
||||
: {}),
|
||||
disk: infrastructureMonitor.disk,
|
||||
...(infrastructureMonitor.disk
|
||||
? { usage_disk: infrastructureMonitor.usage_disk }
|
||||
: {}),
|
||||
temperature: infrastructureMonitor.temperature,
|
||||
...(infrastructureMonitor.temperature
|
||||
? { usage_temperature: infrastructureMonitor.usage_temperature }
|
||||
: {}),
|
||||
secret: infrastructureMonitor.secret,
|
||||
};
|
||||
|
||||
const { error } = infrastructureMonitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
const form = buildForm(infrastructureMonitor, https);
|
||||
const error = validateForm(form);
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
console.log(newErrors);
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Please check the form for errors." });
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the thresholds for the form
|
||||
const {
|
||||
cpu,
|
||||
usage_cpu,
|
||||
memory,
|
||||
usage_memory,
|
||||
disk,
|
||||
usage_disk,
|
||||
temperature,
|
||||
usage_temperature,
|
||||
...rest
|
||||
} = form;
|
||||
|
||||
const thresholds = {
|
||||
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
|
||||
...(memory ? { usage_memory: usage_memory / 100 } : {}),
|
||||
...(disk ? { usage_disk: usage_disk / 100 } : {}),
|
||||
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
|
||||
};
|
||||
|
||||
form = {
|
||||
...(isCreate ? {} : { _id: monitorId }),
|
||||
...rest,
|
||||
description: form.name,
|
||||
type: "hardware",
|
||||
notifications: infrastructureMonitor.notifications,
|
||||
thresholds,
|
||||
};
|
||||
|
||||
// Handle create or update
|
||||
isCreate
|
||||
? await createMonitor({ monitor: form, redirect: "/infrastructure" })
|
||||
: await updateMonitor({ monitor: form, redirect: "/infrastructure" });
|
||||
submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId);
|
||||
};
|
||||
|
||||
const triggerUpdate = () => {
|
||||
@@ -252,28 +105,8 @@ const CreateInfrastructureMonitor = () => {
|
||||
|
||||
const onChange = (event) => {
|
||||
const { value, name } = event.target;
|
||||
setInfrastructureMonitor({
|
||||
...infrastructureMonitor,
|
||||
[name]: value,
|
||||
});
|
||||
|
||||
const { error } = infrastructureMonitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (event) => {
|
||||
const { name } = event.target;
|
||||
const { checked } = event.target;
|
||||
setInfrastructureMonitor({
|
||||
...infrastructureMonitor,
|
||||
[name]: checked,
|
||||
});
|
||||
onChangeForm(name, value);
|
||||
validateField(name, value);
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
@@ -336,105 +169,19 @@ const CreateInfrastructureMonitor = () => {
|
||||
)}
|
||||
</Typography>
|
||||
{!isCreate && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={pagespeedStatusMsg[determineState(monitor)]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: { offset: [0, -8] },
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="monitorUrl"
|
||||
>
|
||||
{infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: theme.spacing(2),
|
||||
height: theme.spacing(2),
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.primary.contrastTextTertiary,
|
||||
opacity: 0.8,
|
||||
left: theme.spacing(-5),
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("editing")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<MonitorStatusHeader
|
||||
monitor={monitor}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{!isCreate && (
|
||||
<Box
|
||||
alignSelf="flex-end"
|
||||
ml="auto"
|
||||
>
|
||||
<Button
|
||||
onClick={handlePause}
|
||||
loading={isBusy}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
strokeWidth: 0.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
{t("pause")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
{t("resume")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isBusy}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{ ml: theme.spacing(6) }}
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
</Box>
|
||||
<MonitorActionButtons
|
||||
monitor={monitor}
|
||||
isBusy={isBusy}
|
||||
handlePause={handlePause}
|
||||
handleRemove={handleRemove}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<ConfigBox>
|
||||
@@ -534,58 +281,12 @@ const CreateInfrastructureMonitor = () => {
|
||||
setNotifications={infrastructureMonitor.notifications}
|
||||
/>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("infrastructureCustomizeAlerts")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("infrastructureAlertNotificationDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
{METRICS.map((metric) => {
|
||||
return (
|
||||
<CustomThreshold
|
||||
key={metric}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
errors={errors}
|
||||
checkboxId={metric}
|
||||
checkboxName={metric}
|
||||
checkboxLabel={
|
||||
metric !== "cpu"
|
||||
? capitalizeFirstLetter(metric)
|
||||
: metric.toUpperCase()
|
||||
}
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
isChecked={infrastructureMonitor[metric]}
|
||||
fieldId={METRIC_PREFIX + metric}
|
||||
fieldName={METRIC_PREFIX + metric}
|
||||
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
|
||||
onFieldChange={onChange}
|
||||
alertUnit={metric == "temperature" ? "°C" : "%"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{/* Error text */}
|
||||
{hasAlertError(errors) && (
|
||||
<Typography
|
||||
component="span"
|
||||
className="input-error"
|
||||
color={theme.palette.error.main}
|
||||
mt={theme.spacing(2)}
|
||||
sx={{
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
{getAlertError(errors)}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<CustomAlertsSection
|
||||
errors={errors}
|
||||
onChange={onChange}
|
||||
infrastructureMonitor={infrastructureMonitor}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
@@ -620,17 +321,6 @@ const CreateInfrastructureMonitor = () => {
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{!isCreate && (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
theme={theme}
|
||||
title={t("deleteDialogTitle")}
|
||||
description={t("deleteDialogDescription")}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
confirmationButtonLabel={t("delete")}
|
||||
onConfirm={handleRemove}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useState, useEffect } from "react";
|
||||
import { FormControl, InputLabel, Select, MenuItem, Box } from "@mui/material";
|
||||
import { Box } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Select from "../../../../../Components/Inputs/Select";
|
||||
import NetworkStatBoxes from "./NetworkStatBoxes";
|
||||
import NetworkCharts from "./NetworkCharts";
|
||||
import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader";
|
||||
@@ -63,27 +64,17 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => {
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
{availableInterfaces.length > 0 && (
|
||||
<FormControl
|
||||
variant="outlined"
|
||||
size="small"
|
||||
<Select
|
||||
name="networkInterface"
|
||||
label={t("networkInterface")}
|
||||
value={selectedInterface}
|
||||
onChange={(e) => setSelectedInterface(e.target.value)}
|
||||
items={availableInterfaces.map((interfaceName) => ({
|
||||
_id: interfaceName,
|
||||
name: interfaceName,
|
||||
}))}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
<InputLabel>{t("networkInterface")}</InputLabel>
|
||||
<Select
|
||||
value={selectedInterface}
|
||||
onChange={(e) => setSelectedInterface(e.target.value)}
|
||||
label={t("networkInterface")}
|
||||
>
|
||||
{availableInterfaces.map((interfaceName) => (
|
||||
<MenuItem
|
||||
key={interfaceName}
|
||||
value={interfaceName}
|
||||
>
|
||||
{interfaceName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
/>
|
||||
)}
|
||||
<MonitorTimeFrameHeader
|
||||
isLoading={isLoading}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import Select from "../../../../../Components/Inputs/Select";
|
||||
import PropTypes from "prop-types";
|
||||
const ConfigSelect = ({ configSelection, valueSelect, onChange, ...props }) => {
|
||||
const getValueById = (config, id) => {
|
||||
const item = config.find((config) => config._id === id);
|
||||
return item ? (item.value ? item.value : item.name) : null;
|
||||
};
|
||||
|
||||
const getIdByValue = (config, name) => {
|
||||
const item = config.find((config) => {
|
||||
if (config.value) {
|
||||
return config.value === name;
|
||||
} else {
|
||||
return config.name === name;
|
||||
}
|
||||
});
|
||||
return item ? item._id : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={getIdByValue(configSelection, valueSelect)}
|
||||
onChange={(event) => {
|
||||
const newValue = getValueById(configSelection, event.target.value);
|
||||
onChange(newValue);
|
||||
}}
|
||||
items={configSelection}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigSelect.propTypes = {
|
||||
configSelection: PropTypes.array.isRequired,
|
||||
valueSelect: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
export default ConfigSelect;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { networkService } from "../../../../main";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
MS_PER_SECOND,
|
||||
MS_PER_MINUTE,
|
||||
MS_PER_HOUR,
|
||||
MS_PER_DAY,
|
||||
MS_PER_WEEK,
|
||||
} from "../../../../Utils/timeUtils";
|
||||
|
||||
const useMaintenanceActions = () => {
|
||||
const MS_LOOKUP = {
|
||||
seconds: MS_PER_SECOND,
|
||||
minutes: MS_PER_MINUTE,
|
||||
hours: MS_PER_HOUR,
|
||||
days: MS_PER_DAY,
|
||||
weeks: MS_PER_WEEK,
|
||||
};
|
||||
const REPEAT_LOOKUP = {
|
||||
none: 0,
|
||||
daily: MS_PER_DAY,
|
||||
weekly: MS_PER_DAY * 7,
|
||||
};
|
||||
const handleSubmitForm = async (maintenanceWindowId, form) => {
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
.set("minute", form.startTime.minute());
|
||||
|
||||
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
|
||||
const durationInMs = form.duration * MS_MULTIPLIER;
|
||||
const end = start.add(durationInMs);
|
||||
|
||||
const repeat = REPEAT_LOOKUP[form.repeat];
|
||||
|
||||
const submit = {
|
||||
monitors: form.monitors.map((monitor) => monitor._id),
|
||||
name: form.name,
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
repeat,
|
||||
};
|
||||
|
||||
if (repeat === 0) {
|
||||
submit.expiry = end;
|
||||
}
|
||||
|
||||
const requestConfig = { maintenanceWindow: submit };
|
||||
|
||||
if (maintenanceWindowId !== undefined) {
|
||||
requestConfig.maintenanceWindowId = maintenanceWindowId;
|
||||
}
|
||||
const request =
|
||||
maintenanceWindowId === undefined
|
||||
? networkService.createMaintenanceWindow(requestConfig)
|
||||
: networkService.editMaintenanceWindow(requestConfig);
|
||||
return request;
|
||||
};
|
||||
|
||||
return {
|
||||
handleSubmitForm,
|
||||
};
|
||||
};
|
||||
|
||||
export default useMaintenanceActions;
|
||||
@@ -0,0 +1,74 @@
|
||||
import { networkService } from "../../../../main";
|
||||
import dayjs from "dayjs";
|
||||
import {
|
||||
MS_PER_SECOND,
|
||||
MS_PER_MINUTE,
|
||||
MS_PER_HOUR,
|
||||
MS_PER_DAY,
|
||||
MS_PER_WEEK,
|
||||
} from "../../../../Utils/timeUtils";
|
||||
const useMaintenanceData = () => {
|
||||
const REVERSE_REPEAT_LOOKUP = {
|
||||
0: "none",
|
||||
[MS_PER_DAY]: "daily",
|
||||
[MS_PER_WEEK]: "weekly",
|
||||
};
|
||||
const getDurationAndUnit = (durationInMs) => {
|
||||
if (durationInMs % MS_PER_DAY === 0) {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_DAY).toString(),
|
||||
durationUnit: "days",
|
||||
};
|
||||
} else if (durationInMs % MS_PER_HOUR === 0) {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_HOUR).toString(),
|
||||
durationUnit: "hours",
|
||||
};
|
||||
} else if (durationInMs % MS_PER_MINUTE === 0) {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_MINUTE).toString(),
|
||||
durationUnit: "minutes",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_SECOND).toString(),
|
||||
durationUnit: "seconds",
|
||||
};
|
||||
}
|
||||
};
|
||||
const fetchMonitorsMaintenance = async () => {
|
||||
const response = await networkService.getMonitorsByTeamId({
|
||||
limit: null,
|
||||
types: ["http", "ping", "pagespeed", "port"],
|
||||
});
|
||||
const fetchedMonitors = response.data.data.monitors;
|
||||
return fetchedMonitors;
|
||||
};
|
||||
|
||||
const initializeMaintenanceForEdit = async (maintenanceWindowId, monitorList) => {
|
||||
const res = await networkService.getMaintenanceWindowById({
|
||||
maintenanceWindowId: maintenanceWindowId,
|
||||
});
|
||||
const maintenanceWindow = res.data.data;
|
||||
const { name, start, end, repeat, monitorId } = maintenanceWindow;
|
||||
const startTime = dayjs(start);
|
||||
const endTime = dayjs(end);
|
||||
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
|
||||
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
|
||||
const monitor = monitorList.find((monitor) => monitor._id === monitorId);
|
||||
const maintenanceWindowInformation = {
|
||||
name,
|
||||
repeat: REVERSE_REPEAT_LOOKUP[repeat],
|
||||
startDate: startTime,
|
||||
startTime,
|
||||
duration,
|
||||
durationUnit,
|
||||
monitors: monitor ? [monitor] : [],
|
||||
};
|
||||
return maintenanceWindowInformation;
|
||||
};
|
||||
|
||||
return { fetchMonitorsMaintenance, initializeMaintenanceForEdit };
|
||||
};
|
||||
|
||||
export default useMaintenanceData;
|
||||
@@ -10,71 +10,21 @@ import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
|
||||
import { maintenanceWindowValidation } from "../../../Validation/validation";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import MonitorList from "./Components/MonitorList";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import ConfigSelect from "./Components/ConfigSelect";
|
||||
import useMaintenanceData from "./hooks/useMaintenanceData";
|
||||
import useMaintenanceActions from "./hooks/useMaintenanceActions";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import CalendarIcon from "../../../assets/icons/calendar.svg?react";
|
||||
import "./index.css";
|
||||
import Search from "../../../Components/Inputs/Search";
|
||||
import { networkService } from "../../../main";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import {
|
||||
MS_PER_SECOND,
|
||||
MS_PER_MINUTE,
|
||||
MS_PER_HOUR,
|
||||
MS_PER_DAY,
|
||||
MS_PER_WEEK,
|
||||
} from "../../../Utils/timeUtils";
|
||||
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const getDurationAndUnit = (durationInMs) => {
|
||||
if (durationInMs % MS_PER_DAY === 0) {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_DAY).toString(),
|
||||
durationUnit: "days",
|
||||
};
|
||||
} else if (durationInMs % MS_PER_HOUR === 0) {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_HOUR).toString(),
|
||||
durationUnit: "hours",
|
||||
};
|
||||
} else if (durationInMs % MS_PER_MINUTE === 0) {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_MINUTE).toString(),
|
||||
durationUnit: "minutes",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
duration: (durationInMs / MS_PER_SECOND).toString(),
|
||||
durationUnit: "seconds",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const MS_LOOKUP = {
|
||||
seconds: MS_PER_SECOND,
|
||||
minutes: MS_PER_MINUTE,
|
||||
hours: MS_PER_HOUR,
|
||||
days: MS_PER_DAY,
|
||||
weeks: MS_PER_WEEK,
|
||||
};
|
||||
|
||||
const REPEAT_LOOKUP = {
|
||||
none: 0,
|
||||
daily: MS_PER_DAY,
|
||||
weekly: MS_PER_DAY * 7,
|
||||
};
|
||||
|
||||
const REVERSE_REPEAT_LOOKUP = {
|
||||
0: "none",
|
||||
[MS_PER_DAY]: "daily",
|
||||
[MS_PER_WEEK]: "weekly",
|
||||
};
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const repeatConfig = [
|
||||
{ _id: 0, name: "Don't repeat", value: "none" },
|
||||
@@ -96,24 +46,10 @@ const durationConfig = [
|
||||
},
|
||||
];
|
||||
|
||||
const getValueById = (config, id) => {
|
||||
const item = config.find((config) => config._id === id);
|
||||
return item ? (item.value ? item.value : item.name) : null;
|
||||
};
|
||||
|
||||
const getIdByValue = (config, name) => {
|
||||
const item = config.find((config) => {
|
||||
if (config.value) {
|
||||
return config.value === name;
|
||||
} else {
|
||||
return config.name === name;
|
||||
}
|
||||
});
|
||||
return item ? item._id : null;
|
||||
};
|
||||
|
||||
const CreateMaintenance = () => {
|
||||
const { maintenanceWindowId } = useParams();
|
||||
const { handleSubmitForm } = useMaintenanceActions();
|
||||
const { fetchMonitorsMaintenance, initializeMaintenanceForEdit } = useMaintenanceData();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -121,6 +57,7 @@ const CreateMaintenance = () => {
|
||||
const [monitors, setMonitors] = useState([]);
|
||||
const [search, setSearch] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState({});
|
||||
const [form, setForm] = useState({
|
||||
repeat: "none",
|
||||
startDate: dayjs(),
|
||||
@@ -130,43 +67,35 @@ const CreateMaintenance = () => {
|
||||
name: "",
|
||||
monitors: [],
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
const handleFormChange = (key, value) => {
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ [key]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, key, error);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMonitors = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await networkService.getMonitorsByTeamId({
|
||||
limit: null,
|
||||
types: ["http", "ping", "pagespeed", "port"],
|
||||
});
|
||||
const monitors = response.data.data.monitors;
|
||||
setMonitors(monitors);
|
||||
const fetchedMonitors = await fetchMonitorsMaintenance();
|
||||
setMonitors(fetchedMonitors);
|
||||
|
||||
if (maintenanceWindowId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await networkService.getMaintenanceWindowById({
|
||||
maintenanceWindowId: maintenanceWindowId,
|
||||
});
|
||||
const maintenanceWindow = res.data.data;
|
||||
const { name, start, end, repeat, monitorId } = maintenanceWindow;
|
||||
const startTime = dayjs(start);
|
||||
const endTime = dayjs(end);
|
||||
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
|
||||
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
|
||||
const monitor = monitors.find((monitor) => monitor._id === monitorId);
|
||||
setForm({
|
||||
...form,
|
||||
name,
|
||||
repeat: REVERSE_REPEAT_LOOKUP[repeat],
|
||||
startDate: startTime,
|
||||
startTime,
|
||||
duration,
|
||||
durationUnit,
|
||||
monitors: monitor ? [monitor] : [],
|
||||
});
|
||||
const maintenanceWindowInformation = await initializeMaintenanceForEdit(
|
||||
maintenanceWindowId,
|
||||
fetchedMonitors
|
||||
);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
...maintenanceWindowInformation,
|
||||
}));
|
||||
} catch (error) {
|
||||
createToast({ body: "Failed to fetch data" });
|
||||
logger.error("Failed to fetch monitors", error);
|
||||
@@ -177,90 +106,9 @@ const CreateMaintenance = () => {
|
||||
fetchMonitors();
|
||||
}, [user]);
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleSelectMonitors = (monitors) => {
|
||||
setForm({ ...form, monitors });
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ monitors },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, "monitors", error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleFormChange = (key, value) => {
|
||||
setForm({ ...form, [key]: value });
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ [key]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, key, error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = (key, newTime) => {
|
||||
setForm({ ...form, [key]: newTime });
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ [key]: newTime },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, key, error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleMonitorsChange = (selected) => {
|
||||
setForm((prev) => ({ ...prev, monitors: selected }));
|
||||
const { error } = maintenanceWindowValidation.validate(
|
||||
{ monitors: selected },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, "monitors", error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return;
|
||||
// Build timestamp for maintenance window from startDate and startTime
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
.set("minute", form.startTime.minute());
|
||||
// Build end timestamp for maintenance window
|
||||
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
|
||||
const durationInMs = form.duration * MS_MULTIPLIER;
|
||||
const end = start.add(durationInMs);
|
||||
|
||||
// Get repeat value in milliseconds
|
||||
const repeat = REPEAT_LOOKUP[form.repeat];
|
||||
|
||||
const submit = {
|
||||
monitors: form.monitors.map((monitor) => monitor._id),
|
||||
name: form.name,
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
repeat,
|
||||
};
|
||||
|
||||
if (repeat === 0) {
|
||||
submit.expiry = end;
|
||||
}
|
||||
|
||||
const requestConfig = { maintenanceWindow: submit };
|
||||
|
||||
if (maintenanceWindowId !== undefined) {
|
||||
requestConfig.maintenanceWindowId = maintenanceWindowId;
|
||||
}
|
||||
const request =
|
||||
maintenanceWindowId === undefined
|
||||
? networkService.createMaintenanceWindow(requestConfig)
|
||||
: networkService.editMaintenanceWindow(requestConfig);
|
||||
|
||||
const request = handleSubmitForm(maintenanceWindowId, form);
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await request;
|
||||
@@ -353,18 +201,13 @@ const CreateMaintenance = () => {
|
||||
error={errors["name"] ? true : false}
|
||||
helperText={errors["name"]}
|
||||
/>
|
||||
<Select
|
||||
<ConfigSelect
|
||||
id="repeat"
|
||||
name="maintenance-repeat"
|
||||
label={t("maintenanceRepeat")}
|
||||
value={getIdByValue(repeatConfig, form.repeat)}
|
||||
onChange={(event) => {
|
||||
handleFormChange(
|
||||
"repeat",
|
||||
getValueById(repeatConfig, event.target.value)
|
||||
);
|
||||
}}
|
||||
items={repeatConfig}
|
||||
valueSelect={form.repeat}
|
||||
configSelection={repeatConfig}
|
||||
onChange={(value) => handleFormChange("repeat", value)}
|
||||
/>
|
||||
<Stack>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
@@ -437,7 +280,7 @@ const CreateMaintenance = () => {
|
||||
}}
|
||||
sx={{}}
|
||||
onChange={(newDate) => {
|
||||
handleTimeChange("startDate", newDate);
|
||||
handleFormChange("startDate", newDate);
|
||||
}}
|
||||
error={errors["startDate"]}
|
||||
/>
|
||||
@@ -453,7 +296,7 @@ const CreateMaintenance = () => {
|
||||
>
|
||||
{t("startTime")}
|
||||
</Typography>
|
||||
<Typography>{t("timeZoneInfo")}</Typography>
|
||||
<Typography> {t("timeZoneInfo")} </Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
@@ -462,7 +305,7 @@ const CreateMaintenance = () => {
|
||||
label={t("startTime")}
|
||||
value={form.startTime}
|
||||
onChange={(newTime) => {
|
||||
handleTimeChange("startTime", newTime);
|
||||
handleFormChange("startTime", newTime);
|
||||
}}
|
||||
slotProps={{
|
||||
nextIconButton: { sx: { ml: theme.spacing(2) } },
|
||||
@@ -508,16 +351,11 @@ const CreateMaintenance = () => {
|
||||
error={errors["duration"] ? true : false}
|
||||
helperText={errors["duration"]}
|
||||
/>
|
||||
<Select
|
||||
<ConfigSelect
|
||||
id="durationUnit"
|
||||
value={getIdByValue(durationConfig, form.durationUnit)}
|
||||
items={durationConfig}
|
||||
onChange={(event) => {
|
||||
handleFormChange(
|
||||
"durationUnit",
|
||||
getValueById(durationConfig, event.target.value)
|
||||
);
|
||||
}}
|
||||
valueSelect={form.durationUnit}
|
||||
configSelection={durationConfig}
|
||||
onChange={(value) => handleFormChange("durationUnit", value)}
|
||||
error={errors["durationUnit"]}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -545,7 +383,9 @@ const CreateMaintenance = () => {
|
||||
inputValue={search}
|
||||
value={form.monitors}
|
||||
handleInputChange={setSearch}
|
||||
handleChange={handleMonitorsChange}
|
||||
handleChange={(selected) => {
|
||||
handleFormChange("monitors", selected);
|
||||
}}
|
||||
error={errors["monitors"]}
|
||||
/>
|
||||
<MonitorList
|
||||
|
||||
@@ -89,8 +89,6 @@ const CreateNotifications = () => {
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
console.log(JSON.stringify(newErrors));
|
||||
console.log(JSON.stringify(form, null, 2));
|
||||
createToast({ body: Object.values(newErrors)[0] });
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
|
||||
@@ -9,7 +9,7 @@ export const NOTIFICATION_TYPES = [
|
||||
export const TITLE_MAP = {
|
||||
email: "createNotifications.emailSettings.title",
|
||||
slack: "createNotifications.slackSettings.title",
|
||||
pager_duty: "createNotifications.pagerDutySettings.title",
|
||||
pager_duty: "createNotifications.pagerdutySettings.title",
|
||||
webhook: "createNotifications.webhookSettings.title",
|
||||
discord: "createNotifications.discordSettings.title",
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export const TITLE_MAP = {
|
||||
export const DESCRIPTION_MAP = {
|
||||
email: "createNotifications.emailSettings.description",
|
||||
slack: "createNotifications.slackSettings.description",
|
||||
pager_duty: "createNotifications.pagerDutySettings.description",
|
||||
pager_duty: "createNotifications.pagerdutySettings.description",
|
||||
webhook: "createNotifications.webhookSettings.description",
|
||||
discord: "createNotifications.discordSettings.description",
|
||||
};
|
||||
@@ -25,7 +25,7 @@ export const DESCRIPTION_MAP = {
|
||||
export const LABEL_MAP = {
|
||||
email: "createNotifications.emailSettings.emailLabel",
|
||||
slack: "createNotifications.slackSettings.webhookLabel",
|
||||
pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel",
|
||||
pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel",
|
||||
webhook: "createNotifications.webhookSettings.webhookLabel",
|
||||
discord: "createNotifications.discordSettings.webhookLabel",
|
||||
};
|
||||
@@ -33,7 +33,7 @@ export const LABEL_MAP = {
|
||||
export const PLACEHOLDER_MAP = {
|
||||
email: "createNotifications.emailSettings.emailPlaceholder",
|
||||
slack: "createNotifications.slackSettings.webhookPlaceholder",
|
||||
pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder",
|
||||
pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder",
|
||||
webhook: "createNotifications.webhookSettings.webhookPlaceholder",
|
||||
discord: "createNotifications.discordSettings.webhookPlaceholder",
|
||||
};
|
||||
|
||||
@@ -152,7 +152,7 @@ const PageSpeedSetup = () => {
|
||||
});
|
||||
|
||||
const { error } = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ [name]: value, type: monitor.type },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
setErrors((prev) => ({
|
||||
|
||||
@@ -29,7 +29,7 @@ const PieChartLegend = ({ audits }) => {
|
||||
: theme.palette.tertiary.main;
|
||||
|
||||
// Find the position where the number ends and the unit begins
|
||||
const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/);
|
||||
const match = audit?.displayValue?.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/);
|
||||
let value;
|
||||
let unit;
|
||||
if (match) {
|
||||
|
||||
@@ -44,20 +44,6 @@ import {
|
||||
useFetchMonitorGames,
|
||||
} from "../../../Hooks/monitorHooks";
|
||||
|
||||
/**
|
||||
* Parses a URL string and returns a URL object.
|
||||
*
|
||||
* @param {string} url - The URL string to parse.
|
||||
* @returns {URL} - The parsed URL object if valid, otherwise an empty string.
|
||||
*/
|
||||
const parseUrl = (url) => {
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create page renders monitor creation or configuration views.
|
||||
* @component
|
||||
@@ -69,6 +55,8 @@ const UptimeCreate = ({ isClone = false }) => {
|
||||
// States
|
||||
const [monitor, setMonitor] = useState({
|
||||
type: "http",
|
||||
statusWindowSize: 5,
|
||||
statusWindowThreshold: 60,
|
||||
matchMethod: "equal",
|
||||
expectedValue: "",
|
||||
jsonPath: "",
|
||||
@@ -191,7 +179,10 @@ const UptimeCreate = ({ isClone = false }) => {
|
||||
? `http${https ? "s" : ""}://` + monitor.url
|
||||
: monitor.url,
|
||||
name: monitor.name || monitor.url.substring(0, 50),
|
||||
statusWindowSize: monitor.statusWindowSize,
|
||||
statusWindowThreshold: monitor.statusWindowThreshold,
|
||||
type: monitor.type,
|
||||
|
||||
port:
|
||||
monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined,
|
||||
interval: monitor.interval,
|
||||
@@ -206,6 +197,8 @@ const UptimeCreate = ({ isClone = false }) => {
|
||||
_id: monitor._id,
|
||||
url: monitor.url,
|
||||
name: monitor.name || monitor.url.substring(0, 50),
|
||||
statusWindowSize: monitor.statusWindowSize,
|
||||
statusWindowThreshold: monitor.statusWindowThreshold,
|
||||
type: monitor.type,
|
||||
matchMethod: monitor.matchMethod,
|
||||
expectedValue: monitor.expectedValue,
|
||||
@@ -299,7 +292,7 @@ const UptimeCreate = ({ isClone = false }) => {
|
||||
|
||||
const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing;
|
||||
const displayInterval = monitor?.interval / MS_PER_MINUTE || 1;
|
||||
const parsedUrl = parseUrl(monitor?.url);
|
||||
const parsedUrl = monitor?.url;
|
||||
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -616,6 +609,39 @@ const UptimeCreate = ({ isClone = false }) => {
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("createMonitorPage.incidentConfigTitle")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("createMonitorPage.incidentConfigDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<TextInput
|
||||
name="statusWindowSize"
|
||||
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
|
||||
type="number"
|
||||
value={monitor.statusWindowSize}
|
||||
onChange={onChange}
|
||||
error={errors["statusWindowSize"] ? true : false}
|
||||
helperText={errors["statusWindowSize"]}
|
||||
/>
|
||||
<TextInput
|
||||
name="statusWindowThreshold"
|
||||
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
|
||||
type="number"
|
||||
value={monitor.statusWindowThreshold}
|
||||
onChange={onChange}
|
||||
error={errors["statusWindowThreshold"] ? true : false}
|
||||
helperText={errors["statusWindowThreshold"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
|
||||
@@ -564,6 +564,10 @@ const baseTheme = (palette) => ({
|
||||
"& .MuiSelect-icon": {
|
||||
color: theme.palette.primary.contrastTextSecondary, // Dropdown + color
|
||||
},
|
||||
"& .MuiSelect-select": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.main, // Background on hover
|
||||
},
|
||||
|
||||
@@ -115,6 +115,16 @@ const monitorValidation = joi.object({
|
||||
_id: joi.string(),
|
||||
userId: joi.string(),
|
||||
teamId: joi.string(),
|
||||
statusWindowSize: joi.number().min(1).max(20).default(5).messages({
|
||||
"number.base": "Status window size must be a number.",
|
||||
"number.min": "Status window size must be at least 1.",
|
||||
"number.max": "Status window size must be at most 20.",
|
||||
}),
|
||||
statusWindowThreshold: joi.number().min(1).max(100).default(60).messages({
|
||||
"number.base": "Incident percentage must be a number.",
|
||||
"number.min": "Incident percentage must be at least 1.",
|
||||
"number.max": "Incident percentage must be at most 100.",
|
||||
}),
|
||||
url: joi.when("type", {
|
||||
is: "docker",
|
||||
then: joi
|
||||
@@ -150,6 +160,11 @@ const monitorValidation = joi.object({
|
||||
// can be replaced by a shortest alternative
|
||||
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
|
||||
"(?:" +
|
||||
// Single hostname without dots (like localhost)
|
||||
"[a-z0-9\\u00a1-\\uffff][a-z0-9\\u00a1-\\uffff_-]{0,62}" +
|
||||
"|" +
|
||||
// Domain with dots
|
||||
"(?:" +
|
||||
"(?:" +
|
||||
"[a-z0-9\\u00a1-\\uffff]" +
|
||||
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
|
||||
@@ -159,6 +174,7 @@ const monitorValidation = joi.object({
|
||||
// TLD identifier name, may end with dot
|
||||
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
|
||||
")" +
|
||||
")" +
|
||||
// port number (optional)
|
||||
"(?::\\d{2,5})?" +
|
||||
// resource path (optional)
|
||||
@@ -439,21 +455,35 @@ const notificationValidation = joi.object({
|
||||
}),
|
||||
|
||||
address: joi.when("type", {
|
||||
is: "email",
|
||||
then: joi
|
||||
.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required()
|
||||
.messages({
|
||||
"string.empty": "E-mail address cannot be empty",
|
||||
"any.required": "E-mail address is required",
|
||||
"string.email": "Please enter a valid e-mail address",
|
||||
}),
|
||||
otherwise: joi.string().uri().required().messages({
|
||||
"string.empty": "Webhook URL cannot be empty",
|
||||
"any.required": "Webhook URL is required",
|
||||
"string.uri": "Please enter a valid Webhook URL",
|
||||
}),
|
||||
switch: [
|
||||
{
|
||||
is: "email",
|
||||
then: joi
|
||||
.string()
|
||||
.email({ tlds: { allow: false } })
|
||||
.required()
|
||||
.messages({
|
||||
"string.empty": "E-mail address cannot be empty",
|
||||
"any.required": "E-mail address is required",
|
||||
"string.email": "Please enter a valid e-mail address",
|
||||
}),
|
||||
},
|
||||
{
|
||||
is: "pager_duty",
|
||||
then: joi.string().required().messages({
|
||||
"string.empty": "PagerDuty routing key cannot be empty",
|
||||
"any.required": "PagerDuty routing key is required",
|
||||
}),
|
||||
},
|
||||
{
|
||||
is: joi.valid("webhook", "slack", "discord"),
|
||||
then: joi.string().uri().required().messages({
|
||||
"string.empty": "Webhook URL cannot be empty",
|
||||
"any.required": "Webhook URL is required",
|
||||
"string.uri": "Please enter a valid Webhook URL",
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
{
|
||||
"ClickUpload": "Click to upload",
|
||||
"DeleteAccountButton": "Remove account",
|
||||
"DeleteAccountTitle": "Remove account",
|
||||
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
|
||||
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
|
||||
"DeleteWarningTitle": "Really remove this account?",
|
||||
"DragandDrop": "drag and drop",
|
||||
"EmailDescriptionText": "This is your current email address — it cannot be changed.",
|
||||
"FirstName": "First name",
|
||||
"LastName": "Last name",
|
||||
"MaxSize": "Maximum Size",
|
||||
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
|
||||
"SupportedFormats": "Supported formats",
|
||||
"YourPhoto": "Profile photo",
|
||||
"aboutus": "About Us",
|
||||
"access": "Access",
|
||||
"actions": "Actions",
|
||||
@@ -202,13 +188,6 @@
|
||||
"avgCpuTemperature": "Average CPU Temperature",
|
||||
"bar": "Bar",
|
||||
"basicInformation": "Basic Information",
|
||||
"bytesPerSecond": "Bytes per second",
|
||||
"bytesReceived": "Bytes Received",
|
||||
"bytesSent": "Bytes Sent",
|
||||
"dataReceived": "Data Received",
|
||||
"dataSent": "Data Sent",
|
||||
"dataRate": "Data Rate",
|
||||
"rate": "Rate",
|
||||
"bulkImport": {
|
||||
"fallbackPage": "Import a file to upload a list of servers in bulk",
|
||||
"invalidFileType": "Invalid file type",
|
||||
@@ -222,17 +201,21 @@
|
||||
"uploadSuccess": "Monitors created successfully!",
|
||||
"validationFailed": "Validation failed"
|
||||
},
|
||||
"bytesPerSecond": "Bytes per second",
|
||||
"bytesReceived": "Bytes Received",
|
||||
"bytesSent": "Bytes Sent",
|
||||
"cancel": "Cancel",
|
||||
"checkFormError": "Please check the form for errors.",
|
||||
"checkFrequency": "Check frequency",
|
||||
"chooseGame": "Choose game",
|
||||
"checkHooks": {
|
||||
"failureResolveAll": "Failed to resolve all incidents.",
|
||||
"failureResolveMonitor": "Failed to resolve monitor incidents.",
|
||||
"failureResolveOne": "Failed to resolve incident."
|
||||
},
|
||||
"checkingEvery": "Checking every",
|
||||
"chooseGame": "Choose game",
|
||||
"city": "CITY",
|
||||
"ClickUpload": "Click to upload",
|
||||
"common": {
|
||||
"appName": "Checkmate",
|
||||
"buttons": {
|
||||
@@ -262,6 +245,12 @@
|
||||
"createMaintenance": "Create maintenance",
|
||||
"createMaintenanceWindow": "Create maintenance window",
|
||||
"createMonitor": "Create monitor",
|
||||
"createMonitorPage": {
|
||||
"incidentConfigDescription": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.",
|
||||
"incidentConfigStatusWindowLabel": "How many checks should be in the sliding window?",
|
||||
"incidentConfigStatusWindowThresholdLabel": "What percentage of checks in the sliding window fail/succeed before monitor status changes?",
|
||||
"incidentConfigTitle": "Incidents"
|
||||
},
|
||||
"createNew": "Create new",
|
||||
"createNotifications": {
|
||||
"dialogDeleteConfirm": "Delete",
|
||||
@@ -311,13 +300,22 @@
|
||||
}
|
||||
},
|
||||
"createYour": "Create your",
|
||||
"dataRate": "Data Rate",
|
||||
"dataReceived": "Data Received",
|
||||
"dataSent": "Data Sent",
|
||||
"date&Time": "Date & Time",
|
||||
"delete": "Delete",
|
||||
"DeleteAccountButton": "Remove account",
|
||||
"DeleteAccountTitle": "Remove account",
|
||||
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
|
||||
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
|
||||
"deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.",
|
||||
"deleteDialogTitle": "Do you really want to delete this monitor?",
|
||||
"deleteStatusPage": "Do you want to delete this status page?",
|
||||
"deleteStatusPageConfirm": "Yes, delete status page",
|
||||
"deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.",
|
||||
"DeleteWarningTitle": "Really remove this account?",
|
||||
"details": "Details",
|
||||
"diagnosticsPage": {
|
||||
"diagnosticDescription": "System diagnostics",
|
||||
"gauges": {
|
||||
@@ -341,15 +339,6 @@
|
||||
},
|
||||
"disk": "Disk",
|
||||
"diskUsage": "Disk Usage",
|
||||
"drops": "Drops",
|
||||
"errors": "Errors",
|
||||
"errorsIn": "Errors In",
|
||||
"errorsOut": "Errors Out",
|
||||
"networkErrors": "Network Errors",
|
||||
"networkDrops": "Network Drops",
|
||||
"networkInterface": "Network Interface",
|
||||
"selectInterface": "Select Interface",
|
||||
"details": "Details",
|
||||
"displayName": "Display name",
|
||||
"distributedRightCategoryTitle": "Monitor",
|
||||
"distributedStatusHeaderText": "Real-time, real-device coverage",
|
||||
@@ -395,6 +384,8 @@
|
||||
"distributedUptimeStatusUptLogo": "Upt Logo",
|
||||
"dockerContainerMonitoring": "Docker container monitoring",
|
||||
"dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.",
|
||||
"DragandDrop": "drag and drop",
|
||||
"drops": "Drops",
|
||||
"duration": "Duration",
|
||||
"edit": "Edit",
|
||||
"editing": "Editing...",
|
||||
@@ -417,6 +408,7 @@
|
||||
"validationErrors": "Validation errors"
|
||||
}
|
||||
},
|
||||
"EmailDescriptionText": "This is your current email address — it cannot be changed.",
|
||||
"emailSent": "Email sent successfully",
|
||||
"errorInvalidFieldId": "Invalid field ID provided",
|
||||
"errorInvalidTypeId": "Invalid notification type provided",
|
||||
@@ -434,6 +426,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": "Errors",
|
||||
"errorsIn": "Errors In",
|
||||
"errorsOut": "Errors Out",
|
||||
"expectedValue": "Expected value",
|
||||
"export": {
|
||||
"failed": "Failed to export monitors",
|
||||
@@ -442,9 +437,12 @@
|
||||
},
|
||||
"failedToSendEmail": "Failed to send email",
|
||||
"features": "Features",
|
||||
"FirstName": "First name",
|
||||
"frequency": "Frequency",
|
||||
"friendlyNameInput": "Friendly name",
|
||||
"friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes",
|
||||
"gameServerMonitoring": "Game Server Monitoring",
|
||||
"gameServerMonitoringDescription": "Check whether your game server is running or not",
|
||||
"gb": "GB",
|
||||
"general": {
|
||||
"noOptionsFound": "No {{unit}} found"
|
||||
@@ -520,6 +518,7 @@
|
||||
"invalidFileFormat": "Unsupported file format!",
|
||||
"invalidFileSize": "File size is too large!",
|
||||
"inviteNoTokenFound": "No invite token found",
|
||||
"LastName": "Last name",
|
||||
"loginHere": "Login here",
|
||||
"logsPage": {
|
||||
"description": "System logs - last 1000 lines",
|
||||
@@ -571,6 +570,7 @@
|
||||
"regexPlaceholder": "^(success|ok)$",
|
||||
"text": "Match Method"
|
||||
},
|
||||
"MaxSize": "Maximum Size",
|
||||
"mb": "MB",
|
||||
"mem": "Mem",
|
||||
"memory": "Memory",
|
||||
@@ -632,6 +632,11 @@
|
||||
"namePlaceholder": "My Container",
|
||||
"placeholder": "abcd1234"
|
||||
},
|
||||
"game": {
|
||||
"label": "URL to monitor",
|
||||
"namePlaceholder": "localhost:5173",
|
||||
"placeholder": "localhost"
|
||||
},
|
||||
"http": {
|
||||
"label": "URL to monitor",
|
||||
"namePlaceholder": "Google",
|
||||
@@ -646,16 +651,14 @@
|
||||
"label": "URL to monitor",
|
||||
"namePlaceholder": "Localhost:5173",
|
||||
"placeholder": "localhost"
|
||||
},
|
||||
"game": {
|
||||
"label": "URL to monitor",
|
||||
"namePlaceholder": "localhost:5173",
|
||||
"placeholder": "localhost"
|
||||
}
|
||||
},
|
||||
"ms": "ms",
|
||||
"navControls": "Controls",
|
||||
"network": "Network",
|
||||
"networkDrops": "Network Drops",
|
||||
"networkErrors": "Network Errors",
|
||||
"networkInterface": "Network Interface",
|
||||
"nextWindow": "Next window",
|
||||
"noNetworkStatsAvailable": "No network stats available.",
|
||||
"notFoundButton": "Go to the main dashboard",
|
||||
@@ -739,6 +742,10 @@
|
||||
"notifySMS": "Notify via SMS (coming soon)",
|
||||
"now": "Now",
|
||||
"os": "OS",
|
||||
"packetsPerSecond": "Packets per second",
|
||||
"packetsReceived": "Packets Received",
|
||||
"packetsReceivedRate": "Packets Received Rate",
|
||||
"packetsSent": "Packets Sent",
|
||||
"pageSpeed": {
|
||||
"fallback": {
|
||||
"actionButton": "Let's create your first PageSpeed monitor!",
|
||||
@@ -769,17 +776,12 @@
|
||||
"passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.",
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"packetsPerSecond": "Packets per second",
|
||||
"packetsReceived": "Packets Received",
|
||||
"packetsReceivedRate": "Packets Received Rate",
|
||||
"packetsSent": "Packets Sent",
|
||||
"pause": "Pause",
|
||||
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
|
||||
"pingMonitoring": "Ping monitoring",
|
||||
"pingMonitoringDescription": "Check whether your server is available or not.",
|
||||
"portMonitoring": "Port monitoring",
|
||||
"portMonitoringDescription": "Check whether your port is open or not.",
|
||||
"gameServerMonitoring": "Game Server Monitoring",
|
||||
"gameServerMonitoringDescription": "Check whether your game server is running or not",
|
||||
"portToMonitor": "Port to monitor",
|
||||
"publicLink": "Public link",
|
||||
"publicURL": "Public URL",
|
||||
@@ -814,6 +816,7 @@
|
||||
"refreshButton": "Refresh",
|
||||
"title": "Queue"
|
||||
},
|
||||
"rate": "Rate",
|
||||
"remove": "Remove",
|
||||
"removeLogo": "Remove Logo",
|
||||
"repeat": "Repeat",
|
||||
@@ -829,6 +832,7 @@
|
||||
},
|
||||
"save": "Save",
|
||||
"selectAll": "Select all",
|
||||
"selectInterface": "Select Interface",
|
||||
"sendTestNotifications": "Send test notifications",
|
||||
"seperateEmails": "You can separate multiple emails with a comma",
|
||||
"settingsAppearance": "Appearance",
|
||||
@@ -873,6 +877,10 @@
|
||||
"title": "Email",
|
||||
"toastEmailRequiredFieldsError": "Email address, host, port and password are required"
|
||||
},
|
||||
"globalThresholds": {
|
||||
"description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring.",
|
||||
"title": "Global Thresholds"
|
||||
},
|
||||
"pageSpeedSettings": {
|
||||
"description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.",
|
||||
"labelApiKey": "PageSpeed API key",
|
||||
@@ -905,10 +913,6 @@
|
||||
"title": "Display timezone"
|
||||
},
|
||||
"title": "Settings",
|
||||
"globalThresholds": {
|
||||
"title": "Global Thresholds",
|
||||
"description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring."
|
||||
},
|
||||
"uiSettings": {
|
||||
"description": "Switch between light and dark mode, or change user interface language.",
|
||||
"labelLanguage": "Language",
|
||||
@@ -985,6 +989,7 @@
|
||||
"statusPageStatusNotPublic": "This status page is not public.",
|
||||
"statusPageStatusServiceStatus": "Service status",
|
||||
"submit": "Submit",
|
||||
"SupportedFormats": "Supported formats",
|
||||
"teamPanel": {
|
||||
"cancel": "Cancel",
|
||||
"email": "Email",
|
||||
@@ -1040,10 +1045,10 @@
|
||||
"uptimeCreateJsonPathQuery": "for query language documentation.",
|
||||
"uptimeGeneralInstructions": {
|
||||
"docker": "Enter the Docker ID of your container. Docker IDs must be the full 64 char Docker ID. You can run docker inspect <short_id> to get the full container ID.",
|
||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
||||
"http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.",
|
||||
"ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
|
||||
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.",
|
||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type."
|
||||
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard."
|
||||
},
|
||||
"uptimeMonitor": {
|
||||
"fallback": {
|
||||
@@ -1066,5 +1071,6 @@
|
||||
"websiteMonitoring": "Website monitoring",
|
||||
"websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.",
|
||||
"whenNewIncident": "When there is a new incident,",
|
||||
"window": "window"
|
||||
"window": "window",
|
||||
"YourPhoto": "Profile photo"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { execSync } from "child_process";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "");
|
||||
let version = "3.0-beta";
|
||||
let version = "3.1-beta";
|
||||
|
||||
return {
|
||||
base: "/",
|
||||
|
||||
190
server/package-lock.json
generated
190
server/package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "8.0.1",
|
||||
"gamedig": "^5.3.1",
|
||||
"got": "14.4.7",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.0.0",
|
||||
"ioredis": "^5.4.2",
|
||||
@@ -1137,6 +1138,12 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
@@ -1159,12 +1166,12 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
|
||||
"integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz",
|
||||
"integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
@@ -1821,21 +1828,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request": {
|
||||
"version": "10.2.14",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz",
|
||||
"integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==",
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz",
|
||||
"integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "^4.0.2",
|
||||
"get-stream": "^6.0.1",
|
||||
"@types/http-cache-semantics": "^4.0.4",
|
||||
"get-stream": "^9.0.1",
|
||||
"http-cache-semantics": "^4.1.1",
|
||||
"keyv": "^4.5.3",
|
||||
"keyv": "^4.5.4",
|
||||
"mimic-response": "^4.0.0",
|
||||
"normalize-url": "^8.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"responselike": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
@@ -3750,12 +3757,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
|
||||
"integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz",
|
||||
"integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.17"
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
@@ -3840,6 +3847,82 @@
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gamedig/node_modules/@sindresorhus/is": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
|
||||
"integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/gamedig/node_modules/cacheable-request": {
|
||||
"version": "10.2.14",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz",
|
||||
"integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "^4.0.2",
|
||||
"get-stream": "^6.0.1",
|
||||
"http-cache-semantics": "^4.1.1",
|
||||
"keyv": "^4.5.3",
|
||||
"mimic-response": "^4.0.0",
|
||||
"normalize-url": "^8.0.0",
|
||||
"responselike": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/gamedig/node_modules/form-data-encoder": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
|
||||
"integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/gamedig/node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gamedig/node_modules/got": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
|
||||
"integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^5.2.0",
|
||||
"@szmarczak/http-timer": "^5.0.1",
|
||||
"cacheable-lookup": "^7.0.0",
|
||||
"cacheable-request": "^10.2.8",
|
||||
"decompress-response": "^6.0.0",
|
||||
"form-data-encoder": "^2.1.2",
|
||||
"get-stream": "^6.0.1",
|
||||
"http2-wrapper": "^2.1.10",
|
||||
"lowercase-keys": "^3.0.0",
|
||||
"p-cancelable": "^3.0.0",
|
||||
"responselike": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/gamedig/node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -3852,6 +3935,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gamedig/node_modules/p-cancelable": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
|
||||
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
|
||||
@@ -3930,12 +4022,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
||||
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sec-ant/readable-stream": "^0.4.1",
|
||||
"is-stream": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream/node_modules/is-stream": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
||||
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
@@ -4024,28 +4132,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
|
||||
"integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
|
||||
"version": "14.4.7",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz",
|
||||
"integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^5.2.0",
|
||||
"@sindresorhus/is": "^7.0.1",
|
||||
"@szmarczak/http-timer": "^5.0.1",
|
||||
"cacheable-lookup": "^7.0.0",
|
||||
"cacheable-request": "^10.2.8",
|
||||
"cacheable-request": "^12.0.1",
|
||||
"decompress-response": "^6.0.0",
|
||||
"form-data-encoder": "^2.1.2",
|
||||
"get-stream": "^6.0.1",
|
||||
"http2-wrapper": "^2.1.10",
|
||||
"form-data-encoder": "^4.0.2",
|
||||
"http2-wrapper": "^2.2.1",
|
||||
"lowercase-keys": "^3.0.0",
|
||||
"p-cancelable": "^3.0.0",
|
||||
"responselike": "^3.0.0"
|
||||
"p-cancelable": "^4.0.1",
|
||||
"responselike": "^3.0.0",
|
||||
"type-fest": "^4.26.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/got/node_modules/type-fest": {
|
||||
"version": "4.41.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
@@ -6350,12 +6470,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
|
||||
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
|
||||
"integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "8.0.1",
|
||||
"gamedig": "^5.3.1",
|
||||
"got": "14.4.7",
|
||||
"handlebars": "^4.7.8",
|
||||
"helmet": "^8.0.0",
|
||||
"ioredis": "^5.4.2",
|
||||
|
||||
@@ -19,8 +19,10 @@ import MaintenanceWindowService from "../service/business/maintenanceWindowServi
|
||||
import MonitorService from "../service/business/monitorService.js";
|
||||
import papaparse from "papaparse";
|
||||
import axios from "axios";
|
||||
import got from "got";
|
||||
import ping from "ping";
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
import Docker from "dockerode";
|
||||
import net from "net";
|
||||
import fs from "fs";
|
||||
@@ -32,6 +34,8 @@ import mjml2html from "mjml";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
import { games } from "gamedig";
|
||||
import jmespath from "jmespath";
|
||||
import { GameDig } from "gamedig";
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import { ObjectId } from "mongodb";
|
||||
@@ -43,8 +47,6 @@ import { ParseBoolean } from "../utils/utils.js";
|
||||
|
||||
// Models
|
||||
import Check from "../db/models/Check.js";
|
||||
import HardwareCheck from "../db/models/HardwareCheck.js";
|
||||
import PageSpeedCheck from "../db/models/PageSpeedCheck.js";
|
||||
import Monitor from "../db/models/Monitor.js";
|
||||
import User from "../db/models/User.js";
|
||||
import InviteToken from "../db/models/InviteToken.js";
|
||||
@@ -52,7 +54,6 @@ import StatusPage from "../db/models/StatusPage.js";
|
||||
import Team from "../db/models/Team.js";
|
||||
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
|
||||
import MonitorStats from "../db/models/MonitorStats.js";
|
||||
import NetworkCheck from "../db/models/NetworkCheck.js";
|
||||
import Notification from "../db/models/Notification.js";
|
||||
import RecoveryToken from "../db/models/RecoveryToken.js";
|
||||
import AppSettings from "../db/models/AppSettings.js";
|
||||
@@ -61,12 +62,9 @@ import InviteModule from "../db/mongo/modules/inviteModule.js";
|
||||
import CheckModule from "../db/mongo/modules/checkModule.js";
|
||||
import StatusPageModule from "../db/mongo/modules/statusPageModule.js";
|
||||
import UserModule from "../db/mongo/modules/userModule.js";
|
||||
import HardwareCheckModule from "../db/mongo/modules/hardwareCheckModule.js";
|
||||
import MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js";
|
||||
import MonitorModule from "../db/mongo/modules/monitorModule.js";
|
||||
import NetworkCheckModule from "../db/mongo/modules/networkCheckModule.js";
|
||||
import NotificationModule from "../db/mongo/modules/notificationModule.js";
|
||||
import PageSpeedCheckModule from "../db/mongo/modules/pageSpeedCheckModule.js";
|
||||
import RecoveryModule from "../db/mongo/modules/recoveryModule.js";
|
||||
import SettingsModule from "../db/mongo/modules/settingsModule.js";
|
||||
|
||||
@@ -80,18 +78,15 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
const stringService = new StringService(translationService);
|
||||
|
||||
// Create DB
|
||||
const checkModule = new CheckModule({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User });
|
||||
const checkModule = new CheckModule({ logger, Check, Monitor, User });
|
||||
const inviteModule = new InviteModule({ InviteToken, crypto, stringService });
|
||||
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService });
|
||||
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
|
||||
const hardwareCheckModule = new HardwareCheckModule({ HardwareCheck, Monitor, logger });
|
||||
const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow });
|
||||
const monitorModule = new MonitorModule({
|
||||
Monitor,
|
||||
MonitorStats,
|
||||
Check,
|
||||
PageSpeedCheck,
|
||||
HardwareCheck,
|
||||
stringService,
|
||||
fs,
|
||||
path,
|
||||
@@ -100,9 +95,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
NormalizeData,
|
||||
NormalizeDataUptimeDetails,
|
||||
});
|
||||
const networkCheckModule = new NetworkCheckModule({ NetworkCheck });
|
||||
const notificationModule = new NotificationModule({ Notification, Monitor });
|
||||
const pageSpeedCheckModule = new PageSpeedCheckModule({ PageSpeedCheck });
|
||||
const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService });
|
||||
const settingsModule = new SettingsModule({ AppSettings });
|
||||
|
||||
@@ -113,19 +106,29 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
inviteModule,
|
||||
statusPageModule,
|
||||
userModule,
|
||||
hardwareCheckModule,
|
||||
maintenanceWindowModule,
|
||||
monitorModule,
|
||||
networkCheckModule,
|
||||
notificationModule,
|
||||
pageSpeedCheckModule,
|
||||
recoveryModule,
|
||||
settingsModule,
|
||||
});
|
||||
|
||||
await db.connect();
|
||||
|
||||
const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService);
|
||||
const networkService = new NetworkService({
|
||||
axios,
|
||||
got,
|
||||
https,
|
||||
jmespath,
|
||||
GameDig,
|
||||
ping,
|
||||
logger,
|
||||
http,
|
||||
Docker,
|
||||
net,
|
||||
stringService,
|
||||
settingsService,
|
||||
});
|
||||
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
||||
const bufferService = new BufferService({ db, logger, envSettings });
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService });
|
||||
|
||||
@@ -1,99 +1,176 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const BaseCheckSchema = mongoose.Schema({
|
||||
/**
|
||||
* Reference to the associated Monitor document.
|
||||
*
|
||||
* @type {mongoose.Schema.Types.ObjectId}
|
||||
*/
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
|
||||
teamId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
/**
|
||||
* Status of the check (true for up, false for down).
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
status: {
|
||||
type: Boolean,
|
||||
index: true,
|
||||
},
|
||||
/**
|
||||
* Response time of the check in milliseconds.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
responseTime: {
|
||||
type: Number,
|
||||
},
|
||||
/**
|
||||
* HTTP status code received during the check.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
statusCode: {
|
||||
type: Number,
|
||||
index: true,
|
||||
},
|
||||
/**
|
||||
* Message or description of the check result.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* Expiry date of the check, auto-calculated to expire after 30 days.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
/**
|
||||
* Acknowledgment of the check.
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
ack: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* Resolution date of the check (when the check was resolved).
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
ackAt: {
|
||||
type: Date,
|
||||
},
|
||||
const cpuSchema = mongoose.Schema({
|
||||
physical_core: { type: Number, default: 0 },
|
||||
logical_core: { type: Number, default: 0 },
|
||||
frequency: { type: Number, default: 0 },
|
||||
temperature: { type: [Number], default: [] },
|
||||
free_percent: { type: Number, default: 0 },
|
||||
usage_percent: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
/**
|
||||
* Check Schema for MongoDB collection.
|
||||
*
|
||||
* Represents a check associated with a monitor, storing information
|
||||
* about the status and response of a particular check event.
|
||||
*/
|
||||
const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: true });
|
||||
const memorySchema = mongoose.Schema({
|
||||
total_bytes: { type: Number, default: 0 },
|
||||
available_bytes: { type: Number, default: 0 },
|
||||
used_bytes: { type: Number, default: 0 },
|
||||
usage_percent: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
const diskSchema = mongoose.Schema({
|
||||
read_speed_bytes: { type: Number, default: 0 },
|
||||
write_speed_bytes: { type: Number, default: 0 },
|
||||
total_bytes: { type: Number, default: 0 },
|
||||
free_bytes: { type: Number, default: 0 },
|
||||
usage_percent: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
const hostSchema = mongoose.Schema({
|
||||
os: { type: String, default: "" },
|
||||
platform: { type: String, default: "" },
|
||||
kernel_version: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const errorSchema = mongoose.Schema({
|
||||
metric: { type: [String], default: [] },
|
||||
err: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const captureSchema = mongoose.Schema({
|
||||
version: { type: String, default: "" },
|
||||
mode: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const networkInterfaceSchema = mongoose.Schema({
|
||||
name: { type: String },
|
||||
bytes_sent: { type: Number, default: 0 },
|
||||
bytes_recv: { type: Number, default: 0 },
|
||||
packets_sent: { type: Number, default: 0 },
|
||||
packets_recv: { type: Number, default: 0 },
|
||||
err_in: { type: Number, default: 0 },
|
||||
err_out: { type: Number, default: 0 },
|
||||
drop_in: { type: Number, default: 0 },
|
||||
drop_out: { type: Number, default: 0 },
|
||||
fifo_in: { type: Number, default: 0 },
|
||||
fifo_out: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
const CheckSchema = new mongoose.Schema(
|
||||
{
|
||||
// Common fields
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
|
||||
teamId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ["http", "hardware", "pagespeed", "distributed"],
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
|
||||
status: {
|
||||
type: Boolean,
|
||||
index: true,
|
||||
},
|
||||
|
||||
responseTime: {
|
||||
type: Number,
|
||||
},
|
||||
|
||||
timings: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
|
||||
statusCode: {
|
||||
type: Number,
|
||||
index: true,
|
||||
},
|
||||
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
|
||||
ack: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
||||
ackAt: {
|
||||
type: Date,
|
||||
},
|
||||
|
||||
// Hardware fields
|
||||
cpu: {
|
||||
type: cpuSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
memory: {
|
||||
type: memorySchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
disk: {
|
||||
type: [diskSchema],
|
||||
default: () => [],
|
||||
},
|
||||
host: {
|
||||
type: hostSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: [errorSchema],
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
capture: {
|
||||
type: captureSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
net: {
|
||||
type: [networkInterfaceSchema],
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
// PageSpeed fields
|
||||
accessibility: {
|
||||
type: Number,
|
||||
},
|
||||
bestPractices: {
|
||||
type: Number,
|
||||
},
|
||||
seo: {
|
||||
type: Number,
|
||||
},
|
||||
performance: {
|
||||
type: Number,
|
||||
},
|
||||
audits: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
CheckSchema.index({ updatedAt: 1 });
|
||||
CheckSchema.index({ monitorId: 1, updatedAt: 1 });
|
||||
CheckSchema.index({ monitorId: 1, updatedAt: -1 });
|
||||
CheckSchema.index({ teamId: 1, updatedAt: -1 });
|
||||
|
||||
export default mongoose.model("Check", CheckSchema);
|
||||
export { BaseCheckSchema };
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
import { BaseCheckSchema } from "./Check.js";
|
||||
const cpuSchema = mongoose.Schema({
|
||||
physical_core: { type: Number, default: 0 },
|
||||
logical_core: { type: Number, default: 0 },
|
||||
frequency: { type: Number, default: 0 },
|
||||
temperature: { type: [Number], default: [] },
|
||||
free_percent: { type: Number, default: 0 },
|
||||
usage_percent: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
const memorySchema = mongoose.Schema({
|
||||
total_bytes: { type: Number, default: 0 },
|
||||
available_bytes: { type: Number, default: 0 },
|
||||
used_bytes: { type: Number, default: 0 },
|
||||
usage_percent: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
const diskSchema = mongoose.Schema({
|
||||
read_speed_bytes: { type: Number, default: 0 },
|
||||
write_speed_bytes: { type: Number, default: 0 },
|
||||
total_bytes: { type: Number, default: 0 },
|
||||
free_bytes: { type: Number, default: 0 },
|
||||
usage_percent: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
const hostSchema = mongoose.Schema({
|
||||
os: { type: String, default: "" },
|
||||
platform: { type: String, default: "" },
|
||||
kernel_version: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const errorSchema = mongoose.Schema({
|
||||
metric: { type: [String], default: [] },
|
||||
err: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const captureSchema = mongoose.Schema({
|
||||
version: { type: String, default: "" },
|
||||
mode: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const networkInterfaceSchema = mongoose.Schema({
|
||||
name: { type: String },
|
||||
bytes_sent: { type: Number, default: 0 },
|
||||
bytes_recv: { type: Number, default: 0 },
|
||||
packets_sent: { type: Number, default: 0 },
|
||||
packets_recv: { type: Number, default: 0 },
|
||||
err_in: { type: Number, default: 0 },
|
||||
err_out: { type: Number, default: 0 },
|
||||
drop_in: { type: Number, default: 0 },
|
||||
drop_out: { type: Number, default: 0 },
|
||||
fifo_in: { type: Number, default: 0 },
|
||||
fifo_out: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
const HardwareCheckSchema = mongoose.Schema(
|
||||
{
|
||||
...BaseCheckSchema.obj,
|
||||
cpu: {
|
||||
type: cpuSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
memory: {
|
||||
type: memorySchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
disk: {
|
||||
type: [diskSchema],
|
||||
default: () => [],
|
||||
},
|
||||
host: {
|
||||
type: hostSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
errors: {
|
||||
type: [errorSchema],
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
capture: {
|
||||
type: captureSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
net: {
|
||||
type: [networkInterfaceSchema],
|
||||
default: () => [],
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
HardwareCheckSchema.index({ createdAt: 1 });
|
||||
HardwareCheckSchema.index({ monitorId: 1, createdAt: 1 });
|
||||
HardwareCheckSchema.index({ monitorId: 1, createdAt: -1 });
|
||||
|
||||
export default mongoose.model("HardwareCheck", HardwareCheckSchema);
|
||||
@@ -1,6 +1,4 @@
|
||||
import mongoose from "mongoose";
|
||||
import HardwareCheck from "./HardwareCheck.js";
|
||||
import PageSpeedCheck from "./PageSpeedCheck.js";
|
||||
import Check from "./Check.js";
|
||||
import MonitorStats from "./MonitorStats.js";
|
||||
import StatusPage from "./StatusPage.js";
|
||||
@@ -30,6 +28,18 @@ const MonitorSchema = mongoose.Schema(
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
statusWindow: {
|
||||
type: [Boolean],
|
||||
default: [],
|
||||
},
|
||||
statusWindowSize: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
statusWindowThreshold: {
|
||||
type: Number,
|
||||
default: 0.6,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
@@ -133,13 +143,7 @@ MonitorSchema.pre("findOneAndDelete", async function (next) {
|
||||
throw new Error("Monitor not found");
|
||||
}
|
||||
|
||||
if (doc?.type === "pagespeed") {
|
||||
await PageSpeedCheck.deleteMany({ monitorId: doc._id });
|
||||
} else if (doc?.type === "hardware") {
|
||||
await HardwareCheck.deleteMany({ monitorId: doc._id });
|
||||
} else {
|
||||
await Check.deleteMany({ monitorId: doc._id });
|
||||
}
|
||||
await Check.deleteMany({ monitorId: doc._id });
|
||||
|
||||
// Deal with status pages
|
||||
await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } });
|
||||
@@ -156,13 +160,7 @@ MonitorSchema.pre("deleteMany", async function (next) {
|
||||
const monitors = await this.model.find(filter).select(["_id", "type"]).lean();
|
||||
|
||||
for (const monitor of monitors) {
|
||||
if (monitor.type === "pagespeed") {
|
||||
await PageSpeedCheck.deleteMany({ monitorId: monitor._id });
|
||||
} else if (monitor.type === "hardware") {
|
||||
await HardwareCheck.deleteMany({ monitorId: monitor._id });
|
||||
} else {
|
||||
await Check.deleteMany({ monitorId: monitor._id });
|
||||
}
|
||||
await Check.deleteMany({ monitorId: monitor._id });
|
||||
await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } });
|
||||
await MonitorStats.deleteMany({ monitorId: monitor._id.toString() });
|
||||
}
|
||||
|
||||
@@ -40,10 +40,6 @@ const MonitorStatsSchema = new mongoose.Schema(
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
uptBurnt: {
|
||||
type: mongoose.Schema.Types.Decimal128,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
import { BaseCheckSchema } from "./Check.js";
|
||||
|
||||
const networkInterfaceSchema = mongoose.Schema({
|
||||
name: { type: String, required: true },
|
||||
bytes_sent: { type: Number, default: 0 },
|
||||
bytes_recv: { type: Number, default: 0 },
|
||||
packets_sent: { type: Number, default: 0 },
|
||||
packets_recv: { type: Number, default: 0 },
|
||||
err_in: { type: Number, default: 0 },
|
||||
err_out: { type: Number, default: 0 },
|
||||
drop_in: { type: Number, default: 0 },
|
||||
drop_out: { type: Number, default: 0 },
|
||||
fifo_in: { type: Number, default: 0 },
|
||||
fifo_out: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
const captureSchema = mongoose.Schema({
|
||||
version: { type: String, default: "" },
|
||||
mode: { type: String, default: "" },
|
||||
});
|
||||
|
||||
const NetworkCheckSchema = mongoose.Schema(
|
||||
{
|
||||
...BaseCheckSchema.obj,
|
||||
data: {
|
||||
type: [networkInterfaceSchema],
|
||||
default: () => [],
|
||||
},
|
||||
capture: {
|
||||
type: captureSchema,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
NetworkCheckSchema.index({ createdAt: 1 });
|
||||
NetworkCheckSchema.index({ monitorId: 1, createdAt: 1 });
|
||||
NetworkCheckSchema.index({ monitorId: 1, createdAt: -1 });
|
||||
|
||||
export default mongoose.model("NetworkCheck", NetworkCheckSchema);
|
||||
@@ -1,83 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
import { BaseCheckSchema } from "./Check.js";
|
||||
const AuditSchema = mongoose.Schema({
|
||||
id: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
score: { type: Number, required: true },
|
||||
scoreDisplayMode: { type: String, required: true },
|
||||
displayValue: { type: String, required: true },
|
||||
numericValue: { type: Number, required: true },
|
||||
numericUnit: { type: String, required: true },
|
||||
});
|
||||
|
||||
const AuditsSchema = mongoose.Schema({
|
||||
cls: {
|
||||
type: AuditSchema,
|
||||
required: true,
|
||||
},
|
||||
si: {
|
||||
type: AuditSchema,
|
||||
required: true,
|
||||
},
|
||||
fcp: {
|
||||
type: AuditSchema,
|
||||
required: true,
|
||||
},
|
||||
lcp: {
|
||||
type: AuditSchema,
|
||||
required: true,
|
||||
},
|
||||
tbt: {
|
||||
type: AuditSchema,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Mongoose schema for storing metrics from Google Lighthouse.
|
||||
* @typedef {Object} PageSpeedCheck
|
||||
* @property {mongoose.Schema.Types.ObjectId} monitorId - Reference to the Monitor model.
|
||||
* @property {number} accessibility - Accessibility score.
|
||||
* @property {number} bestPractices - Best practices score.
|
||||
* @property {number} seo - SEO score.
|
||||
* @property {number} performance - Performance score.
|
||||
*/
|
||||
|
||||
const PageSpeedCheck = mongoose.Schema(
|
||||
{
|
||||
...BaseCheckSchema.obj,
|
||||
accessibility: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
bestPractices: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
seo: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
performance: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
audits: {
|
||||
type: AuditsSchema,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* Mongoose model for storing metrics from Google Lighthouse.
|
||||
* @typedef {mongoose.Model<PageSpeedCheck>} LighthouseMetricsModel
|
||||
*/
|
||||
|
||||
PageSpeedCheck.index({ createdAt: 1 });
|
||||
PageSpeedCheck.index({ monitorId: 1, createdAt: 1 });
|
||||
PageSpeedCheck.index({ monitorId: 1, createdAt: -1 });
|
||||
|
||||
export default mongoose.model("PageSpeedCheck", PageSpeedCheck);
|
||||
@@ -31,7 +31,7 @@ class CheckModule {
|
||||
}
|
||||
};
|
||||
|
||||
getChecksByMonitor = async ({ monitorId, type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => {
|
||||
getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => {
|
||||
try {
|
||||
status = status === "true" ? true : status === "false" ? false : undefined;
|
||||
page = parseInt(page);
|
||||
@@ -79,19 +79,7 @@ class CheckModule {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
const checkModels = {
|
||||
http: this.Check,
|
||||
ping: this.Check,
|
||||
docker: this.Check,
|
||||
port: this.Check,
|
||||
pagespeed: this.PageSpeedCheck,
|
||||
hardware: this.HardwareCheck,
|
||||
game: this.Check,
|
||||
};
|
||||
|
||||
const Model = checkModels[type];
|
||||
|
||||
const checks = await Model.aggregate([
|
||||
const checks = await this.Check.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { createdAt: sortOrder } },
|
||||
{
|
||||
@@ -166,18 +154,6 @@ class CheckModule {
|
||||
|
||||
const aggregatePipeline = [
|
||||
{ $match: matchStage },
|
||||
{
|
||||
$unionWith: {
|
||||
coll: "hardwarechecks",
|
||||
pipeline: [{ $match: matchStage }],
|
||||
},
|
||||
},
|
||||
{
|
||||
$unionWith: {
|
||||
coll: "pagespeedchecks",
|
||||
pipeline: [{ $match: matchStage }],
|
||||
},
|
||||
},
|
||||
|
||||
{ $sort: { createdAt: sortOrder } },
|
||||
{
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
const SERVICE_NAME = "hardwareCheckModule";
|
||||
|
||||
class HardwareCheckModule {
|
||||
constructor({ HardwareCheck, Monitor, logger }) {
|
||||
this.HardwareCheck = HardwareCheck;
|
||||
this.Monitor = Monitor;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
createHardwareChecks = async (hardwareChecks) => {
|
||||
try {
|
||||
await this.HardwareCheck.insertMany(hardwareChecks, { ordered: false });
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createHardwareChecks";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default HardwareCheckModule;
|
||||
@@ -11,25 +11,10 @@ import {
|
||||
const SERVICE_NAME = "monitorModule";
|
||||
|
||||
class MonitorModule {
|
||||
constructor({
|
||||
Monitor,
|
||||
MonitorStats,
|
||||
Check,
|
||||
PageSpeedCheck,
|
||||
HardwareCheck,
|
||||
stringService,
|
||||
fs,
|
||||
path,
|
||||
fileURLToPath,
|
||||
ObjectId,
|
||||
NormalizeData,
|
||||
NormalizeDataUptimeDetails,
|
||||
}) {
|
||||
constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
|
||||
this.Monitor = Monitor;
|
||||
this.MonitorStats = MonitorStats;
|
||||
this.Check = Check;
|
||||
this.PageSpeedCheck = PageSpeedCheck;
|
||||
this.HardwareCheck = HardwareCheck;
|
||||
this.stringService = stringService;
|
||||
this.fs = fs;
|
||||
this.path = path;
|
||||
@@ -38,15 +23,6 @@ class MonitorModule {
|
||||
this.NormalizeData = NormalizeData;
|
||||
this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails;
|
||||
|
||||
this.CHECK_MODEL_LOOKUP = {
|
||||
http: Check,
|
||||
ping: Check,
|
||||
docker: Check,
|
||||
port: Check,
|
||||
pagespeed: PageSpeedCheck,
|
||||
hardware: HardwareCheck,
|
||||
};
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -146,19 +122,18 @@ class MonitorModule {
|
||||
};
|
||||
|
||||
//Helper
|
||||
getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => {
|
||||
getMonitorChecks = async (monitorId, dateRange, sortOrder) => {
|
||||
const indexSpec = {
|
||||
monitorId: 1,
|
||||
createdAt: sortOrder, // This will be 1 or -1
|
||||
updatedAt: sortOrder, // This will be 1 or -1
|
||||
};
|
||||
|
||||
const [checksAll, checksForDateRange] = await Promise.all([
|
||||
model.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
|
||||
model
|
||||
.find({
|
||||
monitorId,
|
||||
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
|
||||
})
|
||||
this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
|
||||
this.Check.find({
|
||||
monitorId,
|
||||
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
|
||||
})
|
||||
.hint(indexSpec)
|
||||
.lean(),
|
||||
]);
|
||||
@@ -295,9 +270,8 @@ class MonitorModule {
|
||||
const sort = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
// Get Checks for monitor in date range requested
|
||||
const model = this.CHECK_MODEL_LOOKUP[monitor.type];
|
||||
const dates = this.getDateRange(dateRange);
|
||||
const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, model, dates, sort);
|
||||
const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort);
|
||||
|
||||
// Build monitor stats
|
||||
const monitorStats = {
|
||||
@@ -339,7 +313,7 @@ class MonitorModule {
|
||||
};
|
||||
const dateString = formatLookup[dateRange];
|
||||
|
||||
const hardwareStats = await this.HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
|
||||
const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
|
||||
|
||||
const stats = hardwareStats[0];
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
|
||||
{
|
||||
$match: {
|
||||
monitorId: new ObjectId(monitorId),
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
updatedAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -173,6 +173,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
{
|
||||
$match: {
|
||||
monitorId: monitor._id,
|
||||
type: "hardware",
|
||||
createdAt: { $gte: dates.start, $lte: dates.end },
|
||||
},
|
||||
},
|
||||
@@ -218,14 +219,14 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
{
|
||||
$project: {
|
||||
diskCount: {
|
||||
$size: "$disk",
|
||||
$size: { $ifNull: ["$disk", []] },
|
||||
},
|
||||
netCount: { $size: "$net" },
|
||||
netCount: { $size: { $ifNull: ["$net", []] } },
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "hardwarechecks",
|
||||
from: "checks",
|
||||
let: {
|
||||
diskCount: "$diskCount",
|
||||
netCount: "$netCount",
|
||||
@@ -381,7 +382,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
},
|
||||
net: {
|
||||
$map: {
|
||||
input: { $range: [0, { $size: { $arrayElemAt: ["$net", 0] } }] },
|
||||
input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$net", 0] }, []] } }] },
|
||||
as: "netIndex",
|
||||
in: {
|
||||
name: {
|
||||
@@ -409,7 +410,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
$arrayElemAt: [
|
||||
{
|
||||
$map: {
|
||||
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
|
||||
input: {
|
||||
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
|
||||
},
|
||||
as: "iface",
|
||||
in: "$$iface.bytes_sent",
|
||||
},
|
||||
@@ -418,7 +421,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
],
|
||||
},
|
||||
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
|
||||
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
|
||||
tLast: {
|
||||
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
@@ -444,7 +449,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
$arrayElemAt: [
|
||||
{
|
||||
$map: {
|
||||
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
|
||||
input: {
|
||||
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
|
||||
},
|
||||
as: "iface",
|
||||
in: "$$iface.bytes_recv",
|
||||
},
|
||||
@@ -453,7 +460,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
],
|
||||
},
|
||||
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
|
||||
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
|
||||
tLast: {
|
||||
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
@@ -479,7 +488,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
$arrayElemAt: [
|
||||
{
|
||||
$map: {
|
||||
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
|
||||
input: {
|
||||
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
|
||||
},
|
||||
as: "iface",
|
||||
in: "$$iface.packets_sent",
|
||||
},
|
||||
@@ -488,7 +499,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
],
|
||||
},
|
||||
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
|
||||
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
|
||||
tLast: {
|
||||
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
@@ -522,9 +535,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
$map: {
|
||||
input: {
|
||||
$arrayElemAt: [
|
||||
"$net",
|
||||
{ $ifNull: ["$net", []] },
|
||||
{
|
||||
$subtract: [{ $size: "$net" }, 1],
|
||||
$subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -538,9 +551,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
|
||||
tLast: {
|
||||
$arrayElemAt: [
|
||||
"$updatedAts",
|
||||
{ $ifNull: ["$updatedAts", []] },
|
||||
{
|
||||
$subtract: [{ $size: "$updatedAts" }, 1],
|
||||
$subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -566,7 +579,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
$arrayElemAt: [
|
||||
{
|
||||
$map: {
|
||||
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
|
||||
input: {
|
||||
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
|
||||
},
|
||||
as: "iface",
|
||||
in: "$$iface.err_in",
|
||||
},
|
||||
@@ -575,7 +590,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
],
|
||||
},
|
||||
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
|
||||
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
|
||||
tLast: {
|
||||
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
|
||||
},
|
||||
},
|
||||
in: {
|
||||
$cond: [
|
||||
@@ -609,9 +626,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
$map: {
|
||||
input: {
|
||||
$arrayElemAt: [
|
||||
"$net",
|
||||
{ $ifNull: ["$net", []] },
|
||||
{
|
||||
$subtract: [{ $size: "$net" }, 1],
|
||||
$subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -625,9 +642,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
|
||||
tLast: {
|
||||
$arrayElemAt: [
|
||||
"$updatedAts",
|
||||
{ $ifNull: ["$updatedAts", []] },
|
||||
{
|
||||
$subtract: [{ $size: "$updatedAts" }, 1],
|
||||
$subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -664,9 +681,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
$map: {
|
||||
input: {
|
||||
$arrayElemAt: [
|
||||
"$net",
|
||||
{ $ifNull: ["$net", []] },
|
||||
{
|
||||
$subtract: [{ $size: "$net" }, 1],
|
||||
$subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -680,9 +697,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
|
||||
tLast: {
|
||||
$arrayElemAt: [
|
||||
"$updatedAts",
|
||||
{ $ifNull: ["$updatedAts", []] },
|
||||
{
|
||||
$subtract: [{ $size: "$updatedAts" }, 1],
|
||||
$subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -719,9 +736,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
$map: {
|
||||
input: {
|
||||
$arrayElemAt: [
|
||||
"$net",
|
||||
{ $ifNull: ["$net", []] },
|
||||
{
|
||||
$subtract: [{ $size: "$net" }, 1],
|
||||
$subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -735,9 +752,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
|
||||
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
|
||||
tLast: {
|
||||
$arrayElemAt: [
|
||||
"$updatedAts",
|
||||
{ $ifNull: ["$updatedAts", []] },
|
||||
{
|
||||
$subtract: [{ $size: "$updatedAts" }, 1],
|
||||
$subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -970,12 +987,7 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, row
|
||||
|
||||
// Add checks
|
||||
if (limit) {
|
||||
let checksCollection = "checks";
|
||||
if (type === "pagespeed") {
|
||||
checksCollection = "pagespeedchecks";
|
||||
} else if (type === "hardware") {
|
||||
checksCollection = "hardwarechecks";
|
||||
}
|
||||
const checksCollection = "checks";
|
||||
monitorsPipeline.push({
|
||||
$lookup: {
|
||||
from: checksCollection,
|
||||
@@ -1040,12 +1052,8 @@ const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsP
|
||||
|
||||
// Add checks
|
||||
if (limit) {
|
||||
let checksCollection = "checks";
|
||||
if (type === "pagespeed") {
|
||||
checksCollection = "pagespeedchecks";
|
||||
} else if (type === "hardware") {
|
||||
checksCollection = "hardwarechecks";
|
||||
}
|
||||
const checksCollection = "checks";
|
||||
|
||||
pipeline.push({
|
||||
$lookup: {
|
||||
from: checksCollection,
|
||||
@@ -1159,46 +1167,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(limit
|
||||
? [
|
||||
{
|
||||
$lookup: {
|
||||
from: "pagespeedchecks",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
...(limit ? [{ $limit: limit }] : []),
|
||||
],
|
||||
as: "pagespeedchecks",
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(limit
|
||||
? [
|
||||
{
|
||||
$lookup: {
|
||||
from: "hardwarechecks",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [
|
||||
{
|
||||
$match: {
|
||||
$expr: { $eq: ["$monitorId", "$$monitorId"] },
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: -1 } },
|
||||
...(limit ? [{ $limit: limit }] : []),
|
||||
],
|
||||
as: "hardwarechecks",
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
$addFields: {
|
||||
@@ -1209,14 +1177,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
|
||||
case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] },
|
||||
then: "$standardchecks",
|
||||
},
|
||||
{
|
||||
case: { $eq: ["$type", "pagespeed"] },
|
||||
then: "$pagespeedchecks",
|
||||
},
|
||||
{
|
||||
case: { $eq: ["$type", "hardware"] },
|
||||
then: "$hardwarechecks",
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
},
|
||||
@@ -1226,8 +1186,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
|
||||
{
|
||||
$project: {
|
||||
standardchecks: 0,
|
||||
pagespeedchecks: 0,
|
||||
hardwarechecks: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
const SERVICE_NAME = "networkCheckModule";
|
||||
|
||||
class NetworkCheckModule {
|
||||
constructor({ NetworkCheck }) {
|
||||
this.NetworkCheck = NetworkCheck;
|
||||
}
|
||||
createNetworkCheck = async (networkCheckData) => {
|
||||
try {
|
||||
const networkCheck = await new this.NetworkCheck(networkCheckData);
|
||||
await networkCheck.save();
|
||||
return networkCheck;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createNetworkCheck";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
getNetworkChecksByMonitorId = async (monitorId, limit = 100) => {
|
||||
try {
|
||||
const networkChecks = await this.NetworkCheck.find({ monitorId }).sort({ createdAt: -1 }).limit(limit);
|
||||
return networkChecks;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getNetworkChecksByMonitorId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default NetworkCheckModule;
|
||||
@@ -1,31 +0,0 @@
|
||||
// import PageSpeedCheck from "../../models/PageSpeedCheck.js";
|
||||
const SERVICE_NAME = "pageSpeedCheckModule";
|
||||
|
||||
class PageSpeedCheckModule {
|
||||
constructor({ PageSpeedCheck }) {
|
||||
this.PageSpeedCheck = PageSpeedCheck;
|
||||
}
|
||||
|
||||
createPageSpeedChecks = async (pageSpeedChecks) => {
|
||||
try {
|
||||
await this.PageSpeedCheck.insertMany(pageSpeedChecks, { ordered: false });
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createPageSpeedCheck";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deletePageSpeedChecksByMonitorId = async (monitorId) => {
|
||||
try {
|
||||
const result = await this.PageSpeedCheck.deleteMany({ monitorId });
|
||||
return result.deletedCount;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "deletePageSpeedChecksByMonitorId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
export default PageSpeedCheckModule;
|
||||
@@ -33,10 +33,9 @@ class CheckService {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
|
||||
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
|
||||
let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
|
||||
const result = await this.db.checkModule.getChecksByMonitor({
|
||||
monitorId,
|
||||
type,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
filter,
|
||||
|
||||
@@ -54,6 +54,7 @@ class SuperSimpleQueue {
|
||||
id: monitorId.toString(),
|
||||
template: "monitor-job",
|
||||
repeat: monitor.interval,
|
||||
active: monitor.isActive,
|
||||
data: monitor.toObject(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,6 +2,16 @@ const SERVICE_NAME = "JobQueueHelper";
|
||||
|
||||
class SuperSimpleQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* db: import("../database").Database,
|
||||
* logger: import("../logger").Logger,
|
||||
* networkService: import("../networkService").NetworkService,
|
||||
* statusService: import("../statusService").StatusService,
|
||||
* notificationService: import("../notificationService").NotificationService
|
||||
* }}
|
||||
*/
|
||||
constructor({ db, logger, networkService, statusService, notificationService }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
@@ -31,7 +41,8 @@ class SuperSimpleQueueHelper {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const networkResponse = await this.networkService.getStatus(monitor);
|
||||
const networkResponse = await this.networkService.requestStatus(monitor);
|
||||
|
||||
if (!networkResponse) {
|
||||
throw new Error("No network response");
|
||||
}
|
||||
|
||||
@@ -1,32 +1,13 @@
|
||||
const SERVICE_NAME = "BufferService";
|
||||
const TYPE_MAP = {
|
||||
http: "checks",
|
||||
ping: "checks",
|
||||
port: "checks",
|
||||
docker: "checks",
|
||||
pagespeed: "pagespeedChecks",
|
||||
hardware: "hardwareChecks",
|
||||
game: "checks",
|
||||
};
|
||||
|
||||
class BufferService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor({ db, logger, envSettings }) {
|
||||
this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 5000 : 1000 * 60 * 1; // 1 minute
|
||||
this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 1000 : 1000 * 60 * 1; // 1 minute
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.buffers = {
|
||||
checks: [],
|
||||
pagespeedChecks: [],
|
||||
hardwareChecks: [],
|
||||
};
|
||||
this.OPERATION_MAP = {
|
||||
checks: this.db.checkModule.createChecks,
|
||||
pagespeedChecks: this.db.pageSpeedCheckModule.createPageSpeedChecks,
|
||||
hardwareChecks: this.db.hardwareCheckModule.createHardwareChecks,
|
||||
};
|
||||
|
||||
this.buffer = [];
|
||||
this.scheduleNextFlush();
|
||||
this.logger.info({
|
||||
message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`,
|
||||
@@ -39,9 +20,9 @@ class BufferService {
|
||||
return BufferService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
addToBuffer({ check, type }) {
|
||||
addToBuffer({ check }) {
|
||||
try {
|
||||
this.buffers[TYPE_MAP[type]].push(check);
|
||||
this.buffer.push(check);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
@@ -55,7 +36,7 @@ class BufferService {
|
||||
scheduleNextFlush() {
|
||||
this.bufferTimer = setTimeout(async () => {
|
||||
try {
|
||||
await this.flushBuffers();
|
||||
await this.flushBuffer();
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: `Error in flush cycle: ${error.message}`,
|
||||
@@ -69,35 +50,24 @@ class BufferService {
|
||||
}
|
||||
}, this.BUFFER_TIMEOUT);
|
||||
}
|
||||
async flushBuffers() {
|
||||
let items = 0;
|
||||
for (const [bufferName, buffer] of Object.entries(this.buffers)) {
|
||||
items += buffer.length;
|
||||
const operation = this.OPERATION_MAP[bufferName];
|
||||
if (!operation) {
|
||||
this.logger.error({
|
||||
message: `No operation found for ${bufferName}`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffers",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await operation(buffer);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffers",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
this.buffers[bufferName] = [];
|
||||
async flushBuffer() {
|
||||
let items = this.buffer.length;
|
||||
|
||||
try {
|
||||
await this.db.checkModule.createChecks(this.buffer);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffer",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
this.buffer = [];
|
||||
this.logger.debug({
|
||||
message: `Flushed ${items} items`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffers",
|
||||
method: "flushBuffer",
|
||||
});
|
||||
items = 0;
|
||||
}
|
||||
|
||||
493
server/src/service/infrastructure/networkService.js
Executable file → Normal file
493
server/src/service/infrastructure/networkService.js
Executable file → Normal file
@@ -1,23 +1,9 @@
|
||||
import jmespath from "jmespath";
|
||||
import https from "https";
|
||||
import { GameDig } from "gamedig";
|
||||
|
||||
const SERVICE_NAME = "NetworkService";
|
||||
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
|
||||
|
||||
/**
|
||||
* Constructs a new NetworkService instance.
|
||||
*
|
||||
* @param {Object} axios - The axios instance for HTTP requests.
|
||||
* @param {Object} ping - The ping utility for network checks.
|
||||
* @param {Object} logger - The logger instance for logging.
|
||||
* @param {Object} http - The HTTP utility for network operations.
|
||||
* @param {Object} net - The net utility for network operations.
|
||||
*/
|
||||
class NetworkService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor(axios, ping, logger, http, Docker, net, stringService, settingsService) {
|
||||
constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) {
|
||||
this.TYPE_PING = "ping";
|
||||
this.TYPE_HTTP = "http";
|
||||
this.TYPE_PAGESPEED = "pagespeed";
|
||||
@@ -29,6 +15,10 @@ class NetworkService {
|
||||
this.NETWORK_ERROR = 5000;
|
||||
this.PING_ERROR = 5001;
|
||||
this.axios = axios;
|
||||
this.got = got;
|
||||
this.https = https;
|
||||
this.jmespath = jmespath;
|
||||
this.GameDig = GameDig;
|
||||
this.ping = ping;
|
||||
this.logger = logger;
|
||||
this.http = http;
|
||||
@@ -38,151 +28,134 @@ class NetworkService {
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return NetworkService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Times the execution of an asynchronous operation.
|
||||
*
|
||||
* @param {Function} operation - The asynchronous operation to be timed.
|
||||
* @returns {Promise<Object>} An object containing the response, response time, and optionally an error.
|
||||
* @property {Object|null} response - The response from the operation, or null if an error occurred.
|
||||
* @property {number} responseTime - The time taken for the operation to complete, in milliseconds.
|
||||
* @property {Error} [error] - The error object if an error occurred during the operation.
|
||||
*/
|
||||
// Helper functions
|
||||
async timeRequest(operation) {
|
||||
const startTime = Date.now();
|
||||
const start = process.hrtime.bigint();
|
||||
try {
|
||||
const response = await operation();
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
return { response, responseTime };
|
||||
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
|
||||
return { response, responseTime: elapsedMs };
|
||||
} catch (error) {
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
return { response: null, responseTime, error };
|
||||
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
|
||||
return { response: null, responseTime: elapsedMs, error };
|
||||
}
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
async requestStatus(monitor) {
|
||||
const type = monitor?.type || "unknown";
|
||||
switch (type) {
|
||||
case this.TYPE_PING:
|
||||
return await this.requestPing(monitor);
|
||||
case this.TYPE_HTTP:
|
||||
return await this.requestHttp(monitor);
|
||||
case this.TYPE_PAGESPEED:
|
||||
return await this.requestPageSpeed(monitor);
|
||||
case this.TYPE_HARDWARE:
|
||||
return await this.requestHardware(monitor);
|
||||
case this.TYPE_DOCKER:
|
||||
return await this.requestDocker(monitor);
|
||||
case this.TYPE_PORT:
|
||||
return await this.requestPort(monitor);
|
||||
case this.TYPE_GAME:
|
||||
return await this.requestGame(monitor);
|
||||
default:
|
||||
return await this.handleUnsupportedType(type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a ping check to a specified host to verify its availability.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The host URL to ping
|
||||
* @param {string} monitor._id - The unique identifier of the monitor
|
||||
* @returns {Promise<Object>} A promise that resolves to a ping response object
|
||||
* @returns {string} pingResponse.monitorId - The ID of the monitor
|
||||
* @returns {string} pingResponse.type - The type of monitor (always "ping")
|
||||
* @returns {number} pingResponse.responseTime - The time taken for the ping
|
||||
* @returns {Object} pingResponse.payload - The raw ping response data
|
||||
* @returns {boolean} pingResponse.status - Whether the host is alive (true) or not (false)
|
||||
* @returns {number} pingResponse.code - Status code (200 for success, PING_ERROR for failure)
|
||||
* @returns {string} pingResponse.message - Success or failure message
|
||||
* @throws {Error} If there's an error during the ping operation
|
||||
*/
|
||||
async requestPing(monitor) {
|
||||
try {
|
||||
const url = monitor.url;
|
||||
const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(url));
|
||||
if (!monitor?.url) {
|
||||
throw new Error("Monitor URL is required");
|
||||
}
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url));
|
||||
|
||||
if (!response) {
|
||||
throw new Error("Ping failed - no result returned");
|
||||
}
|
||||
|
||||
const pingResponse = {
|
||||
monitorId: monitor._id,
|
||||
type: "ping",
|
||||
status: response.alive,
|
||||
code: 200,
|
||||
responseTime,
|
||||
message: "Success",
|
||||
payload: response,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
pingResponse.status = false;
|
||||
pingResponse.code = this.PING_ERROR;
|
||||
pingResponse.message = "No response";
|
||||
pingResponse.code = 200;
|
||||
pingResponse.message = "Ping failed";
|
||||
return pingResponse;
|
||||
}
|
||||
|
||||
pingResponse.code = 200;
|
||||
pingResponse.status = response.alive;
|
||||
pingResponse.message = "Success";
|
||||
return pingResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestPing";
|
||||
throw error;
|
||||
} catch (err) {
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestPing";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP GET request to a specified URL with optional validation of response data.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The URL to make the HTTP request to
|
||||
* @param {string} [monitor.secret] - Optional Bearer token for authentication
|
||||
* @param {string} monitor._id - The unique identifier of the monitor
|
||||
* @param {string} monitor.name - The name of the monitor
|
||||
* @param {string} monitor.teamId - The team ID associated with the monitor
|
||||
* @param {string} monitor.type - The type of monitor
|
||||
* @param {boolean} [monitor.ignoreTlsErrors] - Whether to ignore TLS certificate errors
|
||||
* @param {string} [monitor.jsonPath] - Optional JMESPath expression to extract data from JSON response
|
||||
* @param {string} [monitor.matchMethod] - Method to match response data ('include', 'regex', or exact match)
|
||||
* @param {string} [monitor.expectedValue] - Expected value to match against response data
|
||||
* @returns {Promise<Object>} A promise that resolves to an HTTP response object
|
||||
* @returns {string} httpResponse.monitorId - The ID of the monitor
|
||||
* @returns {string} httpResponse.teamId - The team ID
|
||||
* @returns {string} httpResponse.type - The type of monitor
|
||||
* @returns {number} httpResponse.responseTime - The time taken for the request
|
||||
* @returns {Object} httpResponse.payload - The response data
|
||||
* @returns {boolean} httpResponse.status - Whether the request was successful and matched expected value (if specified)
|
||||
* @returns {number} httpResponse.code - HTTP status code or NETWORK_ERROR
|
||||
* @returns {string} httpResponse.message - Success or failure message
|
||||
* @throws {Error} If there's an error during the HTTP request or data validation
|
||||
*/
|
||||
async requestHttp(monitor) {
|
||||
const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
|
||||
const httpResponse = {
|
||||
monitorId: _id,
|
||||
teamId: teamId,
|
||||
type,
|
||||
};
|
||||
|
||||
try {
|
||||
const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
|
||||
const config = {};
|
||||
|
||||
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
|
||||
|
||||
if (ignoreTlsErrors === true) {
|
||||
config.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
if (!url) {
|
||||
throw new Error("Monitor URL is required");
|
||||
}
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(() => this.axios.get(url, config));
|
||||
|
||||
const httpResponse = {
|
||||
monitorId: _id,
|
||||
teamId,
|
||||
type,
|
||||
responseTime,
|
||||
payload: response?.data,
|
||||
const config = {
|
||||
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
const code = error.response?.status || this.NETWORK_ERROR;
|
||||
httpResponse.code = code;
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.http.STATUS_CODES[code] || this.stringService.httpNetworkError;
|
||||
return httpResponse;
|
||||
if (ignoreTlsErrors) {
|
||||
config.agent = {
|
||||
https: new this.https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
httpResponse.code = response.status;
|
||||
const response = await this.got(url, config);
|
||||
|
||||
let payload;
|
||||
const contentType = response.headers["content-type"];
|
||||
|
||||
if (contentType && contentType.includes("application/json")) {
|
||||
try {
|
||||
payload = JSON.parse(response.body);
|
||||
} catch {
|
||||
payload = response.body;
|
||||
}
|
||||
} else {
|
||||
payload = response.body;
|
||||
}
|
||||
|
||||
httpResponse.code = response.statusCode;
|
||||
httpResponse.status = response.ok;
|
||||
httpResponse.message = response.statusMessage;
|
||||
httpResponse.responseTime = response.timings.phases.total || 0;
|
||||
httpResponse.payload = payload;
|
||||
httpResponse.timings = response.timings || {};
|
||||
|
||||
if (!expectedValue) {
|
||||
// not configure expected value, return
|
||||
httpResponse.status = true;
|
||||
httpResponse.message = this.http.STATUS_CODES[response.status];
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
// validate if response data match expected value
|
||||
let result = response?.data;
|
||||
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "requestHttp",
|
||||
message: `Job: [${name}](${_id}) match result with expected value`,
|
||||
details: { expectedValue, result, jsonPath, matchMethod },
|
||||
details: { expectedValue, payload, jsonPath, matchMethod },
|
||||
});
|
||||
|
||||
if (jsonPath) {
|
||||
@@ -196,106 +169,75 @@ class NetworkService {
|
||||
}
|
||||
|
||||
try {
|
||||
result = jmespath.search(result, jsonPath);
|
||||
} catch (error) {
|
||||
this.jmespath.search(payload, jsonPath);
|
||||
} catch {
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.stringService.httpJsonPathError;
|
||||
return httpResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (result === null || result === undefined) {
|
||||
return httpResponse;
|
||||
} catch (err) {
|
||||
if (err.name === "HTTPError" || err.name === "RequestError") {
|
||||
httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR;
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.stringService.httpEmptyResult;
|
||||
httpResponse.message = err?.response?.statusCode || err.message;
|
||||
httpResponse.responseTime = err?.timings?.phases?.total || 0;
|
||||
httpResponse.payload = null;
|
||||
httpResponse.timings = err?.timings || {};
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
let match;
|
||||
result = typeof result === "object" ? JSON.stringify(result) : result.toString();
|
||||
if (matchMethod === "include") match = result.includes(expectedValue);
|
||||
else if (matchMethod === "regex") match = new RegExp(expectedValue).test(result);
|
||||
else match = result === expectedValue;
|
||||
|
||||
httpResponse.status = match;
|
||||
httpResponse.message = match ? this.stringService.httpMatchSuccess : this.stringService.httpMatchFail;
|
||||
return httpResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestHttp";
|
||||
throw error;
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestHttp";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the performance of a webpage using Google's PageSpeed Insights API.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The URL of the webpage to analyze
|
||||
* @returns {Promise<Object|undefined>} A promise that resolves to a pagespeed response object or undefined if API key is missing
|
||||
* @returns {string} response.monitorId - The ID of the monitor
|
||||
* @returns {string} response.type - The type of monitor
|
||||
* @returns {number} response.responseTime - The time taken for the analysis
|
||||
* @returns {boolean} response.status - Whether the analysis was successful
|
||||
* @returns {number} response.code - HTTP status code from the PageSpeed API
|
||||
* @returns {string} response.message - Success or failure message
|
||||
* @returns {Object} response.payload - The PageSpeed analysis results
|
||||
* @throws {Error} If there's an error during the PageSpeed analysis
|
||||
*/
|
||||
async requestPagespeed(monitor) {
|
||||
async requestPageSpeed(monitor) {
|
||||
try {
|
||||
const url = monitor.url;
|
||||
const updatedMonitor = { ...monitor };
|
||||
let pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
||||
|
||||
if (!url) {
|
||||
throw new Error("Monitor URL is required");
|
||||
}
|
||||
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
||||
const dbSettings = await this.settingsService.getDBSettings();
|
||||
if (dbSettings?.pagespeedApiKey) {
|
||||
pagespeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
|
||||
pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
|
||||
} else {
|
||||
this.logger.warn({
|
||||
message: "Pagespeed API key not found, job not executed",
|
||||
message: "PageSpeed API key not found, job not executed",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "requestPagespeed",
|
||||
details: { url },
|
||||
});
|
||||
return;
|
||||
}
|
||||
updatedMonitor.url = pagespeedUrl;
|
||||
return await this.requestHttp(updatedMonitor);
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestPagespeed";
|
||||
throw error;
|
||||
return await this.requestHttp({
|
||||
...monitor,
|
||||
url: pageSpeedUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestPageSpeed";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async requestHardware(monitor) {
|
||||
try {
|
||||
return await this.requestHttp(monitor);
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestHardware";
|
||||
throw error;
|
||||
} catch (err) {
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestHardware";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the status of a Docker container by its ID.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The Docker container ID to check
|
||||
* @param {string} monitor._id - The unique identifier of the monitor
|
||||
* @param {string} monitor.type - The type of monitor
|
||||
* @returns {Promise<Object>} A promise that resolves to a docker response object
|
||||
* @returns {string} dockerResponse.monitorId - The ID of the monitor
|
||||
* @returns {string} dockerResponse.type - The type of monitor
|
||||
* @returns {number} dockerResponse.responseTime - The time taken for the container inspection
|
||||
* @returns {boolean} dockerResponse.status - Whether the container is running (true) or not (false)
|
||||
* @returns {number} dockerResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure)
|
||||
* @returns {string} dockerResponse.message - Success or failure message
|
||||
* @throws {Error} If the container is not found or if there's an error inspecting the container
|
||||
*/
|
||||
async requestDocker(monitor) {
|
||||
try {
|
||||
if (!monitor.url) {
|
||||
throw new Error("Monitor URL is required");
|
||||
}
|
||||
|
||||
const docker = new this.Docker({
|
||||
socketPath: "/var/run/docker.sock",
|
||||
handleError: true, // Enable error handling
|
||||
@@ -306,14 +248,17 @@ class NetworkService {
|
||||
if (!containerExists) {
|
||||
throw new Error(this.stringService.dockerNotFound);
|
||||
}
|
||||
const container = docker.getContainer(monitor.url);
|
||||
|
||||
const container = docker.getContainer(monitor.url);
|
||||
const { response, responseTime, error } = await this.timeRequest(() => container.inspect());
|
||||
|
||||
const dockerResponse = {
|
||||
monitorId: monitor._id,
|
||||
type: monitor.type,
|
||||
responseTime,
|
||||
status: response?.State?.Status === "running" ? true : false,
|
||||
code: 200,
|
||||
message: "Docker container status fetched successfully",
|
||||
};
|
||||
|
||||
if (error) {
|
||||
@@ -322,34 +267,15 @@ class NetworkService {
|
||||
dockerResponse.message = error.reason || "Failed to fetch Docker container information";
|
||||
return dockerResponse;
|
||||
}
|
||||
dockerResponse.status = response?.State?.Status === "running" ? true : false;
|
||||
dockerResponse.code = 200;
|
||||
dockerResponse.message = "Docker container status fetched successfully";
|
||||
|
||||
return dockerResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestDocker";
|
||||
throw error;
|
||||
} catch (err) {
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestDocker";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to establish a TCP connection to a specified host and port.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The host URL to connect to
|
||||
* @param {number} monitor.port - The port number to connect to
|
||||
* @param {string} monitor._id - The unique identifier of the monitor
|
||||
* @param {string} monitor.type - The type of monitor
|
||||
* @returns {Promise<Object>} A promise that resolves to a port response object
|
||||
* @returns {string} portResponse.monitorId - The ID of the monitor
|
||||
* @returns {string} portResponse.type - The type of monitor
|
||||
* @returns {number} portResponse.responseTime - The time taken for the connection attempt
|
||||
* @returns {boolean} portResponse.status - Whether the connection was successful
|
||||
* @returns {number} portResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure)
|
||||
* @returns {string} portResponse.message - Success or failure message
|
||||
* @throws {Error} If the connection times out or encounters an error
|
||||
*/
|
||||
async requestPort(monitor) {
|
||||
try {
|
||||
const { url, port } = monitor;
|
||||
@@ -381,21 +307,21 @@ class NetworkService {
|
||||
});
|
||||
|
||||
const portResponse = {
|
||||
code: 200,
|
||||
status: response.success,
|
||||
message: this.stringService.portSuccess,
|
||||
monitorId: monitor._id,
|
||||
type: monitor.type,
|
||||
responseTime,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
portResponse.status = false;
|
||||
portResponse.code = this.NETWORK_ERROR;
|
||||
portResponse.status = false;
|
||||
portResponse.message = this.stringService.portFail;
|
||||
return portResponse;
|
||||
}
|
||||
|
||||
portResponse.status = response.success;
|
||||
portResponse.code = 200;
|
||||
portResponse.message = this.stringService.portSuccess;
|
||||
return portResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
@@ -404,19 +330,55 @@ class NetworkService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unsupported job types by throwing an error with details.
|
||||
*
|
||||
* @param {string} type - The unsupported job type that was provided
|
||||
* @throws {Error} An error with service name, method name and unsupported type message
|
||||
*/
|
||||
handleUnsupportedType(type) {
|
||||
async requestGame(monitor) {
|
||||
try {
|
||||
const { url, port, gameId } = monitor;
|
||||
|
||||
const gameResponse = {
|
||||
code: 200,
|
||||
status: true,
|
||||
message: "Success",
|
||||
monitorId: monitor._id,
|
||||
type: "game",
|
||||
};
|
||||
|
||||
const state = await this.GameDig.query({
|
||||
type: gameId,
|
||||
host: url,
|
||||
port: port,
|
||||
}).catch((error) => {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "requestGame",
|
||||
details: { url, port, gameId },
|
||||
});
|
||||
});
|
||||
|
||||
if (!state) {
|
||||
gameResponse.code = this.NETWORK_ERROR;
|
||||
gameResponse.status = false;
|
||||
gameResponse.message = "No response";
|
||||
return gameResponse;
|
||||
}
|
||||
|
||||
gameResponse.responseTime = state.ping;
|
||||
gameResponse.payload = state;
|
||||
return gameResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestPing";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async handleUnsupportedType(type) {
|
||||
const err = new Error(`Unsupported type: ${type}`);
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "getStatus";
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Other network requests unrelated to monitoring:
|
||||
async requestWebhook(type, url, body) {
|
||||
try {
|
||||
const response = await this.axios.post(url, body, {
|
||||
@@ -471,93 +433,6 @@ class NetworkService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the status of a game monitor.
|
||||
*
|
||||
* @param {Object} monitor - The monitor object to request the status for.
|
||||
* @returns {Promise<Object>} The response from the game status request.
|
||||
* @throws {Error} Throws an error if the request fails or if the monitor is not configured correctly.
|
||||
* @property {string} monitorId - The ID of the monitor.
|
||||
* @property {string} type - The type of the monitor (should be "game").
|
||||
* @property {number} responseTime - The time taken for the request.
|
||||
* @property {Object|null} payload - The game state response or null if the request failed.
|
||||
* @property {boolean} status - Indicates if the request was successful (true) or not (false).
|
||||
* @property {number} code - The status code of the request (200 for success, NETWORK_ERROR for failure).
|
||||
* @property {string} message - A message indicating the result of the request.
|
||||
*/
|
||||
async requestGame(monitor) {
|
||||
try {
|
||||
const { url, port, gameId } = monitor;
|
||||
|
||||
const gameResponse = {
|
||||
code: 200,
|
||||
status: true,
|
||||
message: "Success",
|
||||
monitorId: monitor._id,
|
||||
type: "game",
|
||||
};
|
||||
|
||||
const state = await GameDig.query({
|
||||
type: gameId,
|
||||
host: url,
|
||||
port: port,
|
||||
}).catch((error) => {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "requestGame",
|
||||
details: { url, port, gameId },
|
||||
});
|
||||
});
|
||||
|
||||
if (!state) {
|
||||
gameResponse.status = false;
|
||||
gameResponse.code = this.NETWORK_ERROR;
|
||||
gameResponse.message = "No response";
|
||||
return gameResponse;
|
||||
}
|
||||
|
||||
gameResponse.responseTime = state.ping;
|
||||
gameResponse.payload = state;
|
||||
return gameResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestPing";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of a job based on its type and returns the appropriate response.
|
||||
*
|
||||
* @param {Object} job - The job object containing the data for the status request.
|
||||
* @param {Object} job.data - The data object within the job.
|
||||
* @param {string} job.data.type - The type of the job (e.g., "ping", "http", "pagespeed", "hardware").
|
||||
* @returns {Promise<Object>} The response object from the appropriate request method.
|
||||
* @throws {Error} Throws an error if the job type is unsupported.
|
||||
*/
|
||||
async getStatus(monitor) {
|
||||
const type = monitor.type ?? "unknown";
|
||||
switch (type) {
|
||||
case this.TYPE_PING:
|
||||
return await this.requestPing(monitor);
|
||||
case this.TYPE_HTTP:
|
||||
return await this.requestHttp(monitor);
|
||||
case this.TYPE_PAGESPEED:
|
||||
return await this.requestPagespeed(monitor);
|
||||
case this.TYPE_HARDWARE:
|
||||
return await this.requestHardware(monitor);
|
||||
case this.TYPE_DOCKER:
|
||||
return await this.requestDocker(monitor);
|
||||
case this.TYPE_PORT:
|
||||
return await this.requestPort(monitor);
|
||||
case this.TYPE_GAME:
|
||||
return await this.requestGame(monitor);
|
||||
default:
|
||||
return this.handleUnsupportedType(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import MonitorStats from "../../db/models/MonitorStats.js";
|
||||
import { safelyParseFloat } from "../../utils/dataUtils.js";
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
class StatusService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
/**
|
||||
* Creates an instance of StatusService.
|
||||
*
|
||||
* @param {Object} db - The database instance.
|
||||
* @param {Object} logger - The logger instance.
|
||||
*/
|
||||
constructor({ db, logger, buffer }) {
|
||||
* @param {{
|
||||
* buffer: import("./bufferService.js").BufferService
|
||||
* }}
|
||||
*/ constructor({ db, logger, buffer }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.buffer = buffer;
|
||||
@@ -24,7 +21,7 @@ class StatusService {
|
||||
async updateRunningStats({ monitor, networkResponse }) {
|
||||
try {
|
||||
const monitorId = monitor._id;
|
||||
const { responseTime, status, upt_burnt } = networkResponse;
|
||||
const { responseTime, status } = networkResponse;
|
||||
// Get stats
|
||||
let stats = await MonitorStats.findOne({ monitorId });
|
||||
if (!stats) {
|
||||
@@ -36,8 +33,6 @@ class StatusService {
|
||||
totalDownChecks: 0,
|
||||
uptimePercentage: 0,
|
||||
lastCheck: null,
|
||||
timeSInceLastCheck: 0,
|
||||
uptBurnt: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,12 +77,6 @@ class StatusService {
|
||||
// latest check
|
||||
stats.lastCheckTimestamp = new Date().getTime();
|
||||
|
||||
// UPT burned
|
||||
if (typeof upt_burnt !== "undefined" && upt_burnt !== null) {
|
||||
const currentUptBurnt = safelyParseFloat(stats.uptBurnt);
|
||||
const newUptBurnt = safelyParseFloat(upt_burnt);
|
||||
stats.uptBurnt = currentUptBurnt + newUptBurnt;
|
||||
}
|
||||
await stats.save();
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -119,7 +108,8 @@ class StatusService {
|
||||
* @returns {boolean} returnObject.prevStatus - The previous status of the monitor
|
||||
*/
|
||||
updateStatus = async (networkResponse) => {
|
||||
this.insertCheck(networkResponse);
|
||||
const check = this.buildCheck(networkResponse);
|
||||
await this.insertCheck(check);
|
||||
try {
|
||||
const { monitorId, status, code } = networkResponse;
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
@@ -127,31 +117,67 @@ class StatusService {
|
||||
// Update running stats
|
||||
this.updateRunningStats({ monitor, networkResponse });
|
||||
|
||||
// No change in monitor status, return early
|
||||
if (monitor.status === status)
|
||||
// If the status window size has changed, empty
|
||||
while (monitor.statusWindow.length > monitor.statusWindowSize) {
|
||||
monitor.statusWindow.shift();
|
||||
}
|
||||
|
||||
// Update status sliding window
|
||||
monitor.statusWindow.push(status);
|
||||
if (monitor.statusWindow.length > monitor.statusWindowSize) {
|
||||
monitor.statusWindow.shift();
|
||||
}
|
||||
|
||||
if (monitor.status === undefined || monitor.status === null) {
|
||||
monitor.status = status;
|
||||
}
|
||||
|
||||
let newStatus = monitor.status;
|
||||
let statusChanged = false;
|
||||
const prevStatus = monitor.status;
|
||||
|
||||
// Return early if not enough data points
|
||||
if (monitor.statusWindow.length < monitor.statusWindowSize) {
|
||||
await monitor.save();
|
||||
return {
|
||||
monitor,
|
||||
statusChanged: false,
|
||||
prevStatus: monitor.status,
|
||||
prevStatus,
|
||||
code,
|
||||
timestamp: new Date().getTime(),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
// Monitor status changed, save prev status and update monitor
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
message: `${monitor.name} went from ${this.getStatusString(monitor.status)} to ${this.getStatusString(status)}`,
|
||||
prevStatus: monitor.status,
|
||||
newStatus: status,
|
||||
});
|
||||
// Check if threshold has been met
|
||||
const failures = monitor.statusWindow.filter((s) => s === false).length;
|
||||
const failureRate = (failures / monitor.statusWindow.length) * 100;
|
||||
|
||||
const prevStatus = monitor.status;
|
||||
monitor.status = status;
|
||||
// If threshold has been met and the monitor is not already down, mark down:
|
||||
if (failureRate >= monitor.statusWindowThreshold && monitor.status !== false) {
|
||||
newStatus = false;
|
||||
statusChanged = true;
|
||||
}
|
||||
// If the failure rate is below the threshold and the monitor is down, recover:
|
||||
else if (failureRate <= monitor.statusWindowThreshold && monitor.status === false) {
|
||||
newStatus = true;
|
||||
statusChanged = true;
|
||||
}
|
||||
|
||||
if (statusChanged) {
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`,
|
||||
prevStatus,
|
||||
newStatus,
|
||||
});
|
||||
}
|
||||
|
||||
monitor.status = newStatus;
|
||||
await monitor.save();
|
||||
|
||||
return {
|
||||
monitor,
|
||||
statusChanged: true,
|
||||
statusChanged,
|
||||
prevStatus,
|
||||
code,
|
||||
timestamp: new Date().getTime(),
|
||||
@@ -192,14 +218,17 @@ class StatusService {
|
||||
conn_took,
|
||||
connect_took,
|
||||
tls_took,
|
||||
timings,
|
||||
} = networkResponse;
|
||||
|
||||
const check = {
|
||||
monitorId,
|
||||
teamId,
|
||||
type,
|
||||
status,
|
||||
statusCode: code,
|
||||
responseTime,
|
||||
timings: timings || {},
|
||||
message,
|
||||
first_byte_took,
|
||||
body_read_took,
|
||||
@@ -262,25 +291,24 @@ class StatusService {
|
||||
* @param {Object} networkResponse.payload - The payload of the response.
|
||||
* @returns {Promise<void>} A promise that resolves when the check is inserted.
|
||||
*/
|
||||
insertCheck = async (networkResponse) => {
|
||||
insertCheck = async (check) => {
|
||||
try {
|
||||
const check = this.buildCheck(networkResponse);
|
||||
if (typeof check === "undefined") {
|
||||
this.logger.warn({
|
||||
message: "Failed to build check",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "insertCheck",
|
||||
details: networkResponse,
|
||||
});
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this.buffer.addToBuffer({ check, type: networkResponse.type });
|
||||
this.buffer.addToBuffer({ check });
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: error.service || this.SERVICE_NAME,
|
||||
method: error.method || "insertCheck",
|
||||
details: error.details || `Error inserting check for monitor: ${networkResponse?.monitorId}`,
|
||||
details: error.details || `Error inserting check for monitor: ${check?.monitorId}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -155,6 +155,8 @@ const createMonitorBodyValidation = joi.object({
|
||||
name: joi.string().required(),
|
||||
description: joi.string().required(),
|
||||
type: joi.string().required(),
|
||||
statusWindowSize: joi.number().min(1).max(20).default(5),
|
||||
statusWindowThreshold: joi.number().min(1).max(100).default(60),
|
||||
url: joi.string().required(),
|
||||
ignoreTlsErrors: joi.boolean().default(false),
|
||||
port: joi.number(),
|
||||
@@ -183,6 +185,8 @@ const createMonitorsBodyValidation = joi.array().items(
|
||||
|
||||
const editMonitorBodyValidation = joi.object({
|
||||
name: joi.string(),
|
||||
statusWindowSize: joi.number().min(1).max(20).default(5),
|
||||
statusWindowThreshold: joi.number().min(1).max(100).default(60),
|
||||
description: joi.string(),
|
||||
interval: joi.number(),
|
||||
notifications: joi.array().items(joi.string()),
|
||||
|
||||
Reference in New Issue
Block a user