mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-19 07:58:46 -05:00
Merge pull request #3127 from bluewave-labs/feat/monitor-service-ts
Feat/monitor service ts
This commit is contained in:
@@ -85,7 +85,12 @@ const Search = ({
|
||||
const enhancedOptions = React.useMemo(() => {
|
||||
return multiple && isAdorned
|
||||
? [
|
||||
{ [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" },
|
||||
{
|
||||
[filteredBy]: t("selectAll"),
|
||||
isSelectAll: true,
|
||||
_id: "select_all",
|
||||
id: "select_all",
|
||||
},
|
||||
...options,
|
||||
]
|
||||
: options;
|
||||
@@ -93,7 +98,7 @@ const Search = ({
|
||||
const isOptionSelected = (option) => {
|
||||
if (!multiple && !isAdorned) return false;
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => item._id === option._id);
|
||||
return value.some((item) => (item._id ?? item.id) === (option._id ?? option.id));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -145,7 +150,9 @@ const Search = ({
|
||||
disableClearable
|
||||
options={enhancedOptions}
|
||||
getOptionLabel={(option) => option[filteredBy]}
|
||||
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
|
||||
isOptionEqualToValue={(option, value) =>
|
||||
(option._id ?? option.id) === (value?._id ?? value?.id)
|
||||
} // Compare by unique identifier
|
||||
renderInput={(params) => (
|
||||
<FieldWrapper
|
||||
label={label}
|
||||
@@ -206,7 +213,7 @@ const Search = ({
|
||||
return filtered;
|
||||
}}
|
||||
getOptionKey={(option) => {
|
||||
return option._id;
|
||||
return option._id ?? option.id;
|
||||
}}
|
||||
renderOption={(props, option) => {
|
||||
const { key, ...optionProps } = props;
|
||||
|
||||
@@ -62,6 +62,7 @@ const Select = ({
|
||||
fieldWrapperSx = {},
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const getItemValue = (item) => item?._id ?? item?.id;
|
||||
const itemStyles = {
|
||||
fontSize: "var(--env-var-font-size-medium)",
|
||||
color: theme.palette.primary.contrastTextTertiary,
|
||||
@@ -123,7 +124,7 @@ const Select = ({
|
||||
...sx,
|
||||
}}
|
||||
renderValue={(selected) => {
|
||||
const selectedItem = items.find((item) => item._id === selected);
|
||||
const selectedItem = items.find((item) => getItemValue(item) === selected);
|
||||
const displayName = selectedItem ? selectedItem.name : placeholder;
|
||||
return (
|
||||
<Typography
|
||||
@@ -152,17 +153,25 @@ const Select = ({
|
||||
{placeholder}
|
||||
</MenuItem>
|
||||
)}
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
value={item._id}
|
||||
key={`${id}-${item._id}`}
|
||||
sx={{
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
{items
|
||||
.map((item) => {
|
||||
const itemValue = getItemValue(item);
|
||||
if (itemValue === undefined || itemValue === null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
value={itemValue}
|
||||
key={`${id}-${itemValue}`}
|
||||
sx={{
|
||||
...itemStyles,
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
.filter(Boolean)}
|
||||
</MuiSelect>
|
||||
</FieldWrapper>
|
||||
);
|
||||
@@ -179,8 +188,8 @@ Select.propTypes = {
|
||||
.isRequired,
|
||||
items: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
|
||||
.isRequired,
|
||||
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
||||
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useMonitorUtils } from "./useMonitorUtils.js";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
|
||||
export const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [monitorsSummary, setMonitorsSummary] = useState(undefined);
|
||||
@@ -37,7 +37,7 @@ const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => {
|
||||
return [monitors, monitorsSummary, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchMonitorsWithChecks = ({
|
||||
export const useFetchMonitorsWithChecks = ({
|
||||
types,
|
||||
limit,
|
||||
page,
|
||||
@@ -100,22 +100,9 @@ const useFetchMonitorsWithChecks = ({
|
||||
return [monitors, count, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchMonitorsByTeamId = ({
|
||||
types,
|
||||
limit,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
checkOrder,
|
||||
normalize,
|
||||
status,
|
||||
updateTrigger,
|
||||
}) => {
|
||||
export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [monitors, setMonitors] = useState(undefined);
|
||||
const [summary, setSummary] = useState(undefined);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -123,20 +110,11 @@ const useFetchMonitorsByTeamId = ({
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const res = await networkService.getMonitorsByTeamId({
|
||||
limit,
|
||||
types,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
checkOrder,
|
||||
status,
|
||||
normalize,
|
||||
});
|
||||
if (res?.data?.data?.filteredMonitors) {
|
||||
setMonitors(res.data.data.filteredMonitors);
|
||||
setSummary(res.data.data.summary);
|
||||
if (res?.data?.data) {
|
||||
setMonitors(res.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
@@ -148,23 +126,11 @@ const useFetchMonitorsByTeamId = ({
|
||||
}
|
||||
};
|
||||
fetchMonitors();
|
||||
}, [
|
||||
types,
|
||||
limit,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
updateTrigger,
|
||||
checkOrder,
|
||||
normalize,
|
||||
status,
|
||||
]);
|
||||
return [monitors, summary, isLoading, networkError];
|
||||
}, [types, filter, updateTrigger]);
|
||||
return [monitors, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchStatsByMonitorId = ({
|
||||
export const useFetchStatsByMonitorId = ({
|
||||
monitorId,
|
||||
sortOrder,
|
||||
limit,
|
||||
@@ -203,7 +169,7 @@ const useFetchStatsByMonitorId = ({
|
||||
return [monitor, audits, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
|
||||
export const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
const fetchGames = async () => {
|
||||
@@ -222,7 +188,7 @@ const useFetchMonitorGames = ({ setGames, updateTrigger }) => {
|
||||
return [isLoading];
|
||||
};
|
||||
|
||||
const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
|
||||
export const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
if (typeof monitorId === "undefined") {
|
||||
@@ -245,7 +211,7 @@ const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
|
||||
return [isLoading];
|
||||
};
|
||||
|
||||
const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
|
||||
export const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [monitor, setMonitor] = useState(undefined);
|
||||
@@ -271,8 +237,34 @@ const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) =>
|
||||
}, [monitorId, dateRange, updateTrigger]);
|
||||
return [monitor, isLoading, networkError];
|
||||
};
|
||||
export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [monitor, setMonitor] = useState(undefined);
|
||||
|
||||
const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
|
||||
useEffect(() => {
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
if (!monitorId) {
|
||||
return { monitor: undefined, isLoading: false, networkError: undefined };
|
||||
}
|
||||
const response = await networkService.getPageSpeedDetailsByMonitorId({
|
||||
monitorId: monitorId,
|
||||
dateRange: dateRange,
|
||||
});
|
||||
setMonitor(response.data.data);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchMonitor();
|
||||
}, [monitorId, dateRange, updateTrigger]);
|
||||
return [monitor, isLoading, networkError];
|
||||
};
|
||||
|
||||
export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [monitor, setMonitor] = useState(undefined);
|
||||
@@ -300,7 +292,7 @@ const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
|
||||
return [monitor, monitorStats, isLoading, networkError];
|
||||
};
|
||||
|
||||
const useCreateMonitor = () => {
|
||||
export const useCreateMonitor = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const createMonitor = async ({ monitor, redirect }) => {
|
||||
@@ -320,7 +312,7 @@ const useCreateMonitor = () => {
|
||||
return [createMonitor, isLoading];
|
||||
};
|
||||
|
||||
const useFetchGlobalSettings = () => {
|
||||
export const useFetchGlobalSettings = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [globalSettings, setGlobalSettings] = useState(undefined);
|
||||
useEffect(() => {
|
||||
@@ -342,7 +334,7 @@ const useFetchGlobalSettings = () => {
|
||||
return [globalSettings, isLoading];
|
||||
};
|
||||
|
||||
const useDeleteMonitor = () => {
|
||||
export const useDeleteMonitor = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const deleteMonitor = async ({ monitor, redirect }) => {
|
||||
@@ -363,7 +355,7 @@ const useDeleteMonitor = () => {
|
||||
return [deleteMonitor, isLoading];
|
||||
};
|
||||
|
||||
const useUpdateMonitor = () => {
|
||||
export const useUpdateMonitor = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const updateMonitor = async ({ monitor, redirect }) => {
|
||||
@@ -410,7 +402,7 @@ const useUpdateMonitor = () => {
|
||||
return [updateMonitor, isLoading];
|
||||
};
|
||||
|
||||
const usePauseMonitor = () => {
|
||||
export const usePauseMonitor = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState(undefined);
|
||||
const pauseMonitor = async ({ monitorId, triggerUpdate }) => {
|
||||
@@ -433,7 +425,7 @@ const usePauseMonitor = () => {
|
||||
return [pauseMonitor, isLoading, error];
|
||||
};
|
||||
|
||||
const useAddDemoMonitors = () => {
|
||||
export const useAddDemoMonitors = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const addDemoMonitors = async () => {
|
||||
@@ -450,7 +442,7 @@ const useAddDemoMonitors = () => {
|
||||
return [addDemoMonitors, isLoading];
|
||||
};
|
||||
|
||||
const useDeleteAllMonitors = () => {
|
||||
export const useDeleteAllMonitors = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const deleteAllMonitors = async () => {
|
||||
@@ -467,7 +459,7 @@ const useDeleteAllMonitors = () => {
|
||||
return [deleteAllMonitors, isLoading];
|
||||
};
|
||||
|
||||
const useDeleteMonitorStats = () => {
|
||||
export const useDeleteMonitorStats = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const deleteMonitorStats = async () => {
|
||||
@@ -485,7 +477,7 @@ const useDeleteMonitorStats = () => {
|
||||
return [deleteMonitorStats, isLoading];
|
||||
};
|
||||
|
||||
const useCreateBulkMonitors = () => {
|
||||
export const useCreateBulkMonitors = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const createBulkMonitors = async (file, user) => {
|
||||
@@ -508,7 +500,7 @@ const useCreateBulkMonitors = () => {
|
||||
return [createBulkMonitors, isLoading];
|
||||
};
|
||||
|
||||
const useExportMonitors = () => {
|
||||
export const useExportMonitors = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -541,7 +533,7 @@ const useExportMonitors = () => {
|
||||
return [exportMonitors, isLoading];
|
||||
};
|
||||
|
||||
const useFetchJson = () => {
|
||||
export const useFetchJson = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const fetchJson = async () => {
|
||||
try {
|
||||
@@ -557,25 +549,3 @@ const useFetchJson = () => {
|
||||
};
|
||||
return [fetchJson, isLoading];
|
||||
};
|
||||
|
||||
export {
|
||||
useFetchMonitorsWithSummary,
|
||||
useFetchMonitorsWithChecks,
|
||||
useFetchMonitorsByTeamId,
|
||||
useFetchStatsByMonitorId,
|
||||
useFetchMonitorById,
|
||||
useFetchUptimeMonitorById,
|
||||
useFetchHardwareMonitorById,
|
||||
useCreateMonitor,
|
||||
useFetchGlobalSettings,
|
||||
useDeleteMonitor,
|
||||
useUpdateMonitor,
|
||||
usePauseMonitor,
|
||||
useAddDemoMonitors,
|
||||
useDeleteAllMonitors,
|
||||
useDeleteMonitorStats,
|
||||
useCreateBulkMonitors,
|
||||
useExportMonitors,
|
||||
useFetchMonitorGames,
|
||||
useFetchJson,
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ const Incidents2 = () => {
|
||||
setUpdateTrigger((prev) => !prev);
|
||||
};
|
||||
|
||||
const [monitors, , isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId(
|
||||
const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId(
|
||||
{}
|
||||
);
|
||||
|
||||
|
||||
@@ -13,10 +13,7 @@ import { useTheme } from "@emotion/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
useFetchMonitorsByTeamId,
|
||||
useFetchMonitorsWithChecks,
|
||||
} from "@/Hooks/monitorHooks.js";
|
||||
import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { setRowsPerPage } from "../../../Features/UI/uiSlice.js";
|
||||
|
||||
@@ -24,7 +24,7 @@ const MonitorListItem = ({ monitor, onDelete }) => {
|
||||
|
||||
MonitorListItem.propTypes = {
|
||||
monitor: PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
@@ -33,7 +33,7 @@ MonitorListItem.propTypes = {
|
||||
const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => {
|
||||
const onDelete = (monitorToDelete) => {
|
||||
const newMonitors = selectedMonitors.filter(
|
||||
(monitor) => monitor._id !== monitorToDelete._id
|
||||
(monitor) => monitor.id !== monitorToDelete.id
|
||||
);
|
||||
setSelectedMonitors(newMonitors);
|
||||
};
|
||||
@@ -47,7 +47,7 @@ const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => {
|
||||
>
|
||||
{selectedMonitors?.map((monitor) => (
|
||||
<MonitorListItem
|
||||
key={monitor._id}
|
||||
key={monitor.id}
|
||||
monitor={monitor}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
@@ -59,7 +59,7 @@ const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => {
|
||||
MonitorList.propTypes = {
|
||||
selectedMonitors: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
|
||||
@@ -22,6 +22,12 @@ const useMaintenanceActions = () => {
|
||||
weekly: MS_PER_DAY * 7,
|
||||
};
|
||||
const handleSubmitForm = async (maintenanceWindowId, form) => {
|
||||
const toStringId = (monitor) => {
|
||||
if (!monitor) {
|
||||
return "";
|
||||
}
|
||||
return monitor._id ?? monitor.id ?? "";
|
||||
};
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
.set("minute", form.startTime.minute());
|
||||
@@ -33,7 +39,7 @@ const useMaintenanceActions = () => {
|
||||
const repeat = REPEAT_LOOKUP[form.repeat];
|
||||
|
||||
const submit = {
|
||||
monitors: form.monitors.map((monitor) => monitor._id),
|
||||
monitors: form.monitors.map((monitor) => toStringId(monitor)).filter(Boolean),
|
||||
name: form.name,
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
|
||||
@@ -41,7 +41,7 @@ const useMaintenanceData = () => {
|
||||
limit: null,
|
||||
types: ["http", "ping", "pagespeed", "port"],
|
||||
});
|
||||
const fetchedMonitors = response.data.data.monitors;
|
||||
const fetchedMonitors = response.data.data;
|
||||
return fetchedMonitors;
|
||||
};
|
||||
|
||||
@@ -55,7 +55,9 @@ const useMaintenanceData = () => {
|
||||
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 monitor = monitorList.find(
|
||||
(monitor) => (monitor._id ?? monitor.id) === monitorId
|
||||
);
|
||||
const maintenanceWindowInformation = {
|
||||
name,
|
||||
repeat: REVERSE_REPEAT_LOOKUP[repeat],
|
||||
|
||||
@@ -108,6 +108,7 @@ const CreateMaintenance = () => {
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return;
|
||||
|
||||
const request = handleSubmitForm(maintenanceWindowId, form);
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -11,14 +11,13 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useFetchStatsByMonitorId } from "../../../Hooks/monitorHooks.js";
|
||||
import { useFetchPageSpeedMonitorById } from "../../../Hooks/monitorHooks.js";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
// Constants
|
||||
const BREADCRUMBS = [
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "details", path: `` },
|
||||
// { name: "details", path: `/pagespeed/${monitorId}` }, // Not needed?
|
||||
];
|
||||
|
||||
const PageSpeedDetails = () => {
|
||||
@@ -36,13 +35,9 @@ const PageSpeedDetails = () => {
|
||||
});
|
||||
const [trigger, setTrigger] = useState(false);
|
||||
// Network
|
||||
const [monitor, audits, isLoading, networkError] = useFetchStatsByMonitorId({
|
||||
const [monitor, isLoading, networkError] = useFetchPageSpeedMonitorById({
|
||||
monitorId,
|
||||
sortOrder: "desc",
|
||||
limit: 50,
|
||||
dateRange: "day",
|
||||
numToDisplay: null,
|
||||
normalize: null,
|
||||
updateTrigger: trigger,
|
||||
});
|
||||
|
||||
@@ -116,7 +111,7 @@ const PageSpeedDetails = () => {
|
||||
/>
|
||||
<PerformanceReport
|
||||
shouldRender={!isLoading}
|
||||
audits={audits}
|
||||
audits={monitor?.checks[0]?.audits}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -11,10 +11,7 @@ import FallbackPageSpeedWarning from "@/Components/v1/Fallback/FallbackPageSpeed
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||
import {
|
||||
useFetchMonitorsByTeamId,
|
||||
useFetchMonitorsWithChecks,
|
||||
} from "@/Hooks/monitorHooks.js";
|
||||
import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js";
|
||||
import { useFetchSettings } from "@/Hooks/settingsHooks.js";
|
||||
// Constants
|
||||
const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
|
||||
@@ -23,16 +20,6 @@ const PageSpeed = () => {
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
|
||||
// const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({
|
||||
// limit: 10,
|
||||
// types: TYPES,
|
||||
// page: null,
|
||||
// rowsPerPage: null,
|
||||
// filter: null,
|
||||
// field: null,
|
||||
// order: null,
|
||||
// });
|
||||
|
||||
const [
|
||||
monitorsWithChecks,
|
||||
monitorsWithChecksCount,
|
||||
|
||||
@@ -43,7 +43,7 @@ const MonitorListItem = ({
|
||||
const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => {
|
||||
const onDelete = (monitorToDelete) => {
|
||||
const newMonitors = selectedMonitors.filter(
|
||||
(monitor) => monitor._id !== monitorToDelete._id
|
||||
(monitor) => monitor.id !== monitorToDelete.id
|
||||
);
|
||||
setSelectedMonitors(newMonitors);
|
||||
};
|
||||
@@ -80,8 +80,8 @@ const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => {
|
||||
>
|
||||
{selectedMonitors?.map((monitor, index) => (
|
||||
<Draggable
|
||||
key={monitor._id}
|
||||
draggableId={monitor._id}
|
||||
key={monitor.id}
|
||||
draggableId={monitor.id}
|
||||
index={index}
|
||||
>
|
||||
{(provided, snapshot) => (
|
||||
|
||||
@@ -24,7 +24,7 @@ const Content = ({
|
||||
// Handlers
|
||||
const handleMonitorsChange = (selectedMonitors) => {
|
||||
handleFormChange({
|
||||
target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor._id) },
|
||||
target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor.id) },
|
||||
});
|
||||
setSelectedMonitors(selectedMonitors);
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ const useMonitorsFetch = () => {
|
||||
limit: null, // donot return any checks for the monitors
|
||||
types: ["http", "ping", "port", "game"], // include game servers in status page monitor selection
|
||||
});
|
||||
setMonitors(response.data.data.monitors);
|
||||
setMonitors(response.data.data);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({ body: error.message });
|
||||
|
||||
@@ -62,7 +62,6 @@ const CreateStatusPage = () => {
|
||||
useStatusPageFetch(isCreate, url);
|
||||
const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url);
|
||||
|
||||
console.log(JSON.stringify(form, null, 2));
|
||||
// Handlers
|
||||
const handleFormChange = (e) => {
|
||||
let { type, name, value, checked } = e.target;
|
||||
@@ -144,6 +143,8 @@ const CreateStatusPage = () => {
|
||||
...form,
|
||||
logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null },
|
||||
};
|
||||
|
||||
console.log(toSubmit);
|
||||
const { error } = statusPageValidation.validate(toSubmit, {
|
||||
abortEarly: false,
|
||||
});
|
||||
@@ -205,7 +206,7 @@ const CreateStatusPage = () => {
|
||||
companyName: statusPage?.companyName,
|
||||
isPublished: statusPage?.isPublished,
|
||||
timezone: statusPage?.timezone,
|
||||
monitors: statusPageMonitors.map((monitor) => monitor._id),
|
||||
monitors: statusPageMonitors.map((monitor) => monitor.id),
|
||||
color: statusPage?.color,
|
||||
logo: newLogo,
|
||||
showCharts: statusPage?.showCharts ?? true,
|
||||
|
||||
@@ -28,14 +28,14 @@ const MonitorsList = ({
|
||||
const status = determineState(monitor);
|
||||
return (
|
||||
<Stack
|
||||
key={monitor._id}
|
||||
key={monitor.id}
|
||||
width="100%"
|
||||
gap={theme.spacing(10)}
|
||||
margin="0 auto"
|
||||
maxWidth="95%"
|
||||
>
|
||||
<Host
|
||||
key={monitor._id}
|
||||
key={monitor.id}
|
||||
url={monitor.url}
|
||||
title={monitor.name}
|
||||
percentageColor={monitor.percentageColor}
|
||||
|
||||
@@ -144,20 +144,16 @@ class NetworkService {
|
||||
*/
|
||||
|
||||
async getMonitorsByTeamId(config) {
|
||||
const { limit, types, page, rowsPerPage, filter, field, order } = config;
|
||||
const { types, filter } = config ?? {};
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (limit) params.append("limit", limit);
|
||||
if (types) {
|
||||
types.forEach((type) => {
|
||||
params.append("type", type);
|
||||
});
|
||||
}
|
||||
if (page) params.append("page", page);
|
||||
if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
|
||||
if (filter) params.append("filter", filter);
|
||||
if (field) params.append("field", field);
|
||||
if (order) params.append("order", order);
|
||||
if (filter !== undefined && filter !== null && filter !== "")
|
||||
params.append("filter", filter);
|
||||
|
||||
return this.axiosInstance.get(`/monitors/team?${params.toString()}`, {
|
||||
headers: {
|
||||
@@ -202,6 +198,14 @@ class NetworkService {
|
||||
`/monitors/hardware/details/${config.monitorId}?${params.toString()}`
|
||||
);
|
||||
}
|
||||
async getPageSpeedDetailsByMonitorId(config) {
|
||||
const params = new URLSearchParams();
|
||||
if (config.dateRange) params.append("dateRange", config.dateRange);
|
||||
|
||||
return this.axiosInstance.get(
|
||||
`/monitors/pagespeed/details/${config.monitorId}?${params.toString()}`
|
||||
);
|
||||
}
|
||||
async getUptimeDetailsById(config) {
|
||||
const params = new URLSearchParams();
|
||||
if (config.dateRange) params.append("dateRange", config.dateRange);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
rootDir: ".",
|
||||
testEnvironment: "node",
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
transform: {
|
||||
"^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
},
|
||||
testMatch: ["<rootDir>/test/**/*.test.ts"],
|
||||
setupFilesAfterEnv: [],
|
||||
collectCoverageFrom: ["src/**/*.ts"],
|
||||
coveragePathIgnorePatterns: ["/node_modules/", "/test/"],
|
||||
clearMocks: true,
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
+3304
-359
File diff suppressed because it is too large
Load Diff
+10
-4
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "c8 mocha",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand",
|
||||
"dev": "nodemon --exec tsx src/index.js",
|
||||
"start": "node --watch ./dist/index.js",
|
||||
"build": "tsc && tsc-alias",
|
||||
@@ -58,21 +58,27 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/dockerode": "^4.0.0",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/gamedig": "^5.0.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/mjml": "^4.7.4",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/nodemailer": "7.0.1",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/ping": "0.4.4",
|
||||
"c8": "10.1.3",
|
||||
"chai": "5.2.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-mocha": "^10.5.0",
|
||||
"esm": "3.2.25",
|
||||
"globals": "^15.14.0",
|
||||
"mocha": "11.1.0",
|
||||
"jest": "^30.2.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"prettier": "^3.3.3",
|
||||
"sinon": "19.0.2",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.20.5",
|
||||
"typescript": "5.9.2"
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import mongoose from "mongoose";
|
||||
import { MonitorModel } from "../dist/db/models/Monitor.js";
|
||||
import { CheckModel } from "../dist/db/models/Check.js";
|
||||
|
||||
const DEFAULT_MONITOR_ID = "000000000000000000000001";
|
||||
const DEFAULT_TEAM_ID = "0000000000000000000000aa";
|
||||
const DEFAULT_USER_ID = "0000000000000000000000bb";
|
||||
const DEFAULT_MONITOR_TYPE = "http";
|
||||
const DEFAULT_TOTAL = 1_000_000;
|
||||
const DEFAULT_BATCH_SIZE = 5_000;
|
||||
|
||||
const parseObjectId = (value, fallback) => {
|
||||
try {
|
||||
return new mongoose.Types.ObjectId(value || fallback);
|
||||
} catch (error) {
|
||||
console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`);
|
||||
return new mongoose.Types.ObjectId(fallback);
|
||||
}
|
||||
};
|
||||
|
||||
async function ensureMonitor({ monitorId, teamId, userId, type }) {
|
||||
const existing = await MonitorModel.findById(monitorId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
console.log(`Monitor ${monitorId.toString()} not found, creating it.`);
|
||||
const monitor = new MonitorModel({
|
||||
_id: monitorId,
|
||||
userId,
|
||||
teamId,
|
||||
name: `Seed Monitor ${monitorId.toString()}`,
|
||||
description: "Synthetic monitor for performance testing",
|
||||
statusWindow: [],
|
||||
statusWindowSize: 5,
|
||||
statusWindowThreshold: 60,
|
||||
type,
|
||||
ignoreTlsErrors: false,
|
||||
url: "https://example.com",
|
||||
isActive: true,
|
||||
interval: 60000,
|
||||
alertThreshold: 5,
|
||||
cpuAlertThreshold: 5,
|
||||
memoryAlertThreshold: 5,
|
||||
diskAlertThreshold: 5,
|
||||
tempAlertThreshold: 5,
|
||||
selectedDisks: [],
|
||||
});
|
||||
|
||||
await monitor.save();
|
||||
return monitor;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const mongoUri = process.env.MONGO_URI ?? "mongodb://localhost:27017/uptime_db";
|
||||
const monitorId = parseObjectId(process.env.MONITOR_ID ?? DEFAULT_MONITOR_ID, DEFAULT_MONITOR_ID);
|
||||
const teamId = parseObjectId("69648b0578209af45f9ffe30");
|
||||
const userId = parseObjectId("69648b0678209af45f9ffe32");
|
||||
const monitorType = process.env.MONITOR_TYPE ?? DEFAULT_MONITOR_TYPE;
|
||||
const total = Number(process.env.CHECK_TOTAL ?? DEFAULT_TOTAL);
|
||||
const batchSize = Number(process.env.CHECK_BATCH_SIZE ?? DEFAULT_BATCH_SIZE);
|
||||
|
||||
console.log(`Connecting to MongoDB at ${mongoUri}`);
|
||||
await mongoose.connect(mongoUri);
|
||||
|
||||
await ensureMonitor({ monitorId, teamId, userId, type: monitorType });
|
||||
|
||||
console.log(`Seeding ${total} checks for monitor ${monitorId.toString()} (team ${teamId.toString()}) in batches of ${batchSize}.`);
|
||||
|
||||
const docs = [];
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
const baseTime = Date.now() - (total - i) * 1000;
|
||||
const createdAt = new Date(baseTime);
|
||||
docs.push({
|
||||
metadata: {
|
||||
monitorId,
|
||||
teamId,
|
||||
type: monitorType,
|
||||
},
|
||||
status: i % 50 !== 0,
|
||||
statusCode: i % 50 !== 0 ? 200 : 500,
|
||||
responseTime: Math.floor(Math.random() * 1000),
|
||||
message: i % 50 !== 0 ? "OK" : "Error",
|
||||
expiry: createdAt,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
timings: {
|
||||
start: baseTime,
|
||||
socket: baseTime,
|
||||
lookup: baseTime,
|
||||
connect: baseTime,
|
||||
secureConnect: baseTime,
|
||||
upload: baseTime,
|
||||
response: baseTime + 40,
|
||||
end: baseTime + 45,
|
||||
phases: {
|
||||
wait: 0,
|
||||
dns: 1,
|
||||
tcp: 2,
|
||||
tls: 4,
|
||||
request: 0,
|
||||
firstByte: 30,
|
||||
download: 5,
|
||||
total: 45,
|
||||
},
|
||||
},
|
||||
cpu: {
|
||||
physical_core: 8,
|
||||
logical_core: 16,
|
||||
frequency: 3600,
|
||||
temperature: [50 + Math.random() * 10],
|
||||
free_percent: 40,
|
||||
usage_percent: Math.random() * 100,
|
||||
},
|
||||
memory: {
|
||||
total_bytes: 32 * 1024 ** 3,
|
||||
available_bytes: 16 * 1024 ** 3,
|
||||
used_bytes: 16 * 1024 ** 3,
|
||||
usage_percent: Math.random() * 100,
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
device: "/dev/sda1",
|
||||
mountpoint: "/",
|
||||
read_speed_bytes: Math.random() * 10_000_000,
|
||||
write_speed_bytes: Math.random() * 10_000_000,
|
||||
total_bytes: 512 * 1024 ** 3,
|
||||
free_bytes: 128 * 1024 ** 3,
|
||||
usage_percent: Math.random() * 100,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
os: "linux",
|
||||
platform: "ubuntu",
|
||||
kernel_version: "5.15.0",
|
||||
},
|
||||
net: [
|
||||
{
|
||||
name: "eth0",
|
||||
bytes_sent: Math.random() * 10_000_000,
|
||||
bytes_recv: Math.random() * 10_000_000,
|
||||
packets_sent: Math.random() * 1_000_000,
|
||||
packets_recv: Math.random() * 1_000_000,
|
||||
err_in: 0,
|
||||
err_out: 0,
|
||||
drop_in: 0,
|
||||
drop_out: 0,
|
||||
fifo_in: 0,
|
||||
fifo_out: 0,
|
||||
},
|
||||
],
|
||||
errors: i % 50 === 0 ? [{ metric: ["uptime"], err: "500" }] : [],
|
||||
});
|
||||
|
||||
if (docs.length === batchSize) {
|
||||
await CheckModel.insertMany(docs, { ordered: false });
|
||||
console.log(`Inserted ${i + 1} / ${total}`);
|
||||
docs.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (docs.length > 0) {
|
||||
await CheckModel.insertMany(docs, { ordered: false });
|
||||
}
|
||||
|
||||
await mongoose.disconnect();
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
console.log(`Finished inserting ${total} checks in ${duration}s`);
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Failed to seed checks", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -16,7 +16,7 @@ import CheckService from "../service/business/checkService.js";
|
||||
import DiagnosticService from "../service/business/diagnosticService.js";
|
||||
import InviteService from "../service/business/inviteService.js";
|
||||
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
|
||||
import MonitorService from "../service/business/monitorService.js";
|
||||
import { MonitorService } from "@/service/index.js";
|
||||
import IncidentService from "../service/business/incidentService.js";
|
||||
import papaparse from "papaparse";
|
||||
import axios from "axios";
|
||||
@@ -34,9 +34,8 @@ const { compile } = pkg;
|
||||
import mjml2html from "mjml";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
import { games } from "gamedig";
|
||||
import { games, GameDig } from "gamedig";
|
||||
import jmespath from "jmespath";
|
||||
import { GameDig } from "gamedig";
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import { ObjectId } from "mongodb";
|
||||
@@ -47,7 +46,6 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js";
|
||||
import { ParseBoolean } from "../utils/utils.js";
|
||||
|
||||
// Models
|
||||
import { CheckModel } from "@/db/models/index.js";
|
||||
import Monitor from "../db/models/Monitor.js";
|
||||
import User from "../db/models/User.js";
|
||||
import InviteToken from "../db/models/InviteToken.js";
|
||||
@@ -72,11 +70,11 @@ import SettingsModule from "../db/modules/settingsModule.js";
|
||||
import IncidentModule from "../db/modules/incidentModule.js";
|
||||
|
||||
// repositories
|
||||
import { MongoMonitorsRepository, MongoChecksRepository } from "@/repositories/index.js";
|
||||
import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository } from "@/repositories/index.js";
|
||||
|
||||
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
|
||||
export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => {
|
||||
const serviceRegistry = new ServiceRegistry({ logger });
|
||||
ServiceRegistry.instance = serviceRegistry;
|
||||
(ServiceRegistry as any).instance = serviceRegistry;
|
||||
|
||||
const translationService = new TranslationService(logger);
|
||||
await translationService.initialize();
|
||||
@@ -84,7 +82,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
const stringService = new StringService(translationService);
|
||||
|
||||
// Create DB
|
||||
const checkModule = new CheckModule({ logger, CheckModel, Monitor, User });
|
||||
const checkModule = new CheckModule({ logger, 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 });
|
||||
@@ -125,6 +123,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
// Repositories
|
||||
const monitorsRepository = new MongoMonitorsRepository();
|
||||
const checksRepository = new MongoChecksRepository();
|
||||
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||
|
||||
const networkService = new NetworkService({
|
||||
axios,
|
||||
@@ -152,7 +151,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
|
||||
const bufferService = new BufferService({ db, logger, envSettings, incidentService });
|
||||
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService });
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository });
|
||||
|
||||
const notificationUtils = new NotificationUtils({
|
||||
stringService,
|
||||
@@ -182,6 +181,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
db,
|
||||
logger,
|
||||
helper: superSimpleQueueHelper,
|
||||
monitorsRepository,
|
||||
});
|
||||
|
||||
// Business services
|
||||
@@ -218,7 +218,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
});
|
||||
const monitorService = new MonitorService({
|
||||
db,
|
||||
settingsService,
|
||||
jobQueue: superSimpleQueue,
|
||||
stringService,
|
||||
emailService,
|
||||
@@ -228,6 +227,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
games,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
});
|
||||
|
||||
const services = {
|
||||
@@ -1,3 +1,6 @@
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { type MonitorType, MonitorTypes } from "@/types/index.js";
|
||||
|
||||
const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<any> => {
|
||||
const monitorUrl = new URL(monitor.url);
|
||||
const hostname = monitorUrl.hostname;
|
||||
@@ -8,5 +11,101 @@ const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise<a
|
||||
}
|
||||
return cert;
|
||||
};
|
||||
const requireString = (value: unknown, fieldName: string): string => {
|
||||
if (typeof value === "string" && value.trim().length > 0) {
|
||||
return value;
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} is required`, status: 400 });
|
||||
};
|
||||
|
||||
export { fetchMonitorCertificate };
|
||||
const optionalString = (value: unknown, fieldName: string): string | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
|
||||
};
|
||||
|
||||
const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} must be a number`, status: 400 });
|
||||
};
|
||||
|
||||
const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (value === "true") {
|
||||
return true;
|
||||
}
|
||||
if (value === "false") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 });
|
||||
};
|
||||
|
||||
const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => {
|
||||
const parseSingle = (input: unknown): MonitorType => {
|
||||
if (typeof input !== "string") {
|
||||
throw new AppError({ message: "Monitor type must be a string", status: 400 });
|
||||
}
|
||||
if (!MonitorTypes.includes(input as MonitorType)) {
|
||||
throw new AppError({ message: `Invalid monitor type: ${input}`, status: 400 });
|
||||
}
|
||||
return input as MonitorType;
|
||||
};
|
||||
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => parseSingle(entry));
|
||||
}
|
||||
return parseSingle(value);
|
||||
};
|
||||
|
||||
const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (value === "asc" || value === "desc") {
|
||||
return value;
|
||||
}
|
||||
throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 });
|
||||
};
|
||||
|
||||
const requireTeamId = (teamId?: string): string => {
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
return teamId;
|
||||
};
|
||||
|
||||
export {
|
||||
fetchMonitorCertificate,
|
||||
requireString,
|
||||
optionalString,
|
||||
optionalNumber,
|
||||
optionalBoolean,
|
||||
parseMonitorTypeFilter,
|
||||
parseSortOrder,
|
||||
requireTeamId,
|
||||
};
|
||||
|
||||
@@ -5,26 +5,35 @@ import {
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByTeamIdParamValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
getMonitorsWithChecksQueryValidation,
|
||||
createMonitorBodyValidation,
|
||||
editMonitorBodyValidation,
|
||||
pauseMonitorParamValidation,
|
||||
getMonitorStatsByIdParamValidation,
|
||||
getMonitorStatsByIdQueryValidation,
|
||||
getCertificateParamValidation,
|
||||
getHardwareDetailsByIdParamValidation,
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
} from "@/validation/joi.js";
|
||||
import sslChecker from "ssl-checker";
|
||||
import { fetchMonitorCertificate } from "./controllerUtils.js";
|
||||
import {
|
||||
fetchMonitorCertificate,
|
||||
requireString,
|
||||
optionalString,
|
||||
optionalNumber,
|
||||
optionalBoolean,
|
||||
parseMonitorTypeFilter,
|
||||
parseSortOrder,
|
||||
requireTeamId,
|
||||
} from "./controllerUtils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorService } from "@/service/index.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
class MonitorController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private monitorService: any;
|
||||
private monitorService: IMonitorService;
|
||||
|
||||
constructor(monitorService: any) {
|
||||
constructor(monitorService: IMonitorService) {
|
||||
this.monitorService = monitorService;
|
||||
}
|
||||
|
||||
@@ -33,33 +42,17 @@ class MonitorController {
|
||||
}
|
||||
|
||||
async verifyTeamAccess(teamId: string, monitorId: string) {
|
||||
const monitor = await this.monitorService.getMonitorById(monitorId);
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||
if (monitor.teamId !== teamId) {
|
||||
throw new AppError({ message: "Access denied", status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
getAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const monitors = await this.monitorService.getAllMonitors();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Retrieved all monitors successfully",
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getCertificateParamValidation.validateAsync(req.params);
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const { monitorId } = req.params;
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = requireString(req.params?.monitorId, "Monitor ID");
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
|
||||
|
||||
@@ -77,15 +70,10 @@ class MonitorController {
|
||||
|
||||
getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const monitorId = req?.params?.monitorId;
|
||||
const dateRange = req?.query?.dateRange;
|
||||
const normalize = req?.query?.normalize;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const normalize = optionalBoolean(req?.query?.normalize, "normalize");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getUptimeDetailsById({
|
||||
teamId,
|
||||
@@ -103,50 +91,14 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorStatsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
|
||||
const monitorId = req?.params?.monitorId;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const monitorStats = await this.monitorService.getMonitorStatsById({
|
||||
teamId,
|
||||
monitorId,
|
||||
limit,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Monitor stats retrieved successfully",
|
||||
data: monitorStats,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
||||
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const monitorId = req?.params?.monitorId;
|
||||
const dateRange = req?.query?.dateRange;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.getHardwareDetailsById({
|
||||
teamId,
|
||||
@@ -163,18 +115,40 @@ class MonitorController {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
||||
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.getPageSpeedDetailsById({
|
||||
teamId,
|
||||
monitorId,
|
||||
dateRange,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Page speed details retrieved successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId });
|
||||
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -190,10 +164,10 @@ class MonitorController {
|
||||
try {
|
||||
await createMonitorBodyValidation.validateAsync(req.body);
|
||||
|
||||
const userId = req?.user?._id;
|
||||
const teamId = req?.user?.teamId;
|
||||
const userId = requireString(req?.user?._id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.createMonitor({ teamId, userId, body: req.body });
|
||||
const monitor = await this.monitorService.createMonitor(teamId, userId, req.body);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -219,19 +193,15 @@ class MonitorController {
|
||||
throw new AppError({ message: "File is empty", status: 400 });
|
||||
}
|
||||
|
||||
const userId = req?.user?._id;
|
||||
const teamId = req?.user?.teamId;
|
||||
|
||||
if (!userId || !teamId) {
|
||||
throw new AppError({ message: "Missing userId or teamId", status: 400 });
|
||||
}
|
||||
const userId = requireString(req?.user?._id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const fileData = req?.file?.buffer?.toString("utf-8");
|
||||
if (!fileData) {
|
||||
throw new AppError({ message: "Cannot get file from buffer", status: 400 });
|
||||
}
|
||||
|
||||
const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId });
|
||||
const monitors = await this.monitorService.createBulkMonitors(fileData, userId, teamId);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -246,11 +216,8 @@ class MonitorController {
|
||||
deleteMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
const monitorId = req.params.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
|
||||
|
||||
@@ -266,10 +233,7 @@ class MonitorController {
|
||||
|
||||
deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
|
||||
|
||||
@@ -286,12 +250,8 @@ class MonitorController {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await editMonitorBodyValidation.validateAsync(req.body);
|
||||
const monitorId = req?.params?.monitorId;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
|
||||
|
||||
@@ -309,11 +269,8 @@ class MonitorController {
|
||||
try {
|
||||
await pauseMonitorParamValidation.validateAsync(req.params);
|
||||
|
||||
const monitorId = req.params.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
|
||||
|
||||
@@ -329,7 +286,8 @@ class MonitorController {
|
||||
|
||||
addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { _id, teamId } = req.user;
|
||||
const _id = requireString(req?.user?._id, "User ID");
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId });
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -365,10 +323,11 @@ class MonitorController {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
|
||||
const teamId = req?.user?.teamId;
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
const type = parseMonitorTypeFilter(req.query?.type);
|
||||
const filter = optionalString(req.query?.filter, "filter");
|
||||
|
||||
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, limit, type, page, rowsPerPage, filter, field, order });
|
||||
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter });
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -385,12 +344,9 @@ class MonitorController {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const explain = req?.query?.explain;
|
||||
const type = req?.query?.type;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const explain = optionalBoolean(req?.query?.explain, "explain");
|
||||
const type = parseMonitorTypeFilter(req?.query?.type);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain });
|
||||
|
||||
@@ -406,14 +362,17 @@ class MonitorController {
|
||||
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
|
||||
await getMonitorsWithChecksQueryValidation.validateAsync(req.query);
|
||||
|
||||
const explain = req?.query?.explain;
|
||||
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
const explain = optionalBoolean(req?.query?.explain, "explain");
|
||||
const limit = optionalNumber(req?.query?.limit, "limit");
|
||||
const page = optionalNumber(req?.query?.page, "page");
|
||||
const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage");
|
||||
const filter = optionalString(req?.query?.filter, "filter");
|
||||
const field = optionalString(req?.query?.field, "field");
|
||||
const order = parseSortOrder(req?.query?.order);
|
||||
const type = parseMonitorTypeFilter(req?.query?.type);
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
|
||||
teamId,
|
||||
|
||||
@@ -11,12 +11,9 @@ class MongoDB {
|
||||
inviteModule,
|
||||
statusPageModule,
|
||||
userModule,
|
||||
hardwareCheckModule,
|
||||
maintenanceWindowModule,
|
||||
monitorModule,
|
||||
networkCheckModule,
|
||||
notificationModule,
|
||||
pageSpeedCheckModule,
|
||||
recoveryModule,
|
||||
settingsModule,
|
||||
incidentModule,
|
||||
@@ -26,15 +23,12 @@ class MongoDB {
|
||||
this.userModule = userModule;
|
||||
this.inviteModule = inviteModule;
|
||||
this.recoveryModule = recoveryModule;
|
||||
this.pageSpeedCheckModule = pageSpeedCheckModule;
|
||||
this.hardwareCheckModule = hardwareCheckModule;
|
||||
this.checkModule = checkModule;
|
||||
this.maintenanceWindowModule = maintenanceWindowModule;
|
||||
this.monitorModule = monitorModule;
|
||||
this.notificationModule = notificationModule;
|
||||
this.settingsModule = settingsModule;
|
||||
this.statusPageModule = statusPageModule;
|
||||
this.networkCheckModule = networkCheckModule;
|
||||
this.incidentModule = incidentModule;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const MonitorStatsSchema = new mongoose.Schema(
|
||||
{
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
avgResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalUpChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalDownChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
uptimePercentage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastCheckTimestamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
timeOfLastFailure: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const MonitorStats = mongoose.model("MonitorStats", MonitorStatsSchema);
|
||||
|
||||
export default MonitorStats;
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
import { Schema, model, type Types } from "mongoose";
|
||||
import type { MonitorStats as MonitorStatsEntity } from "@/types/monitorStats.js";
|
||||
|
||||
type MonitorStatsDocumentBase = Omit<MonitorStatsEntity, "id" | "monitorId" | "createdAt" | "updatedAt"> & {
|
||||
monitorId: Types.ObjectId;
|
||||
};
|
||||
|
||||
interface MonitorStatsDocument extends MonitorStatsDocumentBase {
|
||||
_id: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MonitorStatsSchema = new Schema<MonitorStatsDocument>(
|
||||
{
|
||||
monitorId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
avgResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalUpChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalDownChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
uptimePercentage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastCheckTimestamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
timeOfLastFailure: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const MonitorStatsModel = model<MonitorStatsDocument>("MonitorStats", MonitorStatsSchema);
|
||||
|
||||
export type { MonitorStatsDocument };
|
||||
export { MonitorStatsModel };
|
||||
export default MonitorStatsModel;
|
||||
@@ -3,3 +3,6 @@ export { default as MonitorModel } from "@/db/models/Monitor.js";
|
||||
|
||||
export * from "@/db/models/Check.js";
|
||||
export { default as CheckModel } from "@/db/models/Check.js";
|
||||
|
||||
export * from "@/db/models/MonitorStats.js";
|
||||
export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js";
|
||||
|
||||
@@ -194,17 +194,6 @@ class MonitorModule {
|
||||
};
|
||||
};
|
||||
|
||||
getAllMonitors = async () => {
|
||||
try {
|
||||
const monitors = await this.Monitor.find();
|
||||
return monitors;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getAllMonitors";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorById = async (monitorId) => {
|
||||
try {
|
||||
const monitor = await this.Monitor.findById(monitorId);
|
||||
@@ -264,48 +253,6 @@ class MonitorModule {
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => {
|
||||
try {
|
||||
// Get monitor, if we can't find it, abort with error
|
||||
const monitor = await this.Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(this.stringService.getDbFindMonitorById(monitorId));
|
||||
}
|
||||
|
||||
// Get query params
|
||||
const sort = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
// Get Checks for monitor in date range requested
|
||||
const dates = this.getDateRange(dateRange);
|
||||
const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort);
|
||||
|
||||
// Build monitor stats
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
uptimeDuration: this.calculateUptimeDuration(checksAll),
|
||||
lastChecked: this.getLastChecked(checksAll),
|
||||
latestResponseTime: this.getLatestResponseTime(checksAll),
|
||||
periodIncidents: this.getIncidents(checksForDateRange),
|
||||
periodTotalChecks: checksForDateRange.length,
|
||||
checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize),
|
||||
};
|
||||
|
||||
if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") {
|
||||
// HTTP/PING Specific stats
|
||||
monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange);
|
||||
monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange);
|
||||
const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange);
|
||||
monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats);
|
||||
}
|
||||
|
||||
return monitorStats;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMonitorStatsById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async ({ monitorId, dateRange }) => {
|
||||
try {
|
||||
const monitor = await this.Monitor.findById(monitorId);
|
||||
@@ -342,47 +289,33 @@ class MonitorModule {
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => {
|
||||
limit = parseInt(limit);
|
||||
page = parseInt(page);
|
||||
rowsPerPage = parseInt(rowsPerPage);
|
||||
if (field === undefined) {
|
||||
field = "name";
|
||||
order = "asc";
|
||||
}
|
||||
// Build match stage
|
||||
const matchStage = { teamId: new this.ObjectId(teamId) };
|
||||
if (type !== undefined) {
|
||||
matchStage.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
|
||||
const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage }));
|
||||
const summary = summaryResult[0];
|
||||
|
||||
const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order }));
|
||||
|
||||
const filteredMonitors = await this.Monitor.aggregate(
|
||||
buildFilteredMonitorsByTeamIdPipeline({
|
||||
matchStage,
|
||||
filter,
|
||||
page,
|
||||
rowsPerPage,
|
||||
field,
|
||||
order,
|
||||
limit,
|
||||
type,
|
||||
})
|
||||
);
|
||||
|
||||
const normalizedFilteredMonitors = filteredMonitors.map((monitor) => {
|
||||
if (!monitor.checks) {
|
||||
return monitor;
|
||||
getMonitorsByTeamId = async ({ teamId, type, filter }) => {
|
||||
try {
|
||||
const matchStage = { teamId: new this.ObjectId(teamId) };
|
||||
if (type !== undefined) {
|
||||
matchStage.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
monitor.checks = this.NormalizeData(monitor.checks, 10, 100);
|
||||
return monitor;
|
||||
});
|
||||
|
||||
return { summary, monitors, filteredMonitors: normalizedFilteredMonitors };
|
||||
if (filter !== undefined && filter !== null && filter !== "") {
|
||||
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
|
||||
}
|
||||
const monitors = await this.Monitor.find(matchStage)
|
||||
.sort({ name: 1 })
|
||||
.select({
|
||||
_id: 1,
|
||||
name: 1,
|
||||
type: 1,
|
||||
url: 1,
|
||||
status: 1,
|
||||
isActive: 1,
|
||||
teamId: 1,
|
||||
})
|
||||
.lean();
|
||||
return monitors;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMonitorsByTeamId";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => {
|
||||
|
||||
@@ -1,10 +1,65 @@
|
||||
import type { Check, CheckAudits, MonitorType } from "@/types/index.js";
|
||||
import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
|
||||
|
||||
export interface IChecksRepository {
|
||||
// create
|
||||
// single fetch
|
||||
// collection fetch
|
||||
findLatestChecksByMonitorIds(monitorIds: string[]): Promise<LatestChecksMap>;
|
||||
// update
|
||||
// delete
|
||||
export interface PageSpeedChecksResult {
|
||||
monitorType: "pagespeed";
|
||||
checks: Check[];
|
||||
}
|
||||
|
||||
export interface HardwareChecksResult {
|
||||
monitorType: "hardware";
|
||||
aggregateData: {
|
||||
latestCheck: Check | null;
|
||||
totalChecks: number;
|
||||
};
|
||||
upChecks: {
|
||||
totalChecks: number;
|
||||
};
|
||||
checks: Array<{
|
||||
_id: string;
|
||||
avgCpuUsage: number;
|
||||
avgMemoryUsage: number;
|
||||
avgTemperature: number[];
|
||||
disks: Array<{
|
||||
name: string;
|
||||
readSpeed: number;
|
||||
writeSpeed: number;
|
||||
totalBytes: number;
|
||||
freeBytes: number;
|
||||
usagePercent: number;
|
||||
}>;
|
||||
net: Array<{
|
||||
name: string;
|
||||
bytesSentPerSecond: number;
|
||||
deltaBytesRecv: number;
|
||||
deltaPacketsSent: number;
|
||||
deltaPacketsRecv: number;
|
||||
deltaErrIn: number;
|
||||
deltaErrOut: number;
|
||||
deltaDropIn: number;
|
||||
deltaDropOut: number;
|
||||
deltaFifoIn: number;
|
||||
deltaFifoOut: number;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface UptimeChecksResult {
|
||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
|
||||
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
|
||||
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
}
|
||||
|
||||
export interface IChecksRepository {
|
||||
findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
|
||||
findDateRangeChecksByMonitor(
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string,
|
||||
options?: { type?: MonitorType }
|
||||
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,15 @@ import type {
|
||||
CheckMetadata,
|
||||
CheckNetworkInterfaceInfo,
|
||||
CheckTimings,
|
||||
MonitorType,
|
||||
} from "@/types/index.js";
|
||||
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import {
|
||||
getAggregateData as getHardwareAggregateData,
|
||||
getHardwareStats,
|
||||
getUpChecks as getHardwareUpChecks,
|
||||
} from "@/db/modules/monitorModuleQueries.js";
|
||||
|
||||
export type LatestChecksMap = Record<string, Check[]>;
|
||||
|
||||
@@ -172,15 +178,16 @@ class MongoChecksRepistory implements IChecksRepository {
|
||||
};
|
||||
};
|
||||
|
||||
findLatestChecksByMonitorIds = async (monitorIds: string[]): Promise<LatestChecksMap> => {
|
||||
findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap> => {
|
||||
if (monitorIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const limitPerMonitor = 25;
|
||||
const maxIntervalMs = Number(process.env.MONITOR_MAX_INTERVAL_MS ?? 10 * 60 * 1000);
|
||||
const bufferMs = Number(process.env.MONITOR_INTERVAL_BUFFER_MS ?? maxIntervalMs);
|
||||
const lookbackMs = 25 * maxIntervalMs + bufferMs;
|
||||
const limitPerMonitor = options?.limitPerMonitor ?? 25;
|
||||
const maxIntervalMs = Number(10 * 60 * 1000);
|
||||
const bufferMs = Number(maxIntervalMs);
|
||||
const lookbackMs = limitPerMonitor * maxIntervalMs + bufferMs;
|
||||
|
||||
const cutoffDate = new Date(Date.now() - lookbackMs);
|
||||
const checkGroups = await CheckModel.aggregate([
|
||||
{
|
||||
@@ -202,12 +209,187 @@ class MongoChecksRepistory implements IChecksRepository {
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return checkGroups.reduce<LatestChecksMap>((acc, group) => {
|
||||
const monitorId = group._id.toString();
|
||||
acc[monitorId] = (group.latestChecks ?? []).map((doc: CheckDocument) => this.toEntity(doc));
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => {
|
||||
const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
|
||||
if (options?.type === "hardware") {
|
||||
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
||||
}
|
||||
if (options?.type === "pagespeed") {
|
||||
return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate);
|
||||
}
|
||||
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
|
||||
};
|
||||
|
||||
private findUptimeDateRangeChecks = async (
|
||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
|
||||
monitorObjectId: mongoose.Types.ObjectId,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
) => {
|
||||
const matchStage = {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
updatedAt: { $gte: startDate, $lte: endDate },
|
||||
};
|
||||
const [result] = await CheckModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { updatedAt: 1 } },
|
||||
{
|
||||
$facet: {
|
||||
uptimePercentage: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
percentage: {
|
||||
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
groupedAvgResponseTime: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
],
|
||||
groupedChecks: [
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
},
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
],
|
||||
groupedUpChecks: [
|
||||
{ $match: { status: true } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
},
|
||||
totalChecks: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
],
|
||||
groupedDownChecks: [
|
||||
{ $match: { status: false } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
},
|
||||
totalChecks: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0;
|
||||
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
|
||||
|
||||
return {
|
||||
monitorType,
|
||||
groupedChecks: result?.groupedChecks ?? [],
|
||||
groupedUpChecks: result?.groupedUpChecks ?? [],
|
||||
groupedDownChecks: result?.groupedDownChecks ?? [],
|
||||
uptimePercentage,
|
||||
avgResponseTime,
|
||||
};
|
||||
};
|
||||
|
||||
private findHardwareDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => {
|
||||
const monitorId = monitorObjectId.toHexString();
|
||||
const dates = { start: startDate, end: endDate };
|
||||
const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([
|
||||
getHardwareAggregateData(monitorId, dates),
|
||||
getHardwareUpChecks(monitorId, dates),
|
||||
getHardwareStats(monitorId, dates, dateString),
|
||||
]);
|
||||
|
||||
const aggregateData = {
|
||||
latestCheck: aggregateDataDoc?.latestCheck ? this.toEntity(aggregateDataDoc.latestCheck as CheckDocument) : null,
|
||||
totalChecks: aggregateDataDoc?.totalChecks ?? 0,
|
||||
};
|
||||
|
||||
const upChecks = {
|
||||
totalChecks: upChecksDoc?.totalChecks ?? 0,
|
||||
};
|
||||
|
||||
const checks = (hardwareMetrics ?? []).map((metric) => ({
|
||||
_id: metric._id,
|
||||
avgCpuUsage: metric.avgCpuUsage ?? 0,
|
||||
avgMemoryUsage: metric.avgMemoryUsage ?? 0,
|
||||
avgTemperature: metric.avgTemperature ?? [],
|
||||
disks: (metric.disks ?? []).map((disk: { [key: string]: number | string | undefined }) => ({
|
||||
name: disk?.name ?? "",
|
||||
readSpeed: disk?.readSpeed ?? 0,
|
||||
writeSpeed: disk?.writeSpeed ?? 0,
|
||||
totalBytes: disk?.totalBytes ?? 0,
|
||||
freeBytes: disk?.freeBytes ?? 0,
|
||||
usagePercent: disk?.usagePercent ?? 0,
|
||||
})),
|
||||
net: (metric.net ?? []).map((iface: { [key: string]: number | string | undefined }) => ({
|
||||
name: iface?.name ?? "",
|
||||
bytesSentPerSecond: iface?.bytesSentPerSecond ?? 0,
|
||||
deltaBytesRecv: iface?.deltaBytesRecv ?? 0,
|
||||
deltaPacketsSent: iface?.deltaPacketsSent ?? 0,
|
||||
deltaPacketsRecv: iface?.deltaPacketsRecv ?? 0,
|
||||
deltaErrIn: iface?.deltaErrIn ?? 0,
|
||||
deltaErrOut: iface?.deltaErrOut ?? 0,
|
||||
deltaDropIn: iface?.deltaDropIn ?? 0,
|
||||
deltaDropOut: iface?.deltaDropOut ?? 0,
|
||||
deltaFifoIn: iface?.deltaFifoIn ?? 0,
|
||||
deltaFifoOut: iface?.deltaFifoOut ?? 0,
|
||||
})),
|
||||
}));
|
||||
|
||||
return {
|
||||
monitorType: "hardware" as const,
|
||||
aggregateData,
|
||||
upChecks,
|
||||
checks,
|
||||
};
|
||||
};
|
||||
|
||||
private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date) => {
|
||||
const matchStage = {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
createdAt: { $gte: startDate, $lte: endDate },
|
||||
};
|
||||
|
||||
const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean();
|
||||
return {
|
||||
monitorType: "pagespeed" as const,
|
||||
checks: checks.map((doc) => this.toEntity(doc)),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoChecksRepistory;
|
||||
|
||||
@@ -3,3 +3,6 @@ export { default as MongoMonitorsRepository } from "@/repositories/monitors/Mong
|
||||
|
||||
export * from "@/repositories/checks/IChecksRepository.js";
|
||||
export { default as MongoChecksRepository } from "@/repositories/checks/MongoChecksRepistory.js";
|
||||
|
||||
export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js";
|
||||
export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js";
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { MonitorStats } from "@/types/index.js";
|
||||
export interface IMonitorStatsRepository {
|
||||
// create
|
||||
// single fetch
|
||||
findByMonitorId(monitorId: string): Promise<MonitorStats>;
|
||||
// update
|
||||
// delete
|
||||
// other
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { type MonitorStatsDocument, MonitorStatsModel } from "@/db/models/index.js";
|
||||
import type { MonitorStats } from "@/types/index.js";
|
||||
import { IMonitorStatsRepository } from "@/repositories/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
class MongoMonitorStatsRepository implements IMonitorStatsRepository {
|
||||
private toEntity = (doc: MonitorStatsDocument): MonitorStats => {
|
||||
const toStringId = (value: unknown): string => {
|
||||
if (value instanceof mongoose.Types.ObjectId) {
|
||||
return value.toString();
|
||||
}
|
||||
return value?.toString() ?? "";
|
||||
};
|
||||
|
||||
const toDateString = (value: Date | string): string => {
|
||||
return value instanceof Date ? value.toISOString() : value;
|
||||
};
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
monitorId: toStringId(doc.monitorId),
|
||||
avgResponseTime: doc.avgResponseTime,
|
||||
totalChecks: doc.totalChecks,
|
||||
totalUpChecks: doc.totalUpChecks,
|
||||
totalDownChecks: doc.totalDownChecks,
|
||||
uptimePercentage: doc.uptimePercentage,
|
||||
lastCheckTimestamp: doc.lastCheckTimestamp,
|
||||
lastResponseTime: doc.lastResponseTime,
|
||||
timeOfLastFailure: doc.timeOfLastFailure,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
findByMonitorId = async (monitorId: string): Promise<MonitorStats> => {
|
||||
const monitorStats = await MonitorStatsModel.findOne({ monitorId: new mongoose.Types.ObjectId(monitorId) });
|
||||
if (!monitorStats) {
|
||||
throw new AppError({ message: "Monitor stats not found", status: 404 });
|
||||
}
|
||||
return this.toEntity(monitorStats);
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoMonitorStatsRepository;
|
||||
@@ -12,12 +12,18 @@ export interface TeamQueryConfig {
|
||||
|
||||
export interface IMonitorsRepository {
|
||||
// create
|
||||
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
|
||||
createBulkMonitors(monitors: Monitor[]): Promise<Monitor[]>;
|
||||
// single fetch
|
||||
findById(monitorId: string, teamId?: string): Promise<Monitor | null>;
|
||||
|
||||
// collection fetch
|
||||
findAll(): Promise<Monitor[] | null>;
|
||||
findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null>;
|
||||
// update
|
||||
update(monitorId: string, updates: Partial<Monitor>): Promise<Monitor>;
|
||||
// delete
|
||||
deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>;
|
||||
|
||||
// counts
|
||||
findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise<number>;
|
||||
|
||||
@@ -3,11 +3,44 @@ import type { MonitorDocument } from "@/db/models/Monitor.js";
|
||||
import type { Monitor, MonitorType } from "@/types/monitor.js";
|
||||
import mongoose, { type FilterQuery } from "mongoose";
|
||||
import type { IMonitorsRepository, TeamQueryConfig } from "./IMonitorsRepository.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
findAll = async (): Promise<Monitor[] | null> => {
|
||||
const documents = await MonitorModel.find().exec();
|
||||
return this.mapDocuments(documents);
|
||||
create = async (monitor: Monitor, teamId: string, userId: string) => {
|
||||
const monitorModel = new MonitorModel({ ...monitor, teamId, userId });
|
||||
const saved = await monitorModel.save();
|
||||
return this.toEntity(saved);
|
||||
};
|
||||
|
||||
createBulkMonitors = async (monitors: Monitor[]): Promise<Monitor[]> => {
|
||||
if (!monitors.length) {
|
||||
return [];
|
||||
}
|
||||
const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined }));
|
||||
const inserted = await MonitorModel.insertMany(payload, { ordered: false });
|
||||
return this.mapDocuments(inserted);
|
||||
};
|
||||
|
||||
findById = async (monitorId: string, teamId?: string): Promise<Monitor> => {
|
||||
const match: { _id: string; teamId?: string } = { _id: monitorId };
|
||||
if (teamId) {
|
||||
match.teamId = teamId;
|
||||
}
|
||||
const monitor = await MonitorModel.findOne(match);
|
||||
if (!monitor) {
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new AppError({
|
||||
message: `Monitor with ID ${monitorId} not found.`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
return this.toEntity(monitor);
|
||||
};
|
||||
|
||||
findAll = async (): Promise<Monitor[]> => {
|
||||
const monitors = await MonitorModel.find();
|
||||
return this.mapDocuments(monitors);
|
||||
};
|
||||
|
||||
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
|
||||
@@ -64,9 +97,32 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
return count;
|
||||
};
|
||||
|
||||
private mapDocuments = (documents: MonitorDocument[]): Monitor[] | null => {
|
||||
update = async (monitorId: string, patch: Partial<Monitor>) => {
|
||||
const updatedMonitor = await MonitorModel.findOneAndUpdate(
|
||||
{ _id: monitorId },
|
||||
{
|
||||
$set: {
|
||||
...patch,
|
||||
},
|
||||
},
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
if (!updatedMonitor) {
|
||||
throw new AppError({ message: `Failed to update monitor with id ${monitorId}`, status: 500 });
|
||||
}
|
||||
return this.toEntity(updatedMonitor);
|
||||
};
|
||||
|
||||
deleteByTeamId = async (teamId: string) => {
|
||||
const monitors = await MonitorModel.find({ teamId });
|
||||
const { deletedCount } = await MonitorModel.deleteMany({ teamId });
|
||||
|
||||
return { monitors: this.mapDocuments(monitors), deletedCount };
|
||||
};
|
||||
|
||||
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
|
||||
if (!documents?.length) {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
return documents.map((doc) => this.toEntity(doc));
|
||||
};
|
||||
|
||||
@@ -27,10 +27,11 @@ class MonitorRoutes {
|
||||
|
||||
// Hardware routes
|
||||
this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById);
|
||||
// PageSpeed routes
|
||||
this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById);
|
||||
|
||||
// General monitor routes
|
||||
this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor);
|
||||
this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById);
|
||||
|
||||
// Util routes
|
||||
this.router.get("/certificate/:monitorId", (req, res, next) => {
|
||||
@@ -38,7 +39,6 @@ class MonitorRoutes {
|
||||
});
|
||||
|
||||
// General monitor CRUD routes
|
||||
this.router.get("/", this.monitorController.getAllMonitors);
|
||||
this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor);
|
||||
this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors);
|
||||
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
import { createMonitorsBodyValidation } from "@/validation/joi.js";
|
||||
import { NormalizeData } from "@/utils/dataUtils.js";
|
||||
|
||||
const SERVICE_NAME = "MonitorService";
|
||||
class MonitorService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
settingsService,
|
||||
jobQueue,
|
||||
stringService,
|
||||
emailService,
|
||||
papaparse,
|
||||
logger,
|
||||
errorService,
|
||||
games,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
}) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.jobQueue = jobQueue;
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
this.papaparse = papaparse;
|
||||
this.logger = logger;
|
||||
this.errorService = errorService;
|
||||
this.games = games;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.checksRepository = checksRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return MonitorService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
verifyTeamAccess = async ({ teamId, monitorId }) => {
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
if (!monitor?.teamId?.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
};
|
||||
|
||||
getAllMonitors = async () => {
|
||||
const monitors = await this.db.monitorModule.getAllMonitors();
|
||||
return monitors;
|
||||
};
|
||||
|
||||
getUptimeDetailsById = async ({ teamId, monitorId, dateRange, normalize }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const data = await this.db.monitorModule.getUptimeDetailsById({
|
||||
monitorId,
|
||||
dateRange,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
getMonitorStatsById = async ({ teamId, monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitorStats = await this.db.monitorModule.getMonitorStatsById({
|
||||
monitorId,
|
||||
limit,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return monitorStats;
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.getHardwareDetailsById({ monitorId, dateRange });
|
||||
|
||||
return monitor;
|
||||
};
|
||||
|
||||
getMonitorById = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
|
||||
return monitor;
|
||||
};
|
||||
|
||||
createMonitor = async ({ teamId, userId, body }) => {
|
||||
const monitor = await this.db.monitorModule.createMonitor({
|
||||
body,
|
||||
teamId,
|
||||
userId,
|
||||
});
|
||||
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
};
|
||||
|
||||
createBulkMonitors = async ({ fileData, userId, teamId }) => {
|
||||
const { parse } = this.papaparse;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
parse(fileData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
transform: (value, header) => {
|
||||
if (value === "") return undefined; // Empty fields become undefined
|
||||
|
||||
// Handle 'port' and 'interval' fields, check if they're valid numbers
|
||||
if (["port", "interval"].includes(header)) {
|
||||
const num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
complete: async ({ data, errors }) => {
|
||||
try {
|
||||
if (errors.length > 0) {
|
||||
throw this.errorService.createServerError("Error parsing CSV");
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw this.errorService.createServerError("CSV file contains no data rows");
|
||||
}
|
||||
|
||||
const enrichedData = data.map((monitor) => ({
|
||||
userId,
|
||||
teamId,
|
||||
...monitor,
|
||||
description: monitor.description || monitor.name || monitor.url,
|
||||
name: monitor.name || monitor.url,
|
||||
type: monitor.type || "http",
|
||||
}));
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
const monitors = await this.db.monitorModule.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
})
|
||||
);
|
||||
|
||||
resolve(monitors);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
deleteMonitor = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId });
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
deleteAllMonitors = async ({ teamId }) => {
|
||||
const { monitors, deletedCount } = await this.db.monitorModule.deleteAllMonitors(teamId);
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
try {
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.checkModule.deleteChecks(monitor._id);
|
||||
await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor._id);
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteAllMonitors",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
return deletedCount;
|
||||
};
|
||||
|
||||
editMonitor = async ({ teamId, monitorId, body }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body });
|
||||
await this.jobQueue.updateJob(editedMonitor);
|
||||
};
|
||||
|
||||
pauseMonitor = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.pauseMonitor({ monitorId });
|
||||
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
addDemoMonitors = async ({ userId, teamId }) => {
|
||||
const demoMonitors = await this.db.monitorModule.addDemoMonitors(userId, teamId);
|
||||
|
||||
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor)));
|
||||
return demoMonitors;
|
||||
};
|
||||
|
||||
sendTestEmail = async ({ to }) => {
|
||||
const subject = this.stringService.testEmailSubject;
|
||||
const context = { testName: "Monitoring System" };
|
||||
|
||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
||||
const messageId = await this.emailService.sendEmail(to, subject, html);
|
||||
|
||||
if (!messageId) {
|
||||
throw this.errorService.createServerError("Failed to send test email.");
|
||||
}
|
||||
|
||||
return messageId;
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
teamId,
|
||||
});
|
||||
return monitors;
|
||||
};
|
||||
|
||||
getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => {
|
||||
const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({
|
||||
type,
|
||||
explain,
|
||||
teamId,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => {
|
||||
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
});
|
||||
|
||||
const monitorIds = monitors?.map((m) => m.id) ?? [];
|
||||
const checksMap = await this.checksRepository.findLatestChecksByMonitorIds(monitorIds);
|
||||
const monitorsWithChecks = (monitors ?? []).map((monitor) => {
|
||||
const checks = NormalizeData(checksMap[monitor.id] ?? [], 10, 100);
|
||||
return {
|
||||
...monitor,
|
||||
checks,
|
||||
};
|
||||
});
|
||||
|
||||
return { count, monitors: monitorsWithChecks };
|
||||
};
|
||||
|
||||
exportMonitorsToCSV = async ({ teamId }) => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
}
|
||||
|
||||
const csvData = monitors?.filteredMonitors?.map((monitor) => ({
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
type: monitor.type,
|
||||
url: monitor.url,
|
||||
interval: monitor.interval,
|
||||
port: monitor.port,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
isActive: monitor.isActive,
|
||||
}));
|
||||
|
||||
const csv = this.papaparse.unparse(csvData);
|
||||
return csv;
|
||||
};
|
||||
exportMonitorsToJSON = async ({ teamId }) => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
}
|
||||
|
||||
const json = monitors?.filteredMonitors
|
||||
?.map((monitor) => {
|
||||
const initialType = monitor.type;
|
||||
let parsedType;
|
||||
|
||||
if (initialType === "hardware") {
|
||||
parsedType = "infrastructure";
|
||||
} else if (initialType === "http") {
|
||||
if (monitor.url.startsWith("https://")) {
|
||||
parsedType = "https";
|
||||
} else {
|
||||
parsedType = "http";
|
||||
}
|
||||
} else if (initialType === "pagespeed") {
|
||||
parsedType = initialType;
|
||||
} else {
|
||||
// Skip unsupported types
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
type: parsedType,
|
||||
interval: monitor.interval,
|
||||
n: monitor.statusWindowSize,
|
||||
secret: monitor.secret,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
getAllGames = () => {
|
||||
return this.games;
|
||||
};
|
||||
|
||||
getGroupsByTeamId = async ({ teamId }) => {
|
||||
const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId });
|
||||
return groups;
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorService;
|
||||
@@ -0,0 +1,566 @@
|
||||
import { createMonitorsBodyValidation } from "@/validation/joi.js";
|
||||
import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js";
|
||||
import { type Monitor } from "@/types/index.js";
|
||||
import type { MonitorType } from "@/types/monitor.js";
|
||||
import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository } from "@/repositories/index.js";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
|
||||
import { AppError } from "../infrastructure/errorService.js";
|
||||
|
||||
const SERVICE_NAME = "MonitorService";
|
||||
type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
|
||||
|
||||
export interface IMonitorService {
|
||||
readonly serviceName: string;
|
||||
verifyTeamAccess(args: { teamId: string; monitorId: string }): Promise<void>;
|
||||
|
||||
// create
|
||||
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
|
||||
createBulkMonitors(fileData: string, userId: string, teamId: string): Promise<any>;
|
||||
addDemoMonitors(args: { userId: string; teamId: string }): Promise<any[]>;
|
||||
|
||||
// read
|
||||
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<any>;
|
||||
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
|
||||
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
|
||||
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||
getMonitorsByTeamId(args: {
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
type?: MonitorType | MonitorType[];
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
filter?: string;
|
||||
field?: string;
|
||||
order?: "asc" | "desc";
|
||||
}): Promise<any>;
|
||||
getMonitorsAndSummaryByTeamId(args: { teamId: string; type?: string | string[]; explain?: boolean }): Promise<any>;
|
||||
getMonitorsWithChecksByTeamId(args: {
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
type?: MonitorType | MonitorType[];
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
filter?: string;
|
||||
field?: string;
|
||||
order?: "asc" | "desc";
|
||||
explain?: boolean;
|
||||
}): Promise<{ count: number; monitors: any[] }>;
|
||||
getAllGames(): any;
|
||||
getGroupsByTeamId(args: { teamId: string }): Promise<any[]>;
|
||||
|
||||
// update
|
||||
editMonitor(args: { teamId: string; monitorId: string; body: any }): Promise<void>;
|
||||
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<any>;
|
||||
|
||||
// delete
|
||||
deleteMonitor(args: { teamId: string; monitorId: string }): Promise<any>;
|
||||
deleteAllMonitors(args: { teamId: string }): Promise<number>;
|
||||
|
||||
// other
|
||||
sendTestEmail(args: { to: string }): Promise<string>;
|
||||
exportMonitorsToCSV(args: { teamId: string }): Promise<string>;
|
||||
exportMonitorsToJSON(args: { teamId: string }): Promise<any[]>;
|
||||
}
|
||||
|
||||
export class MonitorService implements IMonitorService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private db: any;
|
||||
private jobQueue: any;
|
||||
private stringService: any;
|
||||
private emailService: any;
|
||||
private papaparse: any;
|
||||
private logger: any;
|
||||
private errorService: any;
|
||||
private games: any;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private checksRepository: IChecksRepository;
|
||||
private monitorStatsRepository: IMonitorStatsRepository;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
jobQueue,
|
||||
stringService,
|
||||
emailService,
|
||||
papaparse,
|
||||
logger,
|
||||
errorService,
|
||||
games,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
}: {
|
||||
db: any;
|
||||
jobQueue: any;
|
||||
stringService: any;
|
||||
emailService: any;
|
||||
papaparse: any;
|
||||
logger: any;
|
||||
errorService: any;
|
||||
games: any;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
checksRepository: IChecksRepository;
|
||||
monitorStatsRepository: IMonitorStatsRepository;
|
||||
}) {
|
||||
this.db = db;
|
||||
this.jobQueue = jobQueue;
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
this.papaparse = papaparse;
|
||||
this.logger = logger;
|
||||
this.errorService = errorService;
|
||||
this.games = games;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.checksRepository = checksRepository;
|
||||
this.monitorStatsRepository = monitorStatsRepository;
|
||||
}
|
||||
|
||||
get serviceName(): string {
|
||||
return MonitorService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
private getDateRange = (dateRange: DateRangeKey) => {
|
||||
const startDates = {
|
||||
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
all: new Date(0),
|
||||
};
|
||||
return {
|
||||
start: startDates[dateRange],
|
||||
end: new Date(),
|
||||
};
|
||||
};
|
||||
|
||||
private getDateFormat = (dateRange: DateRangeKey): string => {
|
||||
const formatLookup = {
|
||||
recent: "%Y-%m-%dT%H:%M:00Z",
|
||||
day: "%Y-%m-%dT%H:00:00Z",
|
||||
week: "%Y-%m-%dT00:00:00Z",
|
||||
month: "%Y-%m-%dT00:00:00Z",
|
||||
all: "%Y-%m-%dT00:00:00Z",
|
||||
};
|
||||
return formatLookup[dateRange];
|
||||
};
|
||||
|
||||
verifyTeamAccess = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<void> => {
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
if (!monitor?.teamId?.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
};
|
||||
|
||||
createMonitor = async (teamId: string, userId: string, body: Monitor): Promise<void> => {
|
||||
const monitor = await this.monitorsRepository.create(body, teamId, userId);
|
||||
if (!monitor) {
|
||||
throw new AppError("Failed to create monitor", 500);
|
||||
}
|
||||
|
||||
this.jobQueue.addJob(monitor.id, monitor);
|
||||
};
|
||||
|
||||
createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise<any> => {
|
||||
const { parse } = this.papaparse;
|
||||
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
parse(fileData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
transform: (value: string, header: string): string | number | undefined => {
|
||||
if (value === "") return undefined; // Empty fields become undefined
|
||||
|
||||
// Handle 'port' and 'interval' fields, check if they're valid numbers
|
||||
if (["port", "interval"].includes(header)) {
|
||||
const num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
complete: async ({ data, errors }: { data: any[]; errors: Error[] }): Promise<void> => {
|
||||
try {
|
||||
if (errors.length > 0) {
|
||||
throw this.errorService.createServerError("Error parsing CSV");
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw this.errorService.createServerError("CSV file contains no data rows");
|
||||
}
|
||||
|
||||
const enrichedData = data.map((monitor: Monitor) => ({
|
||||
...monitor,
|
||||
userId,
|
||||
teamId,
|
||||
description: monitor.description || monitor.name || monitor.url,
|
||||
name: monitor.name || monitor.url,
|
||||
type: monitor.type || "http",
|
||||
}));
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
const monitors = await this.monitorsRepository.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor: Monitor) => {
|
||||
this.jobQueue.addJob(monitor.id, monitor);
|
||||
})
|
||||
);
|
||||
|
||||
resolve(monitors);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const demoMonitorsPath = path.resolve(__dirname, "../../utils/demoMonitors.json");
|
||||
|
||||
const demoData = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8"));
|
||||
const monitors: Monitor[] = demoData.map((monitor: Monitor) => {
|
||||
return {
|
||||
userId,
|
||||
teamId,
|
||||
name: monitor.name,
|
||||
description: monitor.name,
|
||||
type: "http",
|
||||
url: monitor.url,
|
||||
interval: 60000,
|
||||
};
|
||||
});
|
||||
const demoMonitors = await this.monitorsRepository.createBulkMonitors(monitors);
|
||||
|
||||
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
||||
return demoMonitors;
|
||||
};
|
||||
|
||||
getUptimeDetailsById = async ({
|
||||
teamId,
|
||||
monitorId,
|
||||
dateRange,
|
||||
normalize,
|
||||
}: {
|
||||
teamId: string;
|
||||
monitorId: string;
|
||||
dateRange: string;
|
||||
normalize?: boolean;
|
||||
}): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
if (!monitor) {
|
||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||
}
|
||||
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
||||
const { start, end } = this.getDateRange(rangeKey);
|
||||
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
||||
type: monitor.type,
|
||||
});
|
||||
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
|
||||
|
||||
if (
|
||||
checksData.monitorType !== "http" &&
|
||||
checksData.monitorType !== "ping" &&
|
||||
checksData.monitorType !== "docker" &&
|
||||
checksData.monitorType !== "port" &&
|
||||
checksData.monitorType !== "game"
|
||||
) {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
||||
}
|
||||
|
||||
return {
|
||||
monitorData: {
|
||||
monitor,
|
||||
groupedChecks: NormalizeDataUptimeDetails(checksData.groupedChecks, 10, 100),
|
||||
groupedUpChecks: NormalizeDataUptimeDetails(checksData.groupedUpChecks, 10, 100),
|
||||
groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100),
|
||||
groupedAvgResponseTime: checksData.avgResponseTime,
|
||||
groupedUptimePercentage: checksData.uptimePercentage,
|
||||
},
|
||||
monitorStats,
|
||||
};
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
if (!monitor) {
|
||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||
}
|
||||
if (monitor.type !== "hardware") {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 });
|
||||
}
|
||||
|
||||
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
||||
const { start, end } = this.getDateRange(rangeKey);
|
||||
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
||||
type: monitor.type,
|
||||
});
|
||||
|
||||
if (checksData.monitorType !== "hardware") {
|
||||
throw new AppError({ message: "Unable to load hardware stats for this monitor", status: 500 });
|
||||
}
|
||||
|
||||
const stats = {
|
||||
aggregateData: checksData.aggregateData,
|
||||
upChecks: checksData.upChecks,
|
||||
checks: checksData.checks,
|
||||
};
|
||||
|
||||
return {
|
||||
...monitor,
|
||||
stats,
|
||||
};
|
||||
};
|
||||
|
||||
getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
if (!monitor) {
|
||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||
}
|
||||
if (monitor.type !== "pagespeed") {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 });
|
||||
}
|
||||
|
||||
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
||||
const { start, end } = this.getDateRange(rangeKey);
|
||||
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
||||
type: monitor.type,
|
||||
});
|
||||
|
||||
if (checksData.monitorType !== "pagespeed") {
|
||||
throw new AppError({ message: "Unable to load pagespeed stats for this monitor", status: 500 });
|
||||
}
|
||||
return {
|
||||
...monitor,
|
||||
checks: checksData.checks,
|
||||
};
|
||||
};
|
||||
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise<any> => {
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
|
||||
type,
|
||||
filter,
|
||||
});
|
||||
return monitors;
|
||||
};
|
||||
|
||||
getMonitorsAndSummaryByTeamId = async ({
|
||||
teamId,
|
||||
type,
|
||||
explain,
|
||||
}: {
|
||||
teamId: string;
|
||||
type?: string | string[];
|
||||
explain?: boolean;
|
||||
}): Promise<any> => {
|
||||
const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({
|
||||
type,
|
||||
explain,
|
||||
teamId,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
getMonitorsWithChecksByTeamId = async ({
|
||||
teamId,
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
explain,
|
||||
}: {
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
type?: MonitorType | MonitorType[];
|
||||
page?: number;
|
||||
rowsPerPage?: number;
|
||||
filter?: string;
|
||||
field?: string;
|
||||
order?: "asc" | "desc";
|
||||
explain?: boolean;
|
||||
}): Promise<{ count: number; monitors: any[] }> => {
|
||||
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
});
|
||||
|
||||
const monitorsList = (monitors ?? []) as Monitor[];
|
||||
const snapshotTypes: MonitorType[] = ["hardware", "pagespeed"];
|
||||
const requestedTypes = Array.isArray(type) ? type : type ? [type] : [];
|
||||
const snapshotOnlyRequest =
|
||||
requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType));
|
||||
|
||||
const limitPerMonitor = snapshotOnlyRequest ? 1 : 25;
|
||||
const checksMap = await this.checksRepository.findLatestChecksByMonitorIds(
|
||||
monitorsList.map((monitor) => monitor.id),
|
||||
{ limitPerMonitor }
|
||||
);
|
||||
|
||||
const monitorsWithChecks = monitorsList.map((monitor: Monitor) => {
|
||||
const rawChecks = checksMap[monitor.id] ?? [];
|
||||
const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type);
|
||||
const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100);
|
||||
return {
|
||||
...monitor,
|
||||
checks,
|
||||
};
|
||||
});
|
||||
|
||||
return { count, monitors: monitorsWithChecks };
|
||||
};
|
||||
|
||||
getAllGames = (): any => {
|
||||
return this.games;
|
||||
};
|
||||
|
||||
getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
||||
const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId });
|
||||
return groups;
|
||||
};
|
||||
|
||||
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: any }): Promise<void> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body });
|
||||
await this.jobQueue.updateJob(editedMonitor);
|
||||
};
|
||||
|
||||
pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.pauseMonitor({ monitorId });
|
||||
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId });
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
deleteAllMonitors = async ({ teamId }: { teamId: string }): Promise<number> => {
|
||||
const { monitors, deletedCount } = await this.monitorsRepository.deleteByTeamId(teamId);
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
try {
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.checkModule.deleteChecks(monitor.id);
|
||||
await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor.id);
|
||||
await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor.id);
|
||||
} catch (error: any) {
|
||||
this.logger.warn({
|
||||
message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteAllMonitors",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
return deletedCount;
|
||||
};
|
||||
|
||||
sendTestEmail = async ({ to }: { to: string }): Promise<string> => {
|
||||
const subject = this.stringService.testEmailSubject;
|
||||
const context = { testName: "Monitoring System" };
|
||||
|
||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
||||
const messageId = await this.emailService.sendEmail(to, subject, html);
|
||||
|
||||
if (!messageId) {
|
||||
throw this.errorService.createServerError("Failed to send test email.");
|
||||
}
|
||||
|
||||
return messageId;
|
||||
};
|
||||
|
||||
exportMonitorsToCSV = async ({ teamId }: { teamId: string }): Promise<string> => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
}
|
||||
|
||||
const csvData = monitors?.filteredMonitors?.map((monitor: any) => ({
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
type: monitor.type,
|
||||
url: monitor.url,
|
||||
interval: monitor.interval,
|
||||
port: monitor.port,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
isActive: monitor.isActive,
|
||||
}));
|
||||
|
||||
const csv = this.papaparse.unparse(csvData);
|
||||
return csv;
|
||||
};
|
||||
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
}
|
||||
|
||||
const json = monitors?.filteredMonitors
|
||||
?.map((monitor: any) => {
|
||||
const initialType = monitor.type;
|
||||
let parsedType;
|
||||
|
||||
if (initialType === "hardware") {
|
||||
parsedType = "infrastructure";
|
||||
} else if (initialType === "http") {
|
||||
if (monitor.url.startsWith("https://")) {
|
||||
parsedType = "https";
|
||||
} else {
|
||||
parsedType = "http";
|
||||
}
|
||||
} else if (initialType === "pagespeed") {
|
||||
parsedType = initialType;
|
||||
} else {
|
||||
// Skip unsupported types
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
type: parsedType,
|
||||
interval: monitor.interval,
|
||||
n: monitor.statusWindowSize,
|
||||
secret: monitor.secret,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return json;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "@/service/business/monitorService.js";
|
||||
@@ -4,19 +4,20 @@ const SERVICE_NAME = "JobQueue";
|
||||
class SuperSimpleQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ envSettings, db, logger, helper }) {
|
||||
constructor({ envSettings, db, logger, helper, monitorsRepository }) {
|
||||
this.envSettings = envSettings;
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.helper = helper;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return SuperSimpleQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create({ envSettings, db, logger, helper }) {
|
||||
const instance = new SuperSimpleQueue({ envSettings, db, logger, helper });
|
||||
static async create({ envSettings, db, logger, helper, monitorsRepository }) {
|
||||
const instance = new SuperSimpleQueue({ envSettings, db, logger, helper, monitorsRepository });
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
@@ -33,11 +34,11 @@ class SuperSimpleQueue {
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||
const monitors = await this.db.monitorModule.getAllMonitors();
|
||||
const monitors = await this.monitorsRepository.findAll();
|
||||
for (const monitor of monitors) {
|
||||
const randomOffset = Math.floor(Math.random() * 100);
|
||||
setTimeout(() => {
|
||||
this.addJob(monitor._id, monitor);
|
||||
this.addJob(monitor.id, monitor);
|
||||
}, randomOffset);
|
||||
}
|
||||
|
||||
@@ -55,44 +56,44 @@ class SuperSimpleQueue {
|
||||
|
||||
addJob = async (monitorId, monitor) => {
|
||||
this.scheduler.addJob({
|
||||
id: monitorId.toString(),
|
||||
id: monitorId,
|
||||
template: "monitor-job",
|
||||
repeat: monitor.interval,
|
||||
active: monitor.isActive,
|
||||
data: monitor.toObject(),
|
||||
data: monitor,
|
||||
});
|
||||
};
|
||||
|
||||
deleteJob = async (monitor) => {
|
||||
this.scheduler.removeJob(monitor._id.toString());
|
||||
this.scheduler.removeJob(monitor.id);
|
||||
};
|
||||
|
||||
pauseJob = async (monitor) => {
|
||||
const result = this.scheduler.pauseJob(monitor._id.toString());
|
||||
const result = this.scheduler.pauseJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Paused monitor ${monitor._id}`,
|
||||
message: `Paused monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "pauseJob",
|
||||
});
|
||||
};
|
||||
|
||||
resumeJob = async (monitor) => {
|
||||
const result = this.scheduler.resumeJob(monitor._id.toString());
|
||||
const result = this.scheduler.resumeJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Resumed monitor ${monitor._id}`,
|
||||
message: `Resumed monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "resumeJob",
|
||||
});
|
||||
};
|
||||
|
||||
updateJob = async (monitor) => {
|
||||
this.scheduler.updateJob(monitor._id.toString(), { repeat: monitor.interval, data: monitor.toObject() });
|
||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ class SuperSimpleQueueHelper {
|
||||
getMonitorJob = () => {
|
||||
return async (monitor) => {
|
||||
try {
|
||||
const monitorId = monitor._id;
|
||||
const monitorId = monitor.id;
|
||||
const teamId = monitor.teamId;
|
||||
if (!monitorId) {
|
||||
throw new Error("No monitor id");
|
||||
@@ -62,7 +62,7 @@ class SuperSimpleQueueHelper {
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
details: `Error sending notifications for job ${monitor._id}: ${error.message}`,
|
||||
details: `Error sending notifications for job ${monitor.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ class NetworkService {
|
||||
}
|
||||
|
||||
const pingResponse = {
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
type: "ping",
|
||||
status: response.alive,
|
||||
code: 200,
|
||||
@@ -120,9 +120,9 @@ class NetworkService {
|
||||
}
|
||||
|
||||
async requestHttp(monitor) {
|
||||
const { url, secret, _id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
|
||||
const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
|
||||
const httpResponse = {
|
||||
monitorId: _id,
|
||||
monitorId: id,
|
||||
teamId: teamId,
|
||||
type,
|
||||
};
|
||||
@@ -298,7 +298,7 @@ class NetworkService {
|
||||
});
|
||||
|
||||
const dockerResponse = {
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
type: monitor.type,
|
||||
};
|
||||
|
||||
@@ -416,7 +416,7 @@ class NetworkService {
|
||||
code: 200,
|
||||
status: response.success,
|
||||
message: this.stringService.portSuccess,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
type: monitor.type,
|
||||
responseTime: responseTime,
|
||||
};
|
||||
@@ -444,7 +444,7 @@ class NetworkService {
|
||||
code: 200,
|
||||
status: true,
|
||||
message: "Success",
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
type: "game",
|
||||
};
|
||||
|
||||
|
||||
@@ -7,14 +7,19 @@ class StatusService {
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* db: any
|
||||
* logger: any
|
||||
* buffer: import("./bufferService.js").BufferService
|
||||
* incidentService: import("../business/incidentService.js").IncidentService
|
||||
* monitorsRepository: any
|
||||
* }}
|
||||
*/ constructor({ db, logger, buffer, incidentService }) {
|
||||
*/
|
||||
constructor({ db, logger, buffer, incidentService, monitorsRepository }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.buffer = buffer;
|
||||
this.incidentService = incidentService;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -23,7 +28,7 @@ class StatusService {
|
||||
|
||||
async updateRunningStats({ monitor, networkResponse }) {
|
||||
try {
|
||||
const monitorId = monitor._id;
|
||||
const monitorId = monitor.id;
|
||||
const { responseTime, status } = networkResponse;
|
||||
// Get stats
|
||||
let stats = await MonitorStats.findOne({ monitorId });
|
||||
@@ -124,7 +129,7 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleIncidentForCheck",
|
||||
message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
stack: checkError.stack,
|
||||
});
|
||||
savedCheck = null;
|
||||
@@ -139,7 +144,7 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleIncidentForCheck",
|
||||
message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
action,
|
||||
stack: incidentError.stack,
|
||||
});
|
||||
@@ -150,7 +155,7 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleIncidentForCheck",
|
||||
message: `Error in ${errorContext}: ${error.message}`,
|
||||
monitorId: monitor?._id,
|
||||
monitorId: monitor?.id,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
@@ -172,7 +177,8 @@ class StatusService {
|
||||
await this.insertCheck(check);
|
||||
try {
|
||||
const { monitorId, status, code } = networkResponse;
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(monitorId);
|
||||
|
||||
// Update running stats
|
||||
this.updateRunningStats({ monitor, networkResponse });
|
||||
@@ -198,9 +204,9 @@ class StatusService {
|
||||
|
||||
// Return early if not enough data points
|
||||
if (monitor.statusWindow.length < monitor.statusWindowSize) {
|
||||
await monitor.save();
|
||||
const updated = await this.monitorsRepository.update(monitor.id, monitor);
|
||||
return {
|
||||
monitor,
|
||||
monitor: updated,
|
||||
statusChanged: false,
|
||||
prevStatus,
|
||||
code,
|
||||
@@ -240,14 +246,14 @@ class StatusService {
|
||||
|
||||
if (monitor.status === false && !statusChanged) {
|
||||
try {
|
||||
const lastManuallyResolvedIncident = await this.db.incidentModule.getLastManuallyResolvedIncident(monitor._id);
|
||||
const lastManuallyResolvedIncident = await this.db.incidentModule.getLastManuallyResolvedIncident(monitor.id);
|
||||
|
||||
let calculatedFailureRate = failureRate;
|
||||
|
||||
if (lastManuallyResolvedIncident && lastManuallyResolvedIncident.endTime) {
|
||||
try {
|
||||
const checksAfterResolution = await Check.find({
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
createdAt: { $gt: lastManuallyResolvedIncident.endTime },
|
||||
})
|
||||
.sort({ createdAt: 1 })
|
||||
@@ -267,7 +273,7 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "updateStatus",
|
||||
message: `Failed to query checks after manual resolution: ${checkQueryError.message}`,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
stack: checkQueryError.stack,
|
||||
});
|
||||
}
|
||||
@@ -281,17 +287,17 @@ class StatusService {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "updateStatus",
|
||||
message: `Error handling threshold check without status change: ${error.message}`,
|
||||
monitorId: monitor._id,
|
||||
monitorId: monitor.id,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
monitor.status = newStatus;
|
||||
await monitor.save();
|
||||
const updated = await this.monitorsRepository.update(monitor.id, monitor);
|
||||
|
||||
return {
|
||||
monitor,
|
||||
monitor: updated,
|
||||
statusChanged,
|
||||
prevStatus,
|
||||
code,
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "@/types/check.js";
|
||||
export * from "@/types/monitor.js";
|
||||
export * from "@/types/monitorStats.js";
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface MonitorStats {
|
||||
id: string;
|
||||
monitorId: string;
|
||||
avgResponseTime: number;
|
||||
totalChecks: number;
|
||||
totalUpChecks: number;
|
||||
totalDownChecks: number;
|
||||
uptimePercentage: number;
|
||||
lastCheckTimestamp: number;
|
||||
lastResponseTime: number;
|
||||
timeOfLastFailure?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -120,30 +120,30 @@ const getMonitorByIdQueryValidation = joi.object({
|
||||
const getMonitorsByTeamIdParamValidation = joi.object({});
|
||||
|
||||
const getMonitorsByTeamIdQueryValidation = joi.object({
|
||||
limit: joi.number(),
|
||||
type: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"))
|
||||
),
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
filter: joi.string(),
|
||||
field: joi.string(),
|
||||
order: joi.string().valid("asc", "desc"),
|
||||
filter: joi.string().allow("", null),
|
||||
});
|
||||
|
||||
const getMonitorStatsByIdParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
const getMonitorStatsByIdQueryValidation = joi.object({
|
||||
status: joi.string(),
|
||||
limit: joi.number(),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
|
||||
numToDisplay: joi.number(),
|
||||
normalize: joi.boolean(),
|
||||
const getMonitorsWithChecksQueryValidation = joi.object({
|
||||
limit: joi.number().integer().min(1).max(100).optional(),
|
||||
page: joi.number().integer().min(0).optional(),
|
||||
rowsPerPage: joi.number().integer().min(1).max(100).optional(),
|
||||
filter: joi.string().allow("", null).optional(),
|
||||
field: joi.string().optional(),
|
||||
order: joi.string().valid("asc", "desc").optional(),
|
||||
type: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"))
|
||||
)
|
||||
.optional(),
|
||||
explain: joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const getCertificateParamValidation = joi.object({
|
||||
@@ -725,8 +725,7 @@ export {
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByTeamIdParamValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
getMonitorStatsByIdParamValidation,
|
||||
getMonitorStatsByIdQueryValidation,
|
||||
getMonitorsWithChecksQueryValidation,
|
||||
getHardwareDetailsByIdParamValidation,
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
getCertificateParamValidation,
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import { MonitorService } from "../src/service/business/monitorService.ts";
|
||||
import type { IMonitorsRepository, IChecksRepository } from "../src/repositories/index.ts";
|
||||
|
||||
const createMonitorsRepositoryMock = () =>
|
||||
({
|
||||
findMonitorCountByTeamIdAndType: jest.fn(),
|
||||
findByTeamId: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
create: jest.fn(),
|
||||
createBulkMonitors: jest.fn(),
|
||||
deleteByTeamId: jest.fn(),
|
||||
}) as unknown as IMonitorsRepository;
|
||||
|
||||
const createChecksRepositoryMock = () =>
|
||||
({
|
||||
findLatestChecksByMonitorIds: jest.fn(),
|
||||
findDateRangeChecksByMonitor: jest.fn(),
|
||||
}) as unknown as IChecksRepository;
|
||||
|
||||
const createService = ({
|
||||
monitorsRepository = createMonitorsRepositoryMock(),
|
||||
checksRepository = createChecksRepositoryMock(),
|
||||
monitorStatsRepository = { findByMonitorId: jest.fn() },
|
||||
monitorModuleOverrides = {},
|
||||
}: {
|
||||
monitorsRepository?: IMonitorsRepository;
|
||||
checksRepository?: IChecksRepository;
|
||||
monitorStatsRepository?: { findByMonitorId: jest.Mock };
|
||||
monitorModuleOverrides?: Record<string, unknown>;
|
||||
} = {}) => {
|
||||
const monitorModule = {
|
||||
getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: () => true } }),
|
||||
getMonitorStatsById: jest.fn().mockResolvedValue({ latest: {} }),
|
||||
getMonitorsByTeamId: jest.fn().mockResolvedValue([]),
|
||||
getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [], summary: {} }),
|
||||
...monitorModuleOverrides,
|
||||
};
|
||||
|
||||
return new MonitorService({
|
||||
db: {
|
||||
monitorModule,
|
||||
statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() },
|
||||
checkModule: { deleteChecks: jest.fn() },
|
||||
pageSpeedCheckModule: { deletePageSpeedChecksByMonitorId: jest.fn() },
|
||||
notificationsModule: { deleteNotificationsByMonitorId: jest.fn() },
|
||||
},
|
||||
jobQueue: {
|
||||
addJob: jest.fn(),
|
||||
updateJob: jest.fn(),
|
||||
resumeJob: jest.fn(),
|
||||
pauseJob: jest.fn(),
|
||||
deleteJob: jest.fn(),
|
||||
},
|
||||
stringService: {},
|
||||
emailService: { buildEmail: jest.fn(), sendEmail: jest.fn() },
|
||||
papaparse: { parse: jest.fn(), unparse: jest.fn() },
|
||||
logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn() },
|
||||
errorService: {
|
||||
createAuthorizationError: jest.fn(() => new Error("unauthorized")),
|
||||
createServerError: jest.fn(() => new Error("server")),
|
||||
createBadRequestError: jest.fn(() => new Error("bad request")),
|
||||
createNotFoundError: jest.fn(() => new Error("not found")),
|
||||
},
|
||||
games: [],
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
});
|
||||
};
|
||||
|
||||
describe("MonitorService", () => {
|
||||
describe("getMonitorsWithChecksByTeamId", () => {
|
||||
it("returns monitors enriched with normalized checks", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
(monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(2);
|
||||
(monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([
|
||||
{ id: "m1", name: "Monitor 1", interval: 60000 },
|
||||
{ id: "m2", name: "Monitor 2", interval: 60000 },
|
||||
]);
|
||||
|
||||
const checksRepository = createChecksRepositoryMock();
|
||||
(checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({
|
||||
m1: [
|
||||
{ responseTime: 10, status: true, message: "OK" },
|
||||
{ responseTime: 20, status: true, message: "OK" },
|
||||
],
|
||||
m2: [{ responseTime: 50, status: true, message: "OK" }],
|
||||
});
|
||||
|
||||
const service = createService({ monitorsRepository, checksRepository });
|
||||
const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" });
|
||||
|
||||
expect(result).toMatchObject({ count: 2 });
|
||||
expect(result.monitors).toHaveLength(2);
|
||||
expect(result.monitors[0]).toHaveProperty("checks");
|
||||
expect(result.monitors[0].checks.length).toBeGreaterThan(0);
|
||||
expect(result.monitors[0].checks[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
responseTime: expect.any(Number),
|
||||
status: expect.any(Boolean),
|
||||
message: expect.any(String),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMonitorsByTeamId", () => {
|
||||
it("returns monitors array from db module", async () => {
|
||||
const monitorsPayload = [
|
||||
{ id: "m1", name: "Monitor 1" },
|
||||
{ id: "m2", name: "Monitor 2" },
|
||||
];
|
||||
const monitorModuleOverrides = {
|
||||
getMonitorsByTeamId: jest.fn().mockResolvedValue(monitorsPayload),
|
||||
};
|
||||
const service = createService({ monitorModuleOverrides });
|
||||
const result = await service.getMonitorsByTeamId({ teamId: "team" } as any);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toHaveProperty("id", "m1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMonitorsAndSummaryByTeamId", () => {
|
||||
it("returns monitors with summary block", async () => {
|
||||
const monitorModuleOverrides = {
|
||||
getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } }),
|
||||
};
|
||||
const service = createService({ monitorModuleOverrides });
|
||||
const result = await service.getMonitorsAndSummaryByTeamId({ teamId: "team" });
|
||||
expect(result).toEqual({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUptimeDetailsById", () => {
|
||||
it("returns monitorData and monitorStats with expected shape", async () => {
|
||||
const TEAM_ID = "team";
|
||||
const monitor = {
|
||||
id: "monitor-1",
|
||||
teamId: TEAM_ID,
|
||||
name: "Hardware monitor",
|
||||
interval: 60000,
|
||||
statusWindow: [],
|
||||
statusWindowSize: 5,
|
||||
statusWindowThreshold: 60,
|
||||
type: "http",
|
||||
ignoreTlsErrors: false,
|
||||
url: "https://example.com",
|
||||
isActive: true,
|
||||
alertThreshold: 5,
|
||||
cpuAlertThreshold: 5,
|
||||
memoryAlertThreshold: 5,
|
||||
diskAlertThreshold: 5,
|
||||
tempAlertThreshold: 5,
|
||||
selectedDisks: [],
|
||||
notifications: [],
|
||||
group: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
|
||||
const checksRepository = createChecksRepositoryMock();
|
||||
(checksRepository.findDateRangeChecksByMonitor as jest.Mock).mockResolvedValue({
|
||||
monitorType: "uptime",
|
||||
groupedChecks: [{ _id: "2024-01-01", avgResponseTime: 100, totalChecks: 2 }],
|
||||
groupedUpChecks: [{ _id: "2024-01-01", totalChecks: 2, avgResponseTime: 90 }],
|
||||
groupedDownChecks: [{ _id: "2024-01-01", totalChecks: 0, avgResponseTime: 0 }],
|
||||
uptimePercentage: 0.99,
|
||||
avgResponseTime: 95,
|
||||
});
|
||||
|
||||
const monitorStatsRepository = {
|
||||
findByMonitorId: jest.fn().mockResolvedValue({
|
||||
id: "stats-1",
|
||||
monitorId: monitor.id,
|
||||
avgResponseTime: 90,
|
||||
totalChecks: 10,
|
||||
totalUpChecks: 9,
|
||||
totalDownChecks: 1,
|
||||
uptimePercentage: 0.9,
|
||||
lastCheckTimestamp: 123456789,
|
||||
lastResponseTime: 80,
|
||||
timeOfLastFailure: 123456700,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}),
|
||||
} as any;
|
||||
const monitorModuleOverrides = {
|
||||
getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: (value: string) => value === TEAM_ID } }),
|
||||
};
|
||||
|
||||
const service = createService({ monitorsRepository, checksRepository, monitorModuleOverrides, monitorStatsRepository });
|
||||
const result = await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: "monitor-1", dateRange: "recent" });
|
||||
|
||||
expect(result).toHaveProperty("monitorData");
|
||||
expect(result.monitorData.monitor).toMatchObject({ id: monitor.id, name: monitor.name });
|
||||
expect(result.monitorData.groupedChecks[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
_id: expect.any(String),
|
||||
avgResponseTime: expect.any(Number),
|
||||
totalChecks: expect.any(Number),
|
||||
})
|
||||
);
|
||||
expect(result.monitorStats).toEqual(
|
||||
expect.objectContaining({
|
||||
monitorId: monitor.id,
|
||||
avgResponseTime: 90,
|
||||
totalChecks: 10,
|
||||
totalUpChecks: 9,
|
||||
totalDownChecks: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["jest"],
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
@@ -12,7 +12,8 @@
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"skipLibCheck": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["tests", "dist", "node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user