mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-12 12:39:05 -05:00
Merge branch 'develop' of https://github.com/bluewave-labs/bluewave-uptime into fix/PasswordRequirements
This commit is contained in:
@@ -0,0 +1 @@
|
||||
VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL
|
||||
@@ -24,3 +24,5 @@ dist-ssr
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
|
||||
!env.sh
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
for i in $(env | grep UPTIME_APP_)
|
||||
do
|
||||
key=$(echo $i | cut -d '=' -f 1)
|
||||
value=$(echo $i | cut -d '=' -f 2-)
|
||||
echo $key=$value
|
||||
find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' +
|
||||
done
|
||||
Generated
+18
-18
@@ -15,8 +15,8 @@
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.16",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.21.0",
|
||||
"@mui/x-date-pickers": "7.21.0",
|
||||
"@mui/x-data-grid": "7.22.0",
|
||||
"@mui/x-date-pickers": "7.22.0",
|
||||
"@reduxjs/toolkit": "2.3.0",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
@@ -29,7 +29,7 @@
|
||||
"react-router": "^6.23.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"recharts": "2.13.0",
|
||||
"recharts": "2.13.2",
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
@@ -1400,9 +1400,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-charts": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.21.0.tgz",
|
||||
"integrity": "sha512-Qv7U1Koo7hxinn1ncbn+Yfcwd8h3bSJDVCpjyKKgO0247kGIAK4ecrBlFHwVLol4bNTY36Ir1prEaA0G1MmUrg==",
|
||||
"version": "7.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.0.tgz",
|
||||
"integrity": "sha512-B70ix8keyww9CpfdwbsHygQGsgEySCXuHhGrDRiVyFgK+Be4edBWNswbL3ngIp37CHBbWegaYkPp/Q9GDas0AA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1458,9 +1458,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.21.0.tgz",
|
||||
"integrity": "sha512-0JAiwb2yRuVAd4idzfA64Bs1yn6KfC8VPBH7Njba0ySQB0+Ix+hvkzWQySD96hl7tK5heXbvbJ48pYLvhqS4Vw==",
|
||||
"version": "7.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.0.tgz",
|
||||
"integrity": "sha512-gXl7+hG0YRNU3YODlPvz6Q/9+EeUsPAWn/u2YMQmYTgwAxeY5QE3lY224VRnwM5v9SfTFheo1kzAKmXPdjb9tQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1495,9 +1495,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers": {
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.21.0.tgz",
|
||||
"integrity": "sha512-WLpuTu3PvhYwd7IAJSuDWr1Zd8c5C8Cc7rpAYCaV5+tGBoEP0C2UKqClMR4F1wTiU2a7x3dzgQzkcgK72yyqDw==",
|
||||
"version": "7.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.0.tgz",
|
||||
"integrity": "sha512-hopYo3ORP7ddYKnyBsqAtO2txEe2Zf6cehdikS5b1cqMTGOSL+18b11jfGVod9oipjb9L2JcT/WWkjoifs9Iww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -2805,9 +2805,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz",
|
||||
"integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==",
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz",
|
||||
"integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@kurkle/color": "^0.3.0"
|
||||
@@ -5518,9 +5518,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0.tgz",
|
||||
"integrity": "sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==",
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.2.tgz",
|
||||
"integrity": "sha512-UDLGFmnsBluDIPpQb9uty0ejb+jiVI71vkki8vVsR6ZCJdgjBfKQoQfft4re99CKlTy9qjQApxCLG6TrxJkeAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^2.0.0",
|
||||
|
||||
+4
-3
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build-dev": "vite build --mode development",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
@@ -17,8 +18,8 @@
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.16",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.21.0",
|
||||
"@mui/x-date-pickers": "7.21.0",
|
||||
"@mui/x-data-grid": "7.22.0",
|
||||
"@mui/x-date-pickers": "7.22.0",
|
||||
"@reduxjs/toolkit": "2.3.0",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
@@ -31,7 +32,7 @@
|
||||
"react-router": "^6.23.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"recharts": "2.13.0",
|
||||
"recharts": "2.13.2",
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
},
|
||||
|
||||
@@ -66,21 +66,6 @@ function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const thing = async () => {
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ authToken, settings: { apiBaseUrl: "test" } })
|
||||
);
|
||||
|
||||
if (action.payload.success) {
|
||||
console.log(action.payload.data);
|
||||
} else {
|
||||
console.log(action);
|
||||
}
|
||||
};
|
||||
thing();
|
||||
}, [dispatch, authToken]);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
|
||||
<CssBaseline />
|
||||
|
||||
@@ -41,6 +41,7 @@ const Field = forwardRef(
|
||||
placeholder,
|
||||
value,
|
||||
onChange,
|
||||
onBlur,
|
||||
onInput,
|
||||
error,
|
||||
disabled,
|
||||
@@ -115,6 +116,7 @@ const Field = forwardRef(
|
||||
value={value}
|
||||
onInput={onInput}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
inputRef={ref}
|
||||
inputProps={{
|
||||
@@ -216,6 +218,7 @@ Field.propTypes = {
|
||||
placeholder: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onInput: PropTypes.func,
|
||||
error: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
|
||||
@@ -46,6 +46,7 @@ const Select = ({
|
||||
value,
|
||||
items,
|
||||
onChange,
|
||||
onBlur,
|
||||
sx,
|
||||
name = "",
|
||||
}) => {
|
||||
@@ -76,6 +77,7 @@ const Select = ({
|
||||
className="select-component"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
displayEmpty
|
||||
name={name}
|
||||
inputProps={{ id: id }}
|
||||
@@ -140,6 +142,7 @@ Select.propTypes = {
|
||||
})
|
||||
).isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func,
|
||||
sx: PropTypes.object,
|
||||
};
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ export const updateAppSettings = createAsyncThunk(
|
||||
systemEmailAddress: settings.systemEmailAddress,
|
||||
systemEmailPassword: settings.systemEmailPassword,
|
||||
};
|
||||
console.log(parsedSettings);
|
||||
const res = await networkService.updateAppSettings({
|
||||
settings: parsedSettings,
|
||||
authToken,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
|
||||
const initialMode = window?.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? "dark" : "light";
|
||||
|
||||
// Initial state for UI settings.
|
||||
// Add more settings as needed (e.g., theme preferences, user settings)
|
||||
const initialState = {
|
||||
@@ -15,7 +17,7 @@ const initialState = {
|
||||
sidebar: {
|
||||
collapsed: false,
|
||||
},
|
||||
mode: "light",
|
||||
mode: initialMode,
|
||||
greeting: { index: 0, lastUpdate: null },
|
||||
timezone: "America/Toronto",
|
||||
};
|
||||
|
||||
@@ -12,6 +12,8 @@ import { useNavigate } from "react-router";
|
||||
import { getAppSettings, updateAppSettings } from "../../Features/Settings/settingsSlice";
|
||||
import { useState, useEffect } from "react";
|
||||
import Select from "../../Components/Inputs/Select";
|
||||
import { advancedSettingsValidation } from "../../Validation/validation";
|
||||
import { buildErrors, hasValidationErrors } from "../../Validation/error";
|
||||
|
||||
const AdvancedSettings = ({ isAdmin }) => {
|
||||
const navigate = useNavigate();
|
||||
@@ -21,7 +23,7 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
navigate("/");
|
||||
}
|
||||
}, [navigate, isAdmin]);
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const theme = useTheme();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
@@ -33,17 +35,30 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
systemEmailPort: "",
|
||||
systemEmailAddress: "",
|
||||
systemEmailPassword: "",
|
||||
jwtTTL: "",
|
||||
jwtTTLNum: 99,
|
||||
jwtTTLUnits: "days",
|
||||
jwtTTL: "99d",
|
||||
dbType: "",
|
||||
redisHost: "",
|
||||
redisPort: "",
|
||||
pagespeedApiKey: "",
|
||||
});
|
||||
|
||||
const parseJWTTTL = (data) => {
|
||||
if (data.jwtTTL) {
|
||||
const len = data.jwtTTL.length;
|
||||
data.jwtTTLNum = data.jwtTTL.substring(0, len - 1);
|
||||
data.jwtTTLUnits = unitItems.filter(
|
||||
(itm) => itm._id == data.jwtTTL.substring(len - 1)
|
||||
)[0].name;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getSettings = async () => {
|
||||
const action = await dispatch(getAppSettings({ authToken }));
|
||||
if (action.payload.success) {
|
||||
parseJWTTTL(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
} else {
|
||||
createToast({ body: "Failed to get settings" });
|
||||
@@ -66,24 +81,56 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
warn: 4,
|
||||
};
|
||||
|
||||
const unitItemLookup = {
|
||||
days: "d",
|
||||
hours: "h",
|
||||
};
|
||||
const unitItems = Object.keys(unitItemLookup).map((key) => ({
|
||||
_id: unitItemLookup[key],
|
||||
name: key,
|
||||
}));
|
||||
|
||||
const handleLogLevel = (e) => {
|
||||
const id = e.target.value;
|
||||
const newLogLevel = logItems.find((item) => item._id === id).name;
|
||||
setLocalSettings({ ...localSettings, logLevel: newLogLevel });
|
||||
};
|
||||
|
||||
const handleJWTTTLUnits = (e) => {
|
||||
const id = e.target.value;
|
||||
const newUnits = unitItems.find((item) => item._id === id).name;
|
||||
setLocalSettings({ ...localSettings, jwtTTLUnits: newUnits });
|
||||
};
|
||||
|
||||
const handleBlur = (event) => {
|
||||
const { value, id } = event.target;
|
||||
const { error } = advancedSettingsValidation.validate(
|
||||
{ [id]: value },
|
||||
{
|
||||
abortEarly: false,
|
||||
}
|
||||
);
|
||||
setErrors((prev) => {
|
||||
return buildErrors(prev, id, error);
|
||||
});
|
||||
};
|
||||
const handleChange = (event) => {
|
||||
const { value, id } = event.target;
|
||||
setLocalSettings({ ...localSettings, [id]: value });
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
localSettings.jwtTTL =
|
||||
localSettings.jwtTTLNum + unitItemLookup[localSettings.jwtTTLUnits];
|
||||
if (hasValidationErrors(localSettings, advancedSettingsValidation, setErrors)) {
|
||||
return;
|
||||
}
|
||||
const action = await dispatch(
|
||||
updateAppSettings({ settings: localSettings, authToken })
|
||||
);
|
||||
let body = "";
|
||||
if (action.payload.success) {
|
||||
console.log(action.payload.data);
|
||||
parseJWTTTL(action.payload.data);
|
||||
setLocalSettings(action.payload.data);
|
||||
body = "Settings saved successfully";
|
||||
} else {
|
||||
@@ -118,6 +165,8 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
label="API URL Host"
|
||||
value={localSettings.apiBaseUrl}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.apiBaseUrl}
|
||||
/>
|
||||
<Select
|
||||
id="logLevel"
|
||||
@@ -126,6 +175,8 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
items={logItems}
|
||||
value={logItemLookup[localSettings.logLevel]}
|
||||
onChange={handleLogLevel}
|
||||
onBlur={handleBlur}
|
||||
error={errors.logLevel}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@@ -141,18 +192,22 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
<Field
|
||||
type="text"
|
||||
id="systemEmailHost"
|
||||
label="Email host"
|
||||
label="System email host"
|
||||
name="systemEmailHost"
|
||||
value={localSettings.systemEmailHost}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailHost}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="systemEmailPort"
|
||||
label="System email address"
|
||||
label="System email port"
|
||||
name="systemEmailPort"
|
||||
value={localSettings.systemEmailPort.toString()}
|
||||
value={localSettings.systemEmailPort?.toString()}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailPort}
|
||||
/>
|
||||
<Field
|
||||
type="email"
|
||||
@@ -161,6 +216,7 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
name="systemEmailAddress"
|
||||
value={localSettings.systemEmailAddress}
|
||||
onChange={handleChange}
|
||||
error={errors.systemEmailAddress}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
@@ -169,6 +225,8 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
name="systemEmailPassword"
|
||||
value={localSettings.systemEmailPassword}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.systemEmailPassword}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
@@ -180,14 +238,33 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Field
|
||||
type="text"
|
||||
id="jwtTTL"
|
||||
label="JWT time to live"
|
||||
name="jwtTTL"
|
||||
value={localSettings.jwtTTL}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Field
|
||||
type="number"
|
||||
id="jwtTTLNum"
|
||||
label="JWT time to live"
|
||||
name="jwtTTLNum"
|
||||
value={localSettings.jwtTTLNum.toString()}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.jwtTTLNum}
|
||||
/>
|
||||
<Select
|
||||
id="jwtTTLUnits"
|
||||
label="JWT TTL Units"
|
||||
name="jwtTTLUnits"
|
||||
placeholder="Select time"
|
||||
isHidden={true}
|
||||
items={unitItems}
|
||||
value={unitItemLookup[localSettings.jwtTTLUnits]}
|
||||
onChange={handleJWTTTLUnits}
|
||||
onBlur={handleBlur}
|
||||
error={errors.jwtTTLUnits}
|
||||
/>
|
||||
</Stack>
|
||||
<Field
|
||||
type="text"
|
||||
id="dbType"
|
||||
@@ -195,6 +272,8 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
name="dbType"
|
||||
value={localSettings.dbType}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.dbType}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
@@ -203,14 +282,18 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
name="redisHost"
|
||||
value={localSettings.redisHost}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.redisHost}
|
||||
/>
|
||||
<Field
|
||||
type="number"
|
||||
id="redisPort"
|
||||
label="Redis port"
|
||||
name="redisPort"
|
||||
value={localSettings.redisPort.toString()}
|
||||
value={localSettings.redisPort?.toString()}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.redisPort}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
@@ -219,6 +302,8 @@ const AdvancedSettings = ({ isAdmin }) => {
|
||||
name="pagespeedApiKey"
|
||||
value={localSettings.pagespeedApiKey}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors.pagespeedApiKey}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
|
||||
@@ -74,7 +74,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
|
||||
sortOrder: "desc",
|
||||
limit: null,
|
||||
dateRange: null,
|
||||
sitler: filter,
|
||||
filter: filter,
|
||||
page: paginationController.page,
|
||||
rowsPerPage: paginationController.rowsPerPage,
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
MS_PER_WEEK,
|
||||
} from "../../../Utils/timeUtils";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
|
||||
|
||||
const getDurationAndUnit = (durationInMs) => {
|
||||
if (durationInMs % MS_PER_DAY === 0) {
|
||||
@@ -176,16 +177,6 @@ const CreateMaintenance = () => {
|
||||
fetchMonitors();
|
||||
}, [authToken, user]);
|
||||
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
updatedErrors[id] = error.details[0].message;
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
return updatedErrors;
|
||||
};
|
||||
|
||||
const handleSearch = (value) => {
|
||||
setSearch(value);
|
||||
};
|
||||
@@ -224,20 +215,8 @@ const CreateMaintenance = () => {
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const { error } = maintenanceWindowValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
// If errors, return early
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
logger.error(error);
|
||||
return;
|
||||
}
|
||||
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors))
|
||||
return;
|
||||
// Build timestamp for maintenance window from startDate and startTime
|
||||
const start = dayjs(form.startDate)
|
||||
.set("hour", form.startTime.hour())
|
||||
|
||||
@@ -95,6 +95,7 @@ const DetailsPage = ({ isAdmin }) => {
|
||||
setCertificateExpiry(formatDateWithTz(date, dateFormat, uiTimezone) ?? "N/A");
|
||||
}
|
||||
} catch (error) {
|
||||
setCertificateExpiry("N/A");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ class NetworkService {
|
||||
this.setBaseUrl(baseURL);
|
||||
this.unsubscribe = store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
console.log(state.settings.apiBaseUrl);
|
||||
if (BASE_URL !== undefined) {
|
||||
baseURL = BASE_URL;
|
||||
} else if (state?.settings?.apiBaseUrl ?? null) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const buildErrors = (prev, id, error) => {
|
||||
const updatedErrors = { ...prev };
|
||||
if (error) {
|
||||
updatedErrors[id] = error.details[0].message?? "Validation error";
|
||||
} else {
|
||||
delete updatedErrors[id];
|
||||
}
|
||||
return updatedErrors;
|
||||
};
|
||||
|
||||
const hasValidationErrors = (form, validation, setErrors) => {
|
||||
const { error } = validation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
if (
|
||||
![
|
||||
"clientHost",
|
||||
"refreshTokenSecret",
|
||||
"dbConnectionString",
|
||||
"refreshTokenTTL",
|
||||
"jwtTTL",
|
||||
].includes(err.path[0])
|
||||
) {
|
||||
newErrors[err.path[0]] = err.message ?? "Validation error";
|
||||
}
|
||||
});
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export { buildErrors, hasValidationErrors };
|
||||
@@ -135,10 +135,43 @@ const maintenanceWindowValidation = joi.object({
|
||||
monitors: joi.array().min(1),
|
||||
});
|
||||
|
||||
const advancedSettingsValidation = joi.object({
|
||||
apiBaseUrl: joi.string().uri({ allowRelative: true }).trim().messages({
|
||||
"string.empty": "API base url is required.",
|
||||
"string.uri": "The URL you provided is not valid.",
|
||||
}),
|
||||
logLevel: joi.string().valid("debug", "none", "error", "warn").allow(""),
|
||||
systemEmailHost: joi.string().allow(""),
|
||||
systemEmailPort: joi.number().allow(null, ""),
|
||||
systemEmailAddress: joi.string().allow(""),
|
||||
systemEmailPassword: joi.string().allow(""),
|
||||
jwtTTLNum: joi.number().messages({
|
||||
"number.base": "JWT TTL is required.",
|
||||
}),
|
||||
jwtTTLUnits: joi
|
||||
.string()
|
||||
.trim()
|
||||
.custom((value, helpers) => {
|
||||
if (!["days", "hours"].includes(value)) {
|
||||
return helpers.message("JWT TTL unit is required.");
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
dbType: joi.string().trim().messages({
|
||||
"string.empty": "DB type is required.",
|
||||
}),
|
||||
redisHost: joi.string().trim().messages({
|
||||
"string.empty": "Redis host is required.",
|
||||
}),
|
||||
redisPort: joi.number().allow(null, ""),
|
||||
pagespeedApiKey: joi.string().allow(""),
|
||||
});
|
||||
|
||||
export {
|
||||
credentials,
|
||||
imageValidation,
|
||||
monitorValidation,
|
||||
settingsValidation,
|
||||
maintenanceWindowValidation,
|
||||
advancedSettingsValidation
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ RUN npm install
|
||||
|
||||
COPY ../../Client .
|
||||
|
||||
RUN npm run build
|
||||
RUN npm run build-dev
|
||||
|
||||
RUN npm install -g serve
|
||||
|
||||
|
||||
Vendored
+4
-4
@@ -6,10 +6,10 @@ cd ../..
|
||||
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["bluewave/uptime_client"]="./Docker/dist/client.Dockerfile"
|
||||
["bluewave/database_mongo"]="./Docker/dist/mongoDB.Dockerfile"
|
||||
["bluewave/uptime_redis"]="./Docker/dist/redis.Dockerfile"
|
||||
["bluewave/uptime_server"]="./Docker/dist/server.Dockerfile"
|
||||
["bluewaveuptime/uptime_client"]="./Docker/dist/client.Dockerfile"
|
||||
["bluewaveuptime/uptime_database_mongo"]="./Docker/dist/mongoDB.Dockerfile"
|
||||
["bluewaveuptime/uptime_redis"]="./Docker/dist/redis.Dockerfile"
|
||||
["bluewaveuptime/uptime_server"]="./Docker/dist/server.Dockerfile"
|
||||
)
|
||||
|
||||
# Loop through each service and build the corresponding image
|
||||
|
||||
Vendored
+2
@@ -15,5 +15,7 @@ FROM nginx:1.27.1-alpine
|
||||
|
||||
COPY ./Docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
|
||||
RUN chmod +x /docker-entrypoint.d/env.sh
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
Vendored
+2
@@ -1,6 +1,8 @@
|
||||
services:
|
||||
client:
|
||||
image: bluewaveuptime/uptime_client:latest
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
@@ -13,5 +13,6 @@ RUN npm run build
|
||||
FROM nginx:1.27.1-alpine
|
||||
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
|
||||
RUN chmod +x /docker-entrypoint.d/env.sh
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,6 +1,8 @@
|
||||
services:
|
||||
client:
|
||||
image: uptime_client:latest
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://uptime-demo.bluewavelabs.ca/api/v1"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
@@ -17,6 +17,10 @@ const fetchMonitorCertificate = async (sslChecker, monitor) => {
|
||||
const monitorUrl = new URL(monitor.url);
|
||||
const hostname = monitorUrl.hostname;
|
||||
const cert = await sslChecker(hostname);
|
||||
// Throw an error if no cert or if cert.validTo is not present
|
||||
if (cert?.validTo === null || cert?.validTo === undefined) {
|
||||
throw new Error("Certificate not found");
|
||||
}
|
||||
return cert;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -86,21 +86,14 @@ const getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) =>
|
||||
const { monitorId } = req.params;
|
||||
const monitor = await req.db.getMonitorById(monitorId);
|
||||
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
|
||||
if (certificate && certificate.validTo) {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_CERTIFICATE,
|
||||
data: {
|
||||
certificateDate: new Date(certificate.validTo),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_CERTIFICATE,
|
||||
data: { certificateDate: "N/A" },
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_CERTIFICATE,
|
||||
data: {
|
||||
certificateDate: new Date(certificate.validTo),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorCertificate"));
|
||||
}
|
||||
@@ -128,12 +121,6 @@ const getMonitorById = async (req, res, next) => {
|
||||
|
||||
try {
|
||||
const monitor = await req.db.getMonitorById(req.params.monitorId);
|
||||
if (!monitor) {
|
||||
const error = new Error(errorMessages.MONITOR_GET_BY_ID);
|
||||
error.status = 404;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_GET_BY_ID,
|
||||
@@ -238,15 +225,16 @@ const createMonitor = async (req, res, next) => {
|
||||
const notifications = req.body.notifications;
|
||||
const monitor = await req.db.createMonitor(req, res);
|
||||
|
||||
if (notifications && notifications.length !== 0) {
|
||||
if (notifications && notifications.length > 0) {
|
||||
monitor.notifications = await Promise.all(
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = monitor._id;
|
||||
await req.db.createNotification(notification);
|
||||
return await req.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
await monitor.save();
|
||||
}
|
||||
|
||||
await monitor.save();
|
||||
// Add monitor to job queue
|
||||
req.jobQueue.addJob(monitor._id, monitor);
|
||||
return res.status(201).json({
|
||||
@@ -413,14 +401,13 @@ const editMonitor = async (req, res, next) => {
|
||||
|
||||
await req.db.deleteNotificationsByMonitorId(editedMonitor._id);
|
||||
|
||||
if (notifications && notifications.length !== 0) {
|
||||
await Promise.all(
|
||||
await Promise.all(
|
||||
notifications &&
|
||||
notifications.map(async (notification) => {
|
||||
notification.monitorId = editedMonitor._id;
|
||||
await req.db.createNotification(notification);
|
||||
})
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Delete the old job(editedMonitor has the same ID as the old monitor)
|
||||
await req.jobQueue.deleteJob(monitorBeforeEdit);
|
||||
@@ -456,11 +443,10 @@ const pauseMonitor = async (req, res, next) => {
|
||||
|
||||
try {
|
||||
const monitor = await req.db.getMonitorById(req.params.monitorId);
|
||||
if (monitor.isActive) {
|
||||
await req.jobQueue.deleteJob(monitor);
|
||||
} else {
|
||||
await req.jobQueue.addJob(monitor._id, monitor);
|
||||
}
|
||||
monitor.isActive === true
|
||||
? await req.jobQueue.deleteJob(monitor)
|
||||
: await req.jobQueue.addJob(monitor._id, monitor);
|
||||
|
||||
monitor.isActive = !monitor.isActive;
|
||||
monitor.status = undefined;
|
||||
monitor.save();
|
||||
|
||||
+61
-66
@@ -1,6 +1,63 @@
|
||||
import mongoose from "mongoose";
|
||||
import EmailService from "../../service/emailService.js";
|
||||
import Notification from "./Notification.js";
|
||||
|
||||
const BaseCheckSchema = mongoose.Schema({
|
||||
/**
|
||||
* Reference to the associated Monitor document.
|
||||
*
|
||||
* @type {mongoose.Schema.Types.ObjectId}
|
||||
*/
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
/**
|
||||
* Status of the check (true for up, false for down).
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
status: {
|
||||
type: Boolean,
|
||||
index: true,
|
||||
},
|
||||
/**
|
||||
* Response time of the check in milliseconds.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
responseTime: {
|
||||
type: Number,
|
||||
},
|
||||
/**
|
||||
* HTTP status code received during the check.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
statusCode: {
|
||||
type: Number,
|
||||
index: true,
|
||||
},
|
||||
/**
|
||||
* Message or description of the check result.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* Expiry date of the check, auto-calculated to expire after 30 days.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Check Schema for MongoDB collection.
|
||||
@@ -8,69 +65,7 @@ import Notification from "./Notification.js";
|
||||
* Represents a check associated with a monitor, storing information
|
||||
* about the status and response of a particular check event.
|
||||
*/
|
||||
const CheckSchema = mongoose.Schema(
|
||||
{
|
||||
/**
|
||||
* Reference to the associated Monitor document.
|
||||
*
|
||||
* @type {mongoose.Schema.Types.ObjectId}
|
||||
*/
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
/**
|
||||
* Status of the check (true for up, false for down).
|
||||
*
|
||||
* @type {Boolean}
|
||||
*/
|
||||
status: {
|
||||
type: Boolean,
|
||||
index: true,
|
||||
},
|
||||
/**
|
||||
* Response time of the check in milliseconds.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
responseTime: {
|
||||
type: Number,
|
||||
},
|
||||
/**
|
||||
* HTTP status code received during the check.
|
||||
*
|
||||
* @type {Number}
|
||||
*/
|
||||
statusCode: {
|
||||
type: Number,
|
||||
index: true,
|
||||
},
|
||||
/**
|
||||
* Message or description of the check result.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
/**
|
||||
* Expiry date of the check, auto-calculated to expire after 30 days.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true, // Adds createdAt and updatedAt timestamps
|
||||
}
|
||||
);
|
||||
|
||||
const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: true });
|
||||
CheckSchema.index({ createdAt: 1 });
|
||||
export default mongoose.model("Check", CheckSchema);
|
||||
export { BaseCheckSchema };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
import { BaseCheckSchema } from "./Check.js";
|
||||
const cpuSchema = mongoose.Schema({
|
||||
physical_core: { type: Number, default: 0 },
|
||||
logical_core: { type: Number, default: 0 },
|
||||
@@ -16,7 +16,7 @@ const memorySchema = mongoose.Schema({
|
||||
usage_percent: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
const discSchema = mongoose.Schema({
|
||||
const diskSchema = mongoose.Schema({
|
||||
read_speed_bytes: { type: Number, default: 0 },
|
||||
write_speed_bytes: { type: Number, default: 0 },
|
||||
total_bytes: { type: Number, default: 0 },
|
||||
@@ -32,11 +32,7 @@ const hostSchema = mongoose.Schema({
|
||||
|
||||
const HardwareCheckSchema = mongoose.Schema(
|
||||
{
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
},
|
||||
...BaseCheckSchema.obj,
|
||||
cpu: {
|
||||
type: cpuSchema,
|
||||
default: () => ({}),
|
||||
@@ -46,7 +42,7 @@ const HardwareCheckSchema = mongoose.Schema(
|
||||
default: () => ({}),
|
||||
},
|
||||
disk: {
|
||||
type: [discSchema],
|
||||
type: [diskSchema],
|
||||
default: () => [],
|
||||
},
|
||||
host: {
|
||||
@@ -57,4 +53,6 @@ const HardwareCheckSchema = mongoose.Schema(
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
HardwareCheckSchema.index({ createdAt: 1 });
|
||||
|
||||
export default mongoose.model("HardwareCheck", HardwareCheckSchema);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import mongoose from "mongoose";
|
||||
import Notification from "./Notification.js";
|
||||
|
||||
const MonitorSchema = mongoose.Schema(
|
||||
{
|
||||
@@ -48,12 +47,23 @@ const MonitorSchema = mongoose.Schema(
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
thresholds: {
|
||||
type: {
|
||||
usage_cpu: { type: Number },
|
||||
usage_memory: { type: Number },
|
||||
usage_disk: { type: Number },
|
||||
},
|
||||
_id: false,
|
||||
},
|
||||
notifications: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Notification",
|
||||
},
|
||||
],
|
||||
secret: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import mongoose from "mongoose";
|
||||
import { BaseCheckSchema } from "./Check.js";
|
||||
import logger from "../../utils/logger.js";
|
||||
import { time } from "console";
|
||||
const AuditSchema = mongoose.Schema({
|
||||
id: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
@@ -46,15 +48,7 @@ const AuditsSchema = mongoose.Schema({
|
||||
|
||||
const PageSpeedCheck = mongoose.Schema(
|
||||
{
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
...BaseCheckSchema.obj,
|
||||
accessibility: {
|
||||
type: Number,
|
||||
required: true,
|
||||
@@ -76,9 +70,7 @@ const PageSpeedCheck = mongoose.Schema(
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -112,4 +104,6 @@ PageSpeedCheck.pre("save", async function (next) {
|
||||
}
|
||||
});
|
||||
|
||||
PageSpeedCheck.index({ createdAt: 1 });
|
||||
|
||||
export default mongoose.model("PageSpeedCheck", PageSpeedCheck);
|
||||
|
||||
@@ -288,15 +288,20 @@ const getMonitorById = async (monitorId) => {
|
||||
try {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
|
||||
const error = new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
|
||||
error.status = 404;
|
||||
throw error;
|
||||
}
|
||||
// Get notifications
|
||||
const notifications = await Notification.find({
|
||||
monitorId: monitorId,
|
||||
});
|
||||
monitor.notifications = notifications;
|
||||
const monitorWithNotifications = await monitor.save();
|
||||
return monitorWithNotifications;
|
||||
const updatedMonitor = await Monitor.findByIdAndUpdate(
|
||||
monitorId,
|
||||
{ notifications },
|
||||
{ new: true }
|
||||
).populate("notifications");
|
||||
return updatedMonitor;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMonitorById";
|
||||
|
||||
+8
-1
@@ -40,6 +40,9 @@ import mjml2html from "mjml";
|
||||
import SettingsService from "./service/settingsService.js";
|
||||
import AppSettings from "./db/models/AppSettings.js";
|
||||
|
||||
import StatusService from "./service/statusService.js";
|
||||
import NotificationService from "./service/notificationService.js";
|
||||
|
||||
import db from "./db/mongo/MongoDB.js";
|
||||
const SERVICE_NAME = "Server";
|
||||
|
||||
@@ -125,10 +128,14 @@ const startApp = async () => {
|
||||
nodemailer,
|
||||
logger
|
||||
);
|
||||
const networkService = new NetworkService(db, emailService, axios, ping, logger, http);
|
||||
const networkService = new NetworkService(axios, ping, logger, http);
|
||||
const statusService = new StatusService(db, logger);
|
||||
const notificationService = new NotificationService(emailService, db, logger);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
Queue,
|
||||
|
||||
Generated
+10
-10
@@ -35,7 +35,7 @@
|
||||
"c8": "10.1.2",
|
||||
"chai": "5.1.2",
|
||||
"esm": "3.2.25",
|
||||
"mocha": "10.7.3",
|
||||
"mocha": "10.8.2",
|
||||
"nodemon": "3.1.7",
|
||||
"prettier": "^3.3.3",
|
||||
"sinon": "19.0.2"
|
||||
@@ -4129,9 +4129,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mocha": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.3.tgz",
|
||||
"integrity": "sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A==",
|
||||
"version": "10.8.2",
|
||||
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz",
|
||||
"integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4369,9 +4369,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.2.tgz",
|
||||
"integrity": "sha512-Ok4VzMds9p5G3ZSUhmvBm1GdxanbzhS29jpSn02SPj+IXEVFnIdfwAlHHXWkyNscZKlcn8GuMi68FH++jo0flg==",
|
||||
"version": "8.7.3",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.7.3.tgz",
|
||||
"integrity": "sha512-Xl6+dzU5ZpEcDoJ8/AyrIdAwTY099QwpolvV73PIytpK13XqwllLq/9XeVzzLEQgmyvwBVGVgjmMrKbuezxrIA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.7.0",
|
||||
@@ -4624,9 +4624,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.9.15",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz",
|
||||
"integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==",
|
||||
"version": "6.9.16",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
|
||||
"integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@
|
||||
"c8": "10.1.2",
|
||||
"chai": "5.1.2",
|
||||
"esm": "3.2.25",
|
||||
"mocha": "10.7.3",
|
||||
"mocha": "10.8.2",
|
||||
"nodemon": "3.1.7",
|
||||
"prettier": "^3.3.3",
|
||||
"sinon": "19.0.2"
|
||||
|
||||
@@ -65,6 +65,7 @@ class EmailService {
|
||||
serverIsDownTemplate: this.loadTemplate("serverIsDown"),
|
||||
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
|
||||
passwordResetTemplate: this.loadTemplate("passwordReset"),
|
||||
thresholdViolatedTemplate: this.loadTemplate("thresholdViolated"),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+143
-60
@@ -11,12 +11,24 @@ const SERVICE_NAME = "JobQueue";
|
||||
*/
|
||||
class JobQueue {
|
||||
/**
|
||||
* Constructs a new JobQueue
|
||||
* @constructor
|
||||
* @param {SettingsService} settingsService - The settings service
|
||||
* @throws {Error}
|
||||
* @class JobQueue
|
||||
* @classdesc Manages job queue and workers.
|
||||
*
|
||||
* @param {Object} statusService - Service for handling status updates.
|
||||
* @param {Object} notificationService - Service for handling notifications.
|
||||
* @param {Object} settingsService - Service for retrieving settings.
|
||||
* @param {Object} logger - Logger for logging information.
|
||||
* @param {Function} Queue - Queue constructor.
|
||||
* @param {Function} Worker - Worker constructor.
|
||||
*/
|
||||
constructor(settingsService, logger, Queue, Worker) {
|
||||
constructor(
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker
|
||||
) {
|
||||
const settings = settingsService.getSettings() || {};
|
||||
|
||||
const { redisHost = "127.0.0.1", redisPort = 6379 } = settings;
|
||||
@@ -31,27 +43,45 @@ class JobQueue {
|
||||
this.workers = [];
|
||||
this.db = null;
|
||||
this.networkService = null;
|
||||
this.statusService = statusService;
|
||||
this.notificationService = notificationService;
|
||||
this.settingsService = settingsService;
|
||||
this.logger = logger;
|
||||
this.Worker = Worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory method to create a JobQueue
|
||||
* @static
|
||||
* @async
|
||||
* @returns {Promise<JobQueue>} - Returns a new JobQueue
|
||||
* Creates and initializes a JobQueue instance.
|
||||
*
|
||||
* @param {Object} db - Database service for accessing monitors.
|
||||
* @param {Object} networkService - Service for network operations.
|
||||
* @param {Object} statusService - Service for handling status updates.
|
||||
* @param {Object} notificationService - Service for handling notifications.
|
||||
* @param {Object} settingsService - Service for retrieving settings.
|
||||
* @param {Object} logger - Logger for logging information.
|
||||
* @param {Function} Queue - Queue constructor.
|
||||
* @param {Function} Worker - Worker constructor.
|
||||
* @returns {Promise<JobQueue>} - The initialized JobQueue instance.
|
||||
* @throws {Error} - Throws an error if initialization fails.
|
||||
*/
|
||||
static async createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker
|
||||
) {
|
||||
const queue = new JobQueue(settingsService, logger, Queue, Worker);
|
||||
const queue = new JobQueue(
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker
|
||||
);
|
||||
try {
|
||||
queue.db = db;
|
||||
queue.networkService = networkService;
|
||||
@@ -71,62 +101,103 @@ class JobQueue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given monitor is in a maintenance window.
|
||||
*
|
||||
* @param {string} monitorId - The ID of the monitor to check.
|
||||
* @returns {Promise<boolean>} - Returns true if the monitor is in a maintenance window, otherwise false.
|
||||
* @throws {Error} - Throws an error if the database query fails.
|
||||
*/
|
||||
async isInMaintenanceWindow(monitorId) {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
|
||||
// Check for active maintenance window:
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
|
||||
if (window.active) {
|
||||
const start = new Date(window.start);
|
||||
const end = new Date(window.end);
|
||||
const now = new Date();
|
||||
const repeatInterval = window.repeat || 0;
|
||||
|
||||
// If start is < now and end > now, we're in maintenance
|
||||
if (start <= now && end >= now) return true;
|
||||
|
||||
// If maintenance window was set in the past with a repeat,
|
||||
// we need to advance start and end to see if we are in range
|
||||
|
||||
while (start < now && repeatInterval !== 0) {
|
||||
start.setTime(start.getTime() + repeatInterval);
|
||||
end.setTime(end.getTime() + repeatInterval);
|
||||
if (start <= now && end >= now) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return acc;
|
||||
}, false);
|
||||
return maintenanceWindowIsActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a job handler function for processing jobs.
|
||||
*
|
||||
* @returns {Function} An async function that processes a job.
|
||||
*/
|
||||
createJobHandler() {
|
||||
return async (job) => {
|
||||
try {
|
||||
// Get all maintenance windows for this monitor
|
||||
const monitorId = job.data._id;
|
||||
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
|
||||
// If a maintenance window is active, we're done
|
||||
|
||||
if (maintenanceWindowActive) {
|
||||
this.logger.info({
|
||||
message: `Monitor ${monitorId} is in maintenance window`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current status
|
||||
const networkResponse = await this.networkService.getStatus(job);
|
||||
// Handle status change
|
||||
const { monitor, statusChanged, prevStatus } =
|
||||
await this.statusService.updateStatus(networkResponse);
|
||||
|
||||
//If status hasn't changed, we're done
|
||||
if (statusChanged === false) return;
|
||||
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
if (prevStatus === undefined) return;
|
||||
|
||||
this.notificationService.handleNotifications({
|
||||
...networkResponse,
|
||||
monitor,
|
||||
prevStatus,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
details: `Error processing job ${job.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a worker for the queue
|
||||
* Operations are carried out in the async callback
|
||||
* @returns {Worker} The newly created worker
|
||||
*/
|
||||
createWorker() {
|
||||
const worker = new this.Worker(
|
||||
QUEUE_NAME,
|
||||
async (job) => {
|
||||
try {
|
||||
// Get all maintenance windows for this monitor
|
||||
const monitorId = job.data._id;
|
||||
const maintenanceWindows =
|
||||
await this.db.getMaintenanceWindowsByMonitorId(monitorId);
|
||||
// Check for active maintenance window:
|
||||
const maintenanceWindowActive = maintenanceWindows.reduce((acc, window) => {
|
||||
if (window.active) {
|
||||
const start = new Date(window.start);
|
||||
const end = new Date(window.end);
|
||||
const now = new Date();
|
||||
const repeatInterval = window.repeat || 0;
|
||||
|
||||
while ((start < now) & (repeatInterval !== 0)) {
|
||||
start.setTime(start.getTime() + repeatInterval);
|
||||
end.setTime(end.getTime() + repeatInterval);
|
||||
}
|
||||
|
||||
if (start < now && end > now) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, false);
|
||||
if (!maintenanceWindowActive) {
|
||||
await this.networkService.getStatus(job);
|
||||
} else {
|
||||
this.logger.info({
|
||||
message: `Monitor ${monitorId} is in maintenance window`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
details: `Error processing job ${job.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
connection: this.connection,
|
||||
}
|
||||
);
|
||||
const worker = new this.Worker(QUEUE_NAME, this.createJobHandler(), {
|
||||
connection: this.connection,
|
||||
});
|
||||
return worker;
|
||||
}
|
||||
|
||||
@@ -233,6 +304,12 @@ class JobQueue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the statistics of jobs and workers.
|
||||
*
|
||||
* @returns {Promise<Object>} - An object containing job statistics and the number of workers.
|
||||
* @throws {Error} - Throws an error if the job statistics retrieval fails.
|
||||
*/
|
||||
async getJobStats() {
|
||||
try {
|
||||
const jobs = await this.queue.getJobs();
|
||||
@@ -313,6 +390,12 @@ class JobQueue {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the metrics of the job queue.
|
||||
*
|
||||
* @returns {Promise<Object>} - An object containing various job queue metrics.
|
||||
* @throws {Error} - Throws an error if the metrics retrieval fails.
|
||||
*/
|
||||
async getMetrics() {
|
||||
try {
|
||||
const metrics = {
|
||||
|
||||
+137
-377
@@ -1,32 +1,21 @@
|
||||
import { errorMessages, successMessages } from "../utils/messages.js";
|
||||
|
||||
/**
|
||||
* NetworkService
|
||||
* Constructs a new NetworkService instance.
|
||||
*
|
||||
* This service handles all network requests on the back end
|
||||
* This includes pings, http requests, and pagespeed checks
|
||||
* @param {Object} axios - The axios instance for HTTP requests.
|
||||
* @param {Object} ping - The ping utility for network checks.
|
||||
* @param {Object} logger - The logger instance for logging.
|
||||
* @param {Object} http - The HTTP utility for network operations.
|
||||
*/
|
||||
|
||||
class NetworkService {
|
||||
/**
|
||||
* Creates an instance of NetworkService.
|
||||
*
|
||||
* @param {Object} db - The database service.
|
||||
* @param {Object} emailService - The email service.
|
||||
* @param {Object} axios - The axios HTTP client.
|
||||
* @param {Object} ping - The ping service.
|
||||
* @param {Object} logger - The logging service.
|
||||
* @param {Object} http - The HTTP service.
|
||||
*/
|
||||
constructor(db, emailService, axios, ping, logger, http) {
|
||||
this.db = db;
|
||||
this.emailService = emailService;
|
||||
constructor(axios, ping, logger, http) {
|
||||
this.TYPE_PING = "ping";
|
||||
this.TYPE_HTTP = "http";
|
||||
this.TYPE_PAGESPEED = "pagespeed";
|
||||
this.TYPE_HARDWARE = "hardware";
|
||||
this.SERVICE_NAME = "NetworkService";
|
||||
this.NETWORK_ERROR = 5000;
|
||||
this.PING_ERROR = 5001;
|
||||
this.axios = axios;
|
||||
this.ping = ping;
|
||||
this.logger = logger;
|
||||
@@ -34,400 +23,171 @@ class NetworkService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the notification process for a monitor.
|
||||
* Times the execution of an asynchronous operation.
|
||||
*
|
||||
* @param {Object} monitor - The monitor object containing monitor details.
|
||||
* @param {boolean} isAlive - The status of the monitor (true if up, false if down).
|
||||
* @returns {Promise<void>}
|
||||
*/ async handleNotification(monitor, isAlive) {
|
||||
try {
|
||||
let template = isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
let status = isAlive === true ? "up" : "down";
|
||||
|
||||
const notifications = await this.db.getNotificationsByMonitorId(monitor._id);
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
await this.emailService.buildAndSendEmail(
|
||||
template,
|
||||
{ monitorName: monitor.name, monitorUrl: monitor.url },
|
||||
notification.address,
|
||||
`Monitor ${monitor.name} is ${status}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleNotification",
|
||||
details: `notification error for monitor: ${monitor._id}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the status update for a monitor job.
|
||||
*
|
||||
* @param {Object} job - The job object containing job details.
|
||||
* @param {boolean} isAlive - The status of the monitor (true if up, false if down).
|
||||
* @returns {Promise<void>}
|
||||
* @param {Function} operation - The asynchronous operation to be timed.
|
||||
* @returns {Promise<Object>} An object containing the response, response time, and optionally an error.
|
||||
* @property {Object|null} response - The response from the operation, or null if an error occurred.
|
||||
* @property {number} responseTime - The time taken for the operation to complete, in milliseconds.
|
||||
* @property {Error} [error] - The error object if an error occurred during the operation.
|
||||
*/
|
||||
async handleStatusUpdate(job, isAlive) {
|
||||
let monitor;
|
||||
const { _id } = job.data;
|
||||
|
||||
// Look up the monitor, if it doesn't exist, it's probably been removed, return
|
||||
try {
|
||||
monitor = await this.db.getMonitorById(_id);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleStatusUpdate",
|
||||
stack: error.stack,
|
||||
details: `monitor lookup error for monitor: ${_id}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, try to update monitor status
|
||||
try {
|
||||
if (monitor.status === undefined || monitor.status !== isAlive) {
|
||||
const oldStatus = monitor.status;
|
||||
monitor.status = isAlive;
|
||||
await monitor.save();
|
||||
|
||||
if (oldStatus !== undefined && oldStatus !== isAlive) {
|
||||
this.handleNotification(monitor, isAlive);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleStatusUpdate",
|
||||
stack: error.stack,
|
||||
details: `status update error for monitor: ${_id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the response time of an asynchronous operation.
|
||||
* @param {Function} operation - An asynchronous operation to measure.
|
||||
* @returns {Promise<{responseTime: number, response: any}>} An object containing the response time in milliseconds and the response from the operation.
|
||||
* @throws {Error} The error object from the operation, contains response time.
|
||||
*/
|
||||
async measureResponseTime(operation) {
|
||||
async timeRequest(operation) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const response = await operation();
|
||||
const endTime = Date.now();
|
||||
return { responseTime: endTime - startTime, response };
|
||||
const responseTime = endTime - startTime;
|
||||
return { response, responseTime };
|
||||
} catch (error) {
|
||||
const endTime = Date.now();
|
||||
error.responseTime = endTime - startTime;
|
||||
error.service === undefined ? (error.service = this.SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "measureResponseTime") : null;
|
||||
throw error;
|
||||
const responseTime = endTime - startTime;
|
||||
return { response: null, responseTime, error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the ping operation for a given job, measures its response time, and logs the result.
|
||||
* @param {Object} job - The job object containing data for the ping operation.
|
||||
* @returns {Promise<{boolean}} The result of logging and storing the check
|
||||
* Sends a ping request to the specified URL and returns the response.
|
||||
*
|
||||
* @param {Object} job - The job object containing the data for the ping request.
|
||||
* @param {Object} job.data - The data object within the job.
|
||||
* @param {string} job.data.url - The URL to ping.
|
||||
* @param {string} job.data._id - The monitor ID for the ping request.
|
||||
* @returns {Promise<Object>} An object containing the ping response details.
|
||||
* @property {string} monitorId - The monitor ID for the ping request.
|
||||
* @property {string} type - The type of request, which is "ping".
|
||||
* @property {number} responseTime - The time taken for the ping request to complete, in milliseconds.
|
||||
* @property {Object} payload - The response payload from the ping request.
|
||||
* @property {boolean} status - The status of the ping request (true if successful, false otherwise).
|
||||
* @property {number} code - The response code (200 if successful, error code otherwise).
|
||||
* @property {string} message - The message indicating the result of the ping request.
|
||||
*/
|
||||
async handlePing(job) {
|
||||
let isAlive;
|
||||
|
||||
const operation = async () => {
|
||||
const response = await this.ping.promise.probe(job.data.url);
|
||||
return response;
|
||||
};
|
||||
|
||||
try {
|
||||
const { responseTime, response } = await this.measureResponseTime(operation);
|
||||
isAlive = response.alive;
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: isAlive,
|
||||
responseTime,
|
||||
message: isAlive
|
||||
? successMessages.PING_SUCCESS
|
||||
: errorMessages.PING_CANNOT_RESOLVE,
|
||||
};
|
||||
await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} catch (error) {
|
||||
isAlive = false;
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: isAlive,
|
||||
message: errorMessages.PING_CANNOT_RESOLVE,
|
||||
responseTime: error.responseTime,
|
||||
};
|
||||
await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} finally {
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the http operation for a given job, measures its response time, and logs the result.
|
||||
* @param {Object} job - The job object containing data for the ping operation.
|
||||
* @returns {Promise<{boolean}} The result of logging and storing the check
|
||||
*/
|
||||
async handleHttp(job) {
|
||||
// Define operation for timing
|
||||
const operation = async () => {
|
||||
const response = await this.axios.get(job.data.url);
|
||||
return response;
|
||||
};
|
||||
|
||||
let isAlive;
|
||||
|
||||
// attempt connection
|
||||
try {
|
||||
const { responseTime, response } = await this.measureResponseTime(operation);
|
||||
// check if response is in the 200 range, if so, service is up
|
||||
isAlive = response.status >= 200 && response.status < 300;
|
||||
|
||||
//Create a check with relevant data
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: isAlive,
|
||||
responseTime,
|
||||
statusCode: response.status,
|
||||
message: this.http.STATUS_CODES[response.status],
|
||||
};
|
||||
await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} catch (error) {
|
||||
const statusCode = error.response?.status || this.NETWORK_ERROR;
|
||||
let message = this.http.STATUS_CODES[statusCode] || "Network Error";
|
||||
isAlive = false;
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: isAlive,
|
||||
statusCode,
|
||||
responseTime: error.responseTime,
|
||||
message,
|
||||
};
|
||||
await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} finally {
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles PageSpeed job types by fetching and processing PageSpeed insights.
|
||||
*
|
||||
* This method sends a request to the Google PageSpeed Insights API to get performance metrics
|
||||
* for the specified URL, then logs and stores the check results.
|
||||
*
|
||||
* @param {Object} job - The job object containing data related to the PageSpeed check.
|
||||
* @param {string} job.data.url - The URL to be analyzed by the PageSpeed Insights API.
|
||||
* @param {string} job.data._id - The unique identifier for the monitor associated with the check.
|
||||
*
|
||||
* @returns {Promise<void>} A promise that resolves when the check results have been logged and stored.
|
||||
*
|
||||
* @throws {Error} Throws an error if there is an issue with fetching or processing the PageSpeed insights.
|
||||
*/
|
||||
async handlePagespeed(job) {
|
||||
let isAlive;
|
||||
try {
|
||||
const url = job.data.url;
|
||||
|
||||
const response = await this.axios.get(
|
||||
`https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`
|
||||
);
|
||||
const pageSpeedResults = response.data;
|
||||
const categories = pageSpeedResults.lighthouseResult?.categories;
|
||||
const audits = pageSpeedResults.lighthouseResult?.audits;
|
||||
const {
|
||||
"cumulative-layout-shift": cls,
|
||||
"speed-index": si,
|
||||
"first-contentful-paint": fcp,
|
||||
"largest-contentful-paint": lcp,
|
||||
"total-blocking-time": tbt,
|
||||
} = audits;
|
||||
// Weights
|
||||
// First Contentful Paint 10%
|
||||
// Speed Index 10%
|
||||
// Largest Contentful Paint 25%
|
||||
// Total Blocking Time 30%
|
||||
// Cumulative Layout Shift 25%
|
||||
|
||||
isAlive = true;
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: isAlive,
|
||||
statusCode: response.status,
|
||||
message: this.http.STATUS_CODES[response.status],
|
||||
accessibility: (categories.accessibility?.score || 0) * 100,
|
||||
bestPractices: (categories["best-practices"]?.score || 0) * 100,
|
||||
seo: (categories.seo?.score || 0) * 100,
|
||||
performance: (categories.performance?.score || 0) * 100,
|
||||
audits: {
|
||||
cls,
|
||||
si,
|
||||
fcp,
|
||||
lcp,
|
||||
tbt,
|
||||
},
|
||||
};
|
||||
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
|
||||
} catch (error) {
|
||||
isAlive = false;
|
||||
const statusCode = error.response?.status || this.NETWORK_ERROR;
|
||||
const message = this.http.STATUS_CODES[statusCode] || "Network Error";
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: isAlive,
|
||||
statusCode,
|
||||
message,
|
||||
accessibility: 0,
|
||||
bestPractices: 0,
|
||||
seo: 0,
|
||||
performance: 0,
|
||||
};
|
||||
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
|
||||
} finally {
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
}
|
||||
}
|
||||
|
||||
async handleHardware(job) {
|
||||
async requestPing(job) {
|
||||
const url = job.data.url;
|
||||
let isAlive;
|
||||
//TODO Fetch hardware data
|
||||
//For now, fake hardware data:
|
||||
const { response, responseTime, error } = await this.timeRequest(() =>
|
||||
this.ping.promise.probe(url)
|
||||
);
|
||||
|
||||
const hardwareData = {
|
||||
const pingResponse = {
|
||||
monitorId: job.data._id,
|
||||
cpu: {
|
||||
physical_core: 1,
|
||||
logical_core: 1,
|
||||
frequency: 266,
|
||||
temperature: null,
|
||||
free_percent: null,
|
||||
usage_percent: null,
|
||||
},
|
||||
memory: {
|
||||
total_bytes: 4,
|
||||
available_bytes: 4,
|
||||
used_bytes: 2,
|
||||
usage_percent: 0.5,
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
read_speed_bytes: 3,
|
||||
write_speed_bytes: 3,
|
||||
total_bytes: 10,
|
||||
free_bytes: 2,
|
||||
usage_percent: 0.8,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
os: "Linux",
|
||||
platform: "Ubuntu",
|
||||
kernel_version: "24.04",
|
||||
},
|
||||
type: "ping",
|
||||
responseTime,
|
||||
payload: response,
|
||||
};
|
||||
try {
|
||||
isAlive = true;
|
||||
this.logAndStoreCheck(hardwareData, this.db.createHardwareCheck);
|
||||
} catch (error) {
|
||||
isAlive = false;
|
||||
const nullData = {
|
||||
monitorId: job.data._id,
|
||||
cpu: {
|
||||
physical_core: 0,
|
||||
logical_core: 0,
|
||||
frequency: 0,
|
||||
temperature: 0,
|
||||
free_percent: 0,
|
||||
usage_percent: 0,
|
||||
},
|
||||
memory: {
|
||||
total_bytes: 0,
|
||||
available_bytes: 0,
|
||||
used_bytes: 0,
|
||||
usage_percent: 0,
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
read_speed_bytes: 0,
|
||||
write_speed_bytes: 0,
|
||||
total_bytes: 0,
|
||||
free_bytes: 0,
|
||||
usage_percent: 0,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
os: "",
|
||||
platform: "",
|
||||
kernel_version: "",
|
||||
},
|
||||
};
|
||||
this.logAndStoreCheck(nullData, this.db.createHardwareCheck);
|
||||
} finally {
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
if (error) {
|
||||
pingResponse.status = false;
|
||||
pingResponse.code = this.PING_ERROR;
|
||||
pingResponse.message = errorMessages.PING_CANNOT_RESOLVE;
|
||||
return pingResponse;
|
||||
}
|
||||
|
||||
pingResponse.code = 200;
|
||||
pingResponse.status = response.alive;
|
||||
pingResponse.message = successMessages.PING_SUCCESS;
|
||||
return pingResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status of a given job based on its type.
|
||||
* For unsupported job types, it logs an error and returns false.
|
||||
* Sends an HTTP GET request to the specified URL and returns the response.
|
||||
*
|
||||
* @param {Object} job - The job object containing data necessary for processing.
|
||||
* @returns {Promise<boolean>} The status of the job if it is supported and processed successfully, otherwise false.
|
||||
* @param {Object} job - The job object containing the data for the HTTP request.
|
||||
* @param {Object} job.data - The data object within the job.
|
||||
* @param {string} job.data.url - The URL to send the HTTP GET request to.
|
||||
* @param {string} job.data._id - The monitor ID for the HTTP request.
|
||||
* @param {string} [job.data.secret] - Secret for authorization if provided.
|
||||
* @returns {Promise<Object>} An object containing the HTTP response details.
|
||||
* @property {string} monitorId - The monitor ID for the HTTP request.
|
||||
* @property {string} type - The type of request, which is "http".
|
||||
* @property {number} responseTime - The time taken for the HTTP request to complete, in milliseconds.
|
||||
* @property {Object} payload - The response payload from the HTTP request.
|
||||
* @property {boolean} status - The status of the HTTP request (true if successful, false otherwise).
|
||||
* @property {number} code - The response code (200 if successful, error code otherwise).
|
||||
* @property {string} message - The message indicating the result of the HTTP request.
|
||||
*/
|
||||
async requestHttp(job) {
|
||||
const url = job.data.url;
|
||||
const config = {};
|
||||
|
||||
job.data.secret !== undefined &&
|
||||
(config.headers = { Authorization: `Bearer ${job.data.secret}` });
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(() =>
|
||||
this.axios.get(url, config)
|
||||
);
|
||||
|
||||
const httpResponse = {
|
||||
monitorId: job.data._id,
|
||||
type: job.data.type,
|
||||
responseTime,
|
||||
payload: response?.data,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
const code = error.response?.status || this.NETWORK_ERROR;
|
||||
httpResponse.code = code;
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.http.STATUS_CODES[code] || "Network Error";
|
||||
return httpResponse;
|
||||
}
|
||||
httpResponse.status = true;
|
||||
httpResponse.code = response.status;
|
||||
httpResponse.message = this.http.STATUS_CODES[response.status];
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the Google PageSpeed Insights API for the specified URL and returns the response.
|
||||
*
|
||||
* @param {Object} job - The job object containing the data for the PageSpeed request.
|
||||
* @param {Object} job.data - The data object within the job.
|
||||
* @param {string} job.data.url - The URL to analyze with PageSpeed Insights.
|
||||
* @param {string} job.data._id - The monitor ID for the PageSpeed request.
|
||||
* @returns {Promise<Object>} An object containing the PageSpeed response details.
|
||||
* @property {string} monitorId - The monitor ID for the PageSpeed request.
|
||||
* @property {string} type - The type of request, which is "pagespeed".
|
||||
* @property {number} responseTime - The time taken for the PageSpeed request to complete, in milliseconds.
|
||||
* @property {Object} payload - The response payload from the PageSpeed request.
|
||||
* @property {boolean} status - The status of the PageSpeed request (true if successful, false otherwise).
|
||||
* @property {number} code - The response code (200 if successful, error code otherwise).
|
||||
* @property {string} message - The message indicating the result of the PageSpeed request.
|
||||
*/
|
||||
async requestPagespeed(job) {
|
||||
const url = job.data.url;
|
||||
const updatedJob = { ...job };
|
||||
const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
||||
updatedJob.data.url = pagespeedUrl;
|
||||
return this.requestHttp(updatedJob);
|
||||
}
|
||||
|
||||
async requestHardware(job) {
|
||||
return this.requestHttp(job);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of a job based on its type and returns the appropriate response.
|
||||
*
|
||||
* @param {Object} job - The job object containing the data for the status request.
|
||||
* @param {Object} job.data - The data object within the job.
|
||||
* @param {string} job.data.type - The type of the job (e.g., "ping", "http", "pagespeed", "hardware").
|
||||
* @returns {Promise<Object>} The response object from the appropriate request method.
|
||||
* @throws {Error} Throws an error if the job type is unsupported.
|
||||
*/
|
||||
async getStatus(job) {
|
||||
switch (job.data.type) {
|
||||
case this.TYPE_PING:
|
||||
return await this.handlePing(job);
|
||||
return await this.requestPing(job);
|
||||
case this.TYPE_HTTP:
|
||||
return await this.handleHttp(job);
|
||||
return await this.requestHttp(job);
|
||||
case this.TYPE_PAGESPEED:
|
||||
return await this.handlePagespeed(job);
|
||||
return await this.requestPagespeed(job);
|
||||
case this.TYPE_HARDWARE:
|
||||
return await this.handleHardware(job);
|
||||
return await this.requestHardware(job);
|
||||
default:
|
||||
this.logger.error({
|
||||
message: `Unsupported type: ${job.data.type}`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "getStatus",
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs and stores the result of a check for a specific job.
|
||||
*
|
||||
* @param {Object} data - Data to be written
|
||||
* @param {function} writeToDB - DB write method
|
||||
*
|
||||
* @returns {Promise<boolean>} The status of the inserted check if successful, otherwise false.
|
||||
*/
|
||||
|
||||
async logAndStoreCheck(data, writeToDB) {
|
||||
try {
|
||||
const insertedCheck = await writeToDB(data);
|
||||
if (insertedCheck !== null && insertedCheck !== undefined) {
|
||||
return insertedCheck.status;
|
||||
}
|
||||
throw new Error();
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "logAndStoreCheck",
|
||||
details: `Error writing check for ${data.monitorId}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
class NotificationService {
|
||||
/**
|
||||
* Creates an instance of NotificationService.
|
||||
*
|
||||
* @param {Object} emailService - The email service used for sending notifications.
|
||||
* @param {Object} db - The database instance for storing notification data.
|
||||
* @param {Object} logger - The logger instance for logging activities.
|
||||
*/
|
||||
constructor(emailService, db, logger) {
|
||||
this.SERVICE_NAME = "NotificationService";
|
||||
this.emailService = emailService;
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email notification based on the network response.
|
||||
*
|
||||
* @param {Object} networkResponse - The response from the network monitor.
|
||||
* @param {Object} networkResponse.monitor - The monitor object containing details about the monitored service.
|
||||
* @param {string} networkResponse.monitor.name - The name of the monitor.
|
||||
* @param {string} networkResponse.monitor.url - The URL of the monitor.
|
||||
* @param {boolean} networkResponse.status - The current status of the monitor (true for up, false for down).
|
||||
* @param {boolean} networkResponse.prevStatus - The previous status of the monitor (true for up, false for down).
|
||||
* @param {string} address - The email address to send the notification to.
|
||||
*/
|
||||
async sendEmail(networkResponse, address) {
|
||||
const { monitor, status, prevStatus } = networkResponse;
|
||||
const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url };
|
||||
const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`;
|
||||
this.emailService.buildAndSendEmail(template, context, address, subject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles notifications based on the network response.
|
||||
*
|
||||
* @param {Object} networkResponse - The response from the network monitor.
|
||||
* @param {string} networkResponse.monitorId - The ID of the monitor.
|
||||
*/
|
||||
async handleNotifications(networkResponse) {
|
||||
try {
|
||||
const notifications = await this.db.getNotificationsByMonitorId(
|
||||
networkResponse.monitorId
|
||||
);
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
this.sendEmail(networkResponse, notification.address);
|
||||
}
|
||||
// Handle other types of notifications here
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "handleNotifications",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationService;
|
||||
@@ -0,0 +1,147 @@
|
||||
class StatusService {
|
||||
/**
|
||||
* Creates an instance of StatusService.
|
||||
*
|
||||
* @param {Object} db - The database instance.
|
||||
* @param {Object} logger - The logger instance.
|
||||
*/
|
||||
constructor(db, logger) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.SERVICE_NAME = "StatusService";
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the status of a monitor based on the network response.
|
||||
*
|
||||
* @param {Object} networkResponse - The network response containing monitorId and status.
|
||||
* @param {string} networkResponse.monitorId - The ID of the monitor.
|
||||
* @param {string} networkResponse.status - The new status of the monitor.
|
||||
* @returns {Promise<Object>} - A promise that resolves to an object containing the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred.
|
||||
* @returns {Promise<Object>} returnObject - The object returned by the function.
|
||||
* @returns {Object} returnObject.monitor - The monitor object.
|
||||
* @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed.
|
||||
* @returns {boolean} returnObject.prevStatus - The previous status of the monitor
|
||||
*/
|
||||
updateStatus = async (networkResponse) => {
|
||||
this.insertCheck(networkResponse);
|
||||
try {
|
||||
const { monitorId, status } = networkResponse;
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
// No change in monitor status, return early
|
||||
if (monitor.status === status) return { statusChanged: false };
|
||||
// Monitor status changed, save prev status and update monitor
|
||||
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
message: `${monitor.name} went from ${monitor.status === true ? "up" : "down"} to ${status === true ? "up" : "down"}`,
|
||||
prevStatus: monitor.status,
|
||||
newStatus: status,
|
||||
});
|
||||
|
||||
const prevStatus = monitor.status;
|
||||
monitor.status = status;
|
||||
await monitor.save();
|
||||
|
||||
return {
|
||||
monitor,
|
||||
statusChanged: true,
|
||||
prevStatus: prevStatus,
|
||||
};
|
||||
//
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
message: error.message,
|
||||
method: "updateStatus",
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a check object from the network response.
|
||||
*
|
||||
* @param {Object} networkResponse - The network response object.
|
||||
* @param {string} networkResponse.monitorId - The monitor ID.
|
||||
* @param {string} networkResponse.type - The type of the response.
|
||||
* @param {string} networkResponse.status - The status of the response.
|
||||
* @param {number} networkResponse.responseTime - The response time.
|
||||
* @param {number} networkResponse.code - The status code.
|
||||
* @param {string} networkResponse.message - The message.
|
||||
* @param {Object} networkResponse.payload - The payload of the response.
|
||||
* @returns {Object} The check object.
|
||||
*/
|
||||
buildCheck = (networkResponse) => {
|
||||
const { monitorId, type, status, responseTime, code, message, payload } =
|
||||
networkResponse;
|
||||
const check = {
|
||||
monitorId,
|
||||
status,
|
||||
statusCode: code,
|
||||
responseTime,
|
||||
message,
|
||||
};
|
||||
if (type === "pagespeed") {
|
||||
const categories = payload.lighthouseResult?.categories;
|
||||
const audits = payload.lighthouseResult?.audits;
|
||||
const {
|
||||
"cumulative-layout-shift": cls = 0,
|
||||
"speed-index": si = 0,
|
||||
"first-contentful-paint": fcp = 0,
|
||||
"largest-contentful-paint": lcp = 0,
|
||||
"total-blocking-time": tbt = 0,
|
||||
} = audits;
|
||||
check.accessibility = (categories.accessibility?.score || 0) * 100;
|
||||
check.bestPractices = (categories["best-practices"]?.score || 0) * 100;
|
||||
check.seo = (categories.seo?.score || 0) * 100;
|
||||
check.performance = (categories.performance?.score || 0) * 100;
|
||||
check.audits = { cls, si, fcp, lcp, tbt };
|
||||
}
|
||||
|
||||
if (type === "hardware") {
|
||||
check.cpu = payload?.cpu ?? {};
|
||||
check.memory = payload?.memory ?? {};
|
||||
check.disk = payload?.disk ?? {};
|
||||
check.host = payload?.host ?? {};
|
||||
}
|
||||
return check;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a check into the database based on the network response.
|
||||
*
|
||||
* @param {Object} networkResponse - The network response object.
|
||||
* @param {string} networkResponse.monitorId - The monitor ID.
|
||||
* @param {string} networkResponse.type - The type of the response.
|
||||
* @param {string} networkResponse.status - The status of the response.
|
||||
* @param {number} networkResponse.responseTime - The response time.
|
||||
* @param {number} networkResponse.code - The status code.
|
||||
* @param {string} networkResponse.message - The message.
|
||||
* @param {Object} networkResponse.payload - The payload of the response.
|
||||
* @returns {Promise<void>} A promise that resolves when the check is inserted.
|
||||
*/
|
||||
insertCheck = async (networkResponse) => {
|
||||
try {
|
||||
const operationMap = {
|
||||
http: this.db.createCheck,
|
||||
ping: this.db.createCheck,
|
||||
pagespeed: this.db.createPageSpeedCheck,
|
||||
hardware: this.db.createHardwareCheck,
|
||||
};
|
||||
const operation = operationMap[networkResponse.type];
|
||||
const check = this.buildCheck(networkResponse);
|
||||
await operation(check);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "insertCheck",
|
||||
details: `Error inserting check for monitor: ${networkResponse?.monitorId}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
export default StatusService;
|
||||
@@ -1,73 +1,45 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-font
|
||||
name="Roboto"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
|
||||
></mj-font>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
|
||||
<mj-text
|
||||
font-weight="300"
|
||||
font-size="16px"
|
||||
color="#616161"
|
||||
line-height="24px"
|
||||
></mj-text>
|
||||
<mj-section padding="0px"></mj-section>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section padding="20px 0">
|
||||
<mj-column width="100%">
|
||||
<mj-text
|
||||
align="left"
|
||||
font-size="10px"
|
||||
>
|
||||
Message from BlueWave Uptime Service
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column
|
||||
width="45%"
|
||||
padding-top="20px"
|
||||
>
|
||||
<mj-text
|
||||
align="center"
|
||||
font-weight="500"
|
||||
padding="0px"
|
||||
font-size="18px"
|
||||
color="red"
|
||||
>
|
||||
Google.com is down
|
||||
</mj-text>
|
||||
<mj-divider
|
||||
border-width="2px"
|
||||
border-color="#616161"
|
||||
></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column width="100%">
|
||||
<mj-text>
|
||||
<p>Hello {{name}}!</p>
|
||||
<p>
|
||||
We detected an incident on one of your monitors. Your service is currently
|
||||
down. We'll send a message to you once it is up again.
|
||||
</p>
|
||||
<p><b>Monitor name:</b> {{monitor}}</p>
|
||||
<p><b>URL:</b> {{url}}</p>
|
||||
<p><b>Problem:</b> {{problem}}</p>
|
||||
<p><b>Start date:</b> {{startDate}}</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="100%">
|
||||
<mj-divider
|
||||
border-width="1px"
|
||||
border-color="#E0E0E0"
|
||||
></mj-divider>
|
||||
<mj-button background-color="#1570EF"> View incident details </mj-button>
|
||||
<mj-text font-size="12px">
|
||||
<p>This email was sent by BlueWave Uptime.</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
<mj-head>
|
||||
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
|
||||
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
|
||||
<mj-section padding="0px"></mj-section>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section padding="20px 0">
|
||||
<mj-column width="100%">
|
||||
<mj-text align="left" font-size="10px">
|
||||
Message from BlueWave Uptime Service
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="45%" padding-top="20px">
|
||||
<mj-text align="center" font-weight="500" padding="0px" font-size="18px" color="red">
|
||||
Google.com is down
|
||||
</mj-text>
|
||||
<mj-divider border-width="2px" border-color="#616161"></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column width="100%">
|
||||
<mj-text>
|
||||
<p>Hello {{name}}!</p>
|
||||
<p>
|
||||
We detected an incident on one of your monitors. Your service is currently
|
||||
down. We'll send a message to you once it is up again.
|
||||
</p>
|
||||
<p><b>Monitor name:</b> {{monitor}}</p>
|
||||
<p><b>URL:</b> {{url}}</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="100%">
|
||||
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
|
||||
<mj-button background-color="#1570EF"> View incident details </mj-button>
|
||||
<mj-text font-size="12px">
|
||||
<p>This email was sent by BlueWave Uptime.</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -1,72 +1,42 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-font
|
||||
name="Roboto"
|
||||
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
|
||||
></mj-font>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
|
||||
<mj-text
|
||||
font-weight="300"
|
||||
font-size="16px"
|
||||
color="#616161"
|
||||
line-height="24px"
|
||||
></mj-text>
|
||||
<mj-section padding="0px"></mj-section>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section padding="20px 0">
|
||||
<mj-column width="100%">
|
||||
<mj-text
|
||||
align="left"
|
||||
font-size="10px"
|
||||
>
|
||||
Message from BlueWave Uptime Service
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column
|
||||
width="45%"
|
||||
padding-top="20px"
|
||||
>
|
||||
<mj-text
|
||||
align="center"
|
||||
font-weight="500"
|
||||
padding="0px"
|
||||
font-size="18px"
|
||||
color="green"
|
||||
>
|
||||
{{monitor}} is up
|
||||
</mj-text>
|
||||
<mj-divider
|
||||
border-width="2px"
|
||||
border-color="#616161"
|
||||
></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column width="100%">
|
||||
<mj-text>
|
||||
<p>Hello {{name}}!</p>
|
||||
<p>Your latest incident is resolved and your monitored service is up again.</p>
|
||||
<p><b>Monitor name:</b> {{monitor}}</p>
|
||||
<p><b>URL:</b> {{url}}</p>
|
||||
<p><b>Problem:</b> {{problem}}</p>
|
||||
<p><b>Start date:</b> {{startDate}}</p>
|
||||
<p><b>Resolved date:</b> {{resolvedDate}}</p>
|
||||
<p><b>Duration:</b>{{duration}}</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="100%">
|
||||
<mj-divider
|
||||
border-width="1px"
|
||||
border-color="#E0E0E0"
|
||||
></mj-divider>
|
||||
<mj-button background-color="#1570EF"> View incident details </mj-button>
|
||||
<mj-text font-size="12px">
|
||||
<p>This email was sent by BlueWave Uptime.</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
<mj-head>
|
||||
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
|
||||
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
|
||||
<mj-section padding="0px"></mj-section>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section padding="20px 0">
|
||||
<mj-column width="100%">
|
||||
<mj-text align="left" font-size="10px">
|
||||
Message from BlueWave Uptime Service
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="45%" padding-top="20px">
|
||||
<mj-text align="center" font-weight="500" padding="0px" font-size="18px" color="green">
|
||||
{{monitor}} is up
|
||||
</mj-text>
|
||||
<mj-divider border-width="2px" border-color="#616161"></mj-divider>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
<mj-section>
|
||||
<mj-column width="100%">
|
||||
<mj-text>
|
||||
<p>Hello {{name}}!</p>
|
||||
<p>Your latest incident is resolved and your monitored service is up again.</p>
|
||||
<p><b>Monitor name:</b> {{monitor}}</p>
|
||||
<p><b>URL:</b> {{url}}</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="100%">
|
||||
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
|
||||
<mj-button background-color="#1570EF"> View incident details </mj-button>
|
||||
<mj-text font-size="12px">
|
||||
<p>This email was sent by BlueWave Uptime.</p>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -0,0 +1,40 @@
|
||||
<mjml>
|
||||
<mj-head>
|
||||
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
|
||||
<mj-attributes>
|
||||
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
|
||||
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
|
||||
<mj-section padding="0px"></mj-section>
|
||||
</mj-attributes>
|
||||
</mj-head>
|
||||
<mj-body>
|
||||
<mj-section padding="20px 0">
|
||||
<mj-column width="100%">
|
||||
<mj-text align="left" font-size="10px">
|
||||
Message from BlueWave Uptime Service
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
<mj-column width="45%" padding-top="20px">
|
||||
<mj-text font-weight="500" padding="0px" font-size="18px">
|
||||
{{message}}
|
||||
</mj-text>
|
||||
<mj-text font-weight="500" padding="0px" font-size="18px">
|
||||
{{#if cpu}}
|
||||
{{cpu}}
|
||||
{{/if}}
|
||||
</mj-text>
|
||||
<mj-text font-weight="500" padding="0px" font-size="18px">
|
||||
{{#if disk}}
|
||||
{{disk}}
|
||||
{{/if}}
|
||||
</mj-text>
|
||||
<mj-text font-weight="500" padding="0px" font-size="18px">
|
||||
{{#if memory}}
|
||||
{{memory}}
|
||||
{{/if}}
|
||||
</mj-text>
|
||||
|
||||
</mj-column>
|
||||
</mj-section>
|
||||
</mj-body>
|
||||
</mjml>
|
||||
@@ -148,4 +148,11 @@ describe("controllerUtils - fetchMonitorCertificate", () => {
|
||||
const result = await fetchMonitorCertificate(sslChecker, monitor);
|
||||
expect(result).to.deep.equal({ validTo: "2022-01-01" });
|
||||
});
|
||||
it("should throw an error if a ssl-checker returns null", async () => {
|
||||
sslChecker.returns(null);
|
||||
await fetchMonitorCertificate(sslChecker, monitor).catch((error) => {
|
||||
expect(error).to.be.an("error");
|
||||
expect(error.message).to.equal("Certificate not found");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,19 +169,6 @@ describe("Monitor Controller - getMonitorCertificate", () => {
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
it("should return success message and data if all operations succeed with an invalid cert", async () => {
|
||||
req.db.getMonitorById.returns({ url: "https://www.google.com" });
|
||||
fetchMonitorCertificate.returns({});
|
||||
await getMonitorCertificate(req, res, next, fetchMonitorCertificate);
|
||||
expect(res.status.firstCall.args[0]).to.equal(200);
|
||||
expect(
|
||||
res.json.calledOnceWith({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_CERTIFICATE,
|
||||
data: { certificateDate: "N/A" },
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
it("should return an error if fetchMonitorCertificate fails", async () => {
|
||||
req.db.getMonitorById.returns({ url: "https://www.google.com" });
|
||||
fetchMonitorCertificate.throws(new Error("Certificate error"));
|
||||
@@ -232,7 +219,9 @@ describe("Monitor Controller - getMonitorById", () => {
|
||||
expect(next.firstCall.args[0].message).to.equal("DB error");
|
||||
});
|
||||
it("should return 404 if a monitor is not found", async () => {
|
||||
req.db.getMonitorById.returns(null);
|
||||
const error = new Error("Monitor not found");
|
||||
error.status = 404;
|
||||
req.db.getMonitorById.throws(error);
|
||||
await getMonitorById(req, res, next);
|
||||
expect(next.firstCall.args[0]).to.be.an("error");
|
||||
expect(next.firstCall.args[0].status).to.equal(404);
|
||||
@@ -430,20 +419,6 @@ describe("Monitor Controller - createMonitor", () => {
|
||||
expect(next.firstCall.args[0]).to.be.an("error");
|
||||
expect(next.firstCall.args[0].message).to.equal("Job error");
|
||||
});
|
||||
it("should return success message and data if all operations succeed with empty notifications", async () => {
|
||||
req.body.notifications = [];
|
||||
const monitor = { _id: "123", save: sinon.stub() };
|
||||
req.db.createMonitor.returns(monitor);
|
||||
await createMonitor(req, res, next);
|
||||
expect(res.status.firstCall.args[0]).to.equal(201);
|
||||
expect(
|
||||
res.json.calledOnceWith({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_CREATE,
|
||||
data: monitor,
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
it("should return success message and data if all operations succeed", async () => {
|
||||
const monitor = { _id: "123", save: sinon.stub() };
|
||||
req.db.createMonitor.returns(monitor);
|
||||
@@ -809,21 +784,6 @@ describe("Monitor Controller - editMonitor", () => {
|
||||
expect(next.firstCall.args[0]).to.be.an("error");
|
||||
expect(next.firstCall.args[0].message).to.equal("Add Job error");
|
||||
});
|
||||
it("should return success message with data if all operations succeed and empty notifications", async () => {
|
||||
req.body.notifications = [];
|
||||
const monitor = { _id: "123" };
|
||||
req.db.getMonitorById.returns({ teamId: "123" });
|
||||
req.db.editMonitor.returns(monitor);
|
||||
await editMonitor(req, res, next);
|
||||
expect(res.status.firstCall.args[0]).to.equal(200);
|
||||
expect(
|
||||
res.json.calledOnceWith({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_EDIT,
|
||||
data: monitor,
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
it("should return success message with data if all operations succeed", async () => {
|
||||
const monitor = { _id: "123" };
|
||||
req.db.getMonitorById.returns({ teamId: "123" });
|
||||
|
||||
@@ -44,21 +44,41 @@ class WorkerStub {
|
||||
}
|
||||
|
||||
describe("JobQueue", () => {
|
||||
let settingsService, logger, db, networkService;
|
||||
let settingsService,
|
||||
logger,
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
jobQueue;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
settingsService = { getSettings: sinon.stub() };
|
||||
statusService = { updateStatus: sinon.stub() };
|
||||
notificationService = { handleNotifications: sinon.stub() };
|
||||
|
||||
logger = { error: sinon.stub(), info: sinon.stub() };
|
||||
db = {
|
||||
getAllMonitors: sinon.stub().returns([]),
|
||||
getMaintenanceWindowsByMonitorId: sinon.stub().returns([]),
|
||||
};
|
||||
networkService = { getStatus: sinon.stub() };
|
||||
jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("createJobQueue", () => {
|
||||
it("should create a new JobQueue and add jobs for active monitors", async () => {
|
||||
db.getAllMonitors.returns([
|
||||
@@ -68,6 +88,8 @@ describe("JobQueue", () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -81,9 +103,11 @@ describe("JobQueue", () => {
|
||||
it("should reject with an error if an error occurs", async () => {
|
||||
db.getAllMonitors.throws("Error");
|
||||
try {
|
||||
await JobQueue.createJobQueue(
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -94,6 +118,7 @@ describe("JobQueue", () => {
|
||||
expect(error.method).to.equal("createJobQueue");
|
||||
}
|
||||
});
|
||||
|
||||
it("should reject with an error if an error occurs, should not overwrite error data", async () => {
|
||||
const error = new Error("Error");
|
||||
error.service = "otherService";
|
||||
@@ -101,9 +126,11 @@ describe("JobQueue", () => {
|
||||
db.getAllMonitors.throws(error);
|
||||
|
||||
try {
|
||||
await JobQueue.createJobQueue(
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -115,58 +142,53 @@ describe("JobQueue", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Constructor", () => {
|
||||
it("should construct a new JobQueue with default port and host if not provided", () => {
|
||||
it("should construct a new JobQueue with default port and host if not provided", async () => {
|
||||
settingsService.getSettings.returns({});
|
||||
const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub);
|
||||
|
||||
expect(jobQueue.connection.host).to.equal("127.0.0.1");
|
||||
expect(jobQueue.connection.port).to.equal(6379);
|
||||
});
|
||||
it("should construct a new JobQueue with provided port and host", () => {
|
||||
it("should construct a new JobQueue with provided port and host", async () => {
|
||||
settingsService.getSettings.returns({ redisHost: "localhost", redisPort: 1234 });
|
||||
const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
expect(jobQueue.connection.host).to.equal("localhost");
|
||||
expect(jobQueue.connection.port).to.equal(1234);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createWorker", () => {
|
||||
it("should create a new worker", async () => {
|
||||
const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub);
|
||||
const worker = jobQueue.createWorker();
|
||||
expect(worker).to.be.instanceOf(WorkerStub);
|
||||
});
|
||||
it("worker should handle a maintenanceWindow error", async () => {
|
||||
describe("isMaintenanceWindow", () => {
|
||||
it("should throw an error if error occurs", async () => {
|
||||
db.getMaintenanceWindowsByMonitorId.throws("Error");
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const worker = jobQueue.createWorker();
|
||||
await worker.workerTask();
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
try {
|
||||
jobQueue.isInMaintenanceWindow(1);
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("JobQueue");
|
||||
expect(error.method).to.equal("createWorker");
|
||||
}
|
||||
});
|
||||
it("worker should handle a maintenanceWindow that is not active", async () => {
|
||||
db.getMaintenanceWindowsByMonitorId.returns([
|
||||
{ start: 123, end: 123, repeat: 123456 },
|
||||
]);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const worker = jobQueue.createWorker();
|
||||
await worker.workerTask();
|
||||
expect(networkService.getStatus.calledOnce).to.be.true;
|
||||
});
|
||||
it("worker should handle a maintenanceWindow that is active", async () => {
|
||||
|
||||
it("should return true if in maintenance window with no repeat", async () => {
|
||||
db.getMaintenanceWindowsByMonitorId.returns([
|
||||
{
|
||||
active: true,
|
||||
@@ -178,42 +200,193 @@ describe("JobQueue", () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const worker = jobQueue.createWorker();
|
||||
await worker.workerTask();
|
||||
expect(networkService.getStatus.calledOnce).to.be.false;
|
||||
const inWindow = await jobQueue.isInMaintenanceWindow(1);
|
||||
expect(inWindow).to.be.true;
|
||||
});
|
||||
it("worker should handle a maintenanceWindow that is active, has a repeat, but is not in maintenance zone", async () => {
|
||||
|
||||
it("should return true if in maintenance window with repeat", async () => {
|
||||
db.getMaintenanceWindowsByMonitorId.returns([
|
||||
{
|
||||
active: true,
|
||||
start: new Date(Date.now() - 10000).toISOString(),
|
||||
end: new Date(Date.now() + 5000).toISOString(),
|
||||
repeat: 10000,
|
||||
end: new Date(Date.now() - 5000).toISOString(),
|
||||
repeat: 1000,
|
||||
},
|
||||
]);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const worker = jobQueue.createWorker();
|
||||
await worker.workerTask();
|
||||
expect(networkService.getStatus.calledOnce).to.be.true;
|
||||
const inWindow = await jobQueue.isInMaintenanceWindow(1);
|
||||
expect(inWindow).to.be.true;
|
||||
});
|
||||
|
||||
it("should return false if in end < start", async () => {
|
||||
db.getMaintenanceWindowsByMonitorId.returns([
|
||||
{
|
||||
active: true,
|
||||
start: new Date(Date.now() - 5000).toISOString(),
|
||||
end: new Date(Date.now() - 10000).toISOString(),
|
||||
repeat: 1000,
|
||||
},
|
||||
]);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const inWindow = await jobQueue.isInMaintenanceWindow(1);
|
||||
expect(inWindow).to.be.false;
|
||||
});
|
||||
it("should return false if not in maintenance window", async () => {
|
||||
db.getMaintenanceWindowsByMonitorId.returns([
|
||||
{
|
||||
active: false,
|
||||
start: new Date(Date.now() - 5000).toISOString(),
|
||||
end: new Date(Date.now() - 10000).toISOString(),
|
||||
repeat: 1000,
|
||||
},
|
||||
]);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const inWindow = await jobQueue.isInMaintenanceWindow(1);
|
||||
expect(inWindow).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("createJobHandler", () => {
|
||||
it("resolve to an error if an error is thrown within", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.isInMaintenanceWindow = sinon.stub().throws("Error");
|
||||
try {
|
||||
const handler = jobQueue.createJobHandler();
|
||||
await handler({ data: { _id: 1 } });
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("JobQueue");
|
||||
expect(error.details).to.equal(`Error processing job 1: Error`);
|
||||
}
|
||||
});
|
||||
|
||||
it("should log info if job is in maintenance window", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.isInMaintenanceWindow = sinon.stub().returns(true);
|
||||
const handler = jobQueue.createJobHandler();
|
||||
await handler({ data: { _id: 1 } });
|
||||
expect(logger.info.calledOnce).to.be.true;
|
||||
expect(logger.info.firstCall.args[0].message).to.equal(
|
||||
"Monitor 1 is in maintenance window"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return if status has not changed", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.isInMaintenanceWindow = sinon.stub().returns(false);
|
||||
statusService.updateStatus = sinon.stub().returns({ statusChanged: false });
|
||||
const handler = jobQueue.createJobHandler();
|
||||
await handler({ data: { _id: 1 } });
|
||||
expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true;
|
||||
});
|
||||
|
||||
it("should return if status has changed, but prevStatus was undefined (monitor paused)", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.isInMaintenanceWindow = sinon.stub().returns(false);
|
||||
statusService.updateStatus = sinon
|
||||
.stub()
|
||||
.returns({ statusChanged: true, prevStatus: undefined });
|
||||
const handler = jobQueue.createJobHandler();
|
||||
await handler({ data: { _id: 1 } });
|
||||
expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true;
|
||||
});
|
||||
it("should call notification service if status changed and monitor was not paused", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.isInMaintenanceWindow = sinon.stub().returns(false);
|
||||
statusService.updateStatus = sinon
|
||||
.stub()
|
||||
.returns({ statusChanged: true, prevStatus: false });
|
||||
const handler = jobQueue.createJobHandler();
|
||||
await handler({ data: { _id: 1 } });
|
||||
expect(jobQueue.notificationService.handleNotifications.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getWorkerStats", () => {
|
||||
it("should throw an error if getRepeatable Jobs fails", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -233,6 +406,8 @@ describe("JobQueue", () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -252,11 +427,14 @@ describe("JobQueue", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("scaleWorkers", () => {
|
||||
it("should scale workers to 5 if no workers", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -268,6 +446,8 @@ describe("JobQueue", () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -286,6 +466,8 @@ describe("JobQueue", () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -309,6 +491,8 @@ describe("JobQueue", () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -329,6 +513,8 @@ describe("JobQueue", () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -340,11 +526,13 @@ describe("JobQueue", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getJobs", async () => {
|
||||
describe("getJobs", () => {
|
||||
it("should return jobs", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -357,6 +545,8 @@ describe("JobQueue", () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -377,6 +567,8 @@ describe("JobQueue", () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
@@ -398,28 +590,12 @@ describe("JobQueue", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getJobStats", async () => {
|
||||
describe("getJobStats", () => {
|
||||
it("should return job stats for no jobs", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const jobStats = await jobQueue.getJobStats();
|
||||
expect(jobStats).to.deep.equal({ jobs: [], workers: 5 });
|
||||
});
|
||||
it("should return job stats for jobs", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.getJobs = async () => {
|
||||
return [{ data: { url: "test" }, getState: async () => "completed" }];
|
||||
};
|
||||
@@ -430,14 +606,6 @@ describe("JobQueue", () => {
|
||||
});
|
||||
});
|
||||
it("should reject with an error if mapping jobs fails", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.getJobs = async () => {
|
||||
return [
|
||||
{
|
||||
@@ -457,14 +625,6 @@ describe("JobQueue", () => {
|
||||
}
|
||||
});
|
||||
it("should reject with an error if mapping jobs fails but respect existing error data", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.getJobs = async () => {
|
||||
return [
|
||||
{
|
||||
@@ -488,28 +648,12 @@ describe("JobQueue", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("addJob", async () => {
|
||||
describe("addJob", () => {
|
||||
it("should add a job to the queue", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.addJob("test", { url: "test" });
|
||||
expect(jobQueue.queue.jobs.length).to.equal(1);
|
||||
});
|
||||
it("should reject with an error if adding fails", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.add = async () => {
|
||||
throw new Error("Error adding job");
|
||||
};
|
||||
@@ -522,14 +666,6 @@ describe("JobQueue", () => {
|
||||
}
|
||||
});
|
||||
it("should reject with an error if adding fails but respect existing error data", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.add = async () => {
|
||||
const error = new Error("Error adding job");
|
||||
error.service = "otherService";
|
||||
@@ -545,16 +681,8 @@ describe("JobQueue", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("deleteJob", async () => {
|
||||
describe("deleteJob", () => {
|
||||
it("should delete a job from the queue", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] });
|
||||
jobQueue.scaleWorkers = sinon.stub();
|
||||
const monitor = { _id: 1 };
|
||||
@@ -567,14 +695,6 @@ describe("JobQueue", () => {
|
||||
// expect(jobQueue.scaleWorkers.calledOnce).to.be.true;
|
||||
});
|
||||
it("should log an error if job is not found", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] });
|
||||
jobQueue.scaleWorkers = sinon.stub();
|
||||
const monitor = { _id: 1 };
|
||||
@@ -584,14 +704,6 @@ describe("JobQueue", () => {
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
});
|
||||
it("should reject with an error if removeRepeatable fails", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.removeRepeatable = async () => {
|
||||
const error = new Error("removeRepeatable error");
|
||||
throw error;
|
||||
@@ -606,14 +718,6 @@ describe("JobQueue", () => {
|
||||
}
|
||||
});
|
||||
it("should reject with an error if removeRepeatable fails but respect existing error data", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.removeRepeatable = async () => {
|
||||
const error = new Error("removeRepeatable error");
|
||||
error.service = "otherService";
|
||||
@@ -632,14 +736,6 @@ describe("JobQueue", () => {
|
||||
});
|
||||
describe("getMetrics", () => {
|
||||
it("should return metrics for the job queue", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.getWaitingCount = async () => 1;
|
||||
jobQueue.queue.getActiveCount = async () => 2;
|
||||
jobQueue.queue.getCompletedCount = async () => 3;
|
||||
@@ -657,14 +753,6 @@ describe("JobQueue", () => {
|
||||
});
|
||||
});
|
||||
it("should log an error if metrics operations fail", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.getWaitingCount = async () => {
|
||||
throw new Error("Error");
|
||||
};
|
||||
@@ -676,14 +764,6 @@ describe("JobQueue", () => {
|
||||
|
||||
describe("obliterate", () => {
|
||||
it("should return true if obliteration is successful", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.pause = async () => true;
|
||||
jobQueue.getJobs = async () => [{ key: 1, id: 1 }];
|
||||
jobQueue.queue.removeRepeatableByKey = async () => true;
|
||||
@@ -693,15 +773,6 @@ describe("JobQueue", () => {
|
||||
expect(obliteration).to.be.true;
|
||||
});
|
||||
it("should throw an error if obliteration fails", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
|
||||
jobQueue.getMetrics = async () => {
|
||||
throw new Error("Error");
|
||||
};
|
||||
@@ -714,15 +785,6 @@ describe("JobQueue", () => {
|
||||
}
|
||||
});
|
||||
it("should throw an error if obliteration fails but respect existing error data", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
|
||||
jobQueue.getMetrics = async () => {
|
||||
const error = new Error("Error");
|
||||
error.service = "otherService";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
import sinon from "sinon";
|
||||
import NotificationService from "../../service/notificationService.js";
|
||||
import { expect } from "chai";
|
||||
|
||||
describe("NotificationService", () => {
|
||||
let emailService, db, logger, notificationService;
|
||||
beforeEach(() => {
|
||||
db = {
|
||||
getNotificationsByMonitorId: sinon.stub(),
|
||||
};
|
||||
emailService = {
|
||||
buildAndSendEmail: sinon.stub(),
|
||||
};
|
||||
logger = {
|
||||
warn: sinon.stub(),
|
||||
};
|
||||
|
||||
notificationService = new NotificationService(emailService, db, logger);
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create a new instance of NotificationService", () => {
|
||||
expect(notificationService).to.be.an.instanceOf(NotificationService);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendEmail", async () => {
|
||||
it("should send an email notification with Up Template", async () => {
|
||||
const networkResponse = {
|
||||
monitor: {
|
||||
name: "Test Monitor",
|
||||
url: "http://test.com",
|
||||
},
|
||||
status: true,
|
||||
prevStatus: false,
|
||||
};
|
||||
const address = "test@test.com";
|
||||
await notificationService.sendEmail(networkResponse, address);
|
||||
expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true;
|
||||
expect(
|
||||
notificationService.emailService.buildAndSendEmail.calledWith(
|
||||
"serverIsUpTemplate",
|
||||
{ monitor: "Test Monitor", url: "http://test.com" }
|
||||
)
|
||||
);
|
||||
});
|
||||
it("should send an email notification with Down Template", async () => {
|
||||
const networkResponse = {
|
||||
monitor: {
|
||||
name: "Test Monitor",
|
||||
url: "http://test.com",
|
||||
},
|
||||
status: false,
|
||||
prevStatus: true,
|
||||
};
|
||||
const address = "test@test.com";
|
||||
await notificationService.sendEmail(networkResponse, address);
|
||||
expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true;
|
||||
});
|
||||
it("should send an email notification with Up Template", async () => {
|
||||
const networkResponse = {
|
||||
monitor: {
|
||||
name: "Test Monitor",
|
||||
url: "http://test.com",
|
||||
},
|
||||
status: true,
|
||||
prevStatus: false,
|
||||
};
|
||||
const address = "test@test.com";
|
||||
await notificationService.sendEmail(networkResponse, address);
|
||||
expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleNotifications", async () => {
|
||||
it("should handle notifications based on the network response", async () => {
|
||||
notificationService.sendEmail = sinon.stub();
|
||||
notificationService.db.getNotificationsByMonitorId.resolves([
|
||||
{ type: "email", address: "www.google.com" },
|
||||
]);
|
||||
await notificationService.handleNotifications({ monitorId: "123" });
|
||||
expect(notificationService.sendEmail.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle an error when getting notifications", async () => {
|
||||
const testError = new Error("Test Error");
|
||||
notificationService.db.getNotificationsByMonitorId.rejects(testError);
|
||||
await notificationService.handleNotifications({ monitorId: "123" });
|
||||
expect(notificationService.logger.warn.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import sinon from "sinon";
|
||||
import StatusService from "../../service/statusService.js";
|
||||
import { afterEach, describe } from "node:test";
|
||||
|
||||
describe("StatusService", () => {
|
||||
let db, logger, statusService;
|
||||
beforeEach(() => {
|
||||
db = {
|
||||
getMonitorById: sinon.stub(),
|
||||
createCheck: sinon.stub(),
|
||||
createPagespeedCheck: sinon.stub(),
|
||||
};
|
||||
logger = {
|
||||
info: sinon.stub(),
|
||||
error: sinon.stub(),
|
||||
};
|
||||
statusService = new StatusService(db, logger);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("should create an instance of StatusService", () => {
|
||||
expect(statusService).to.be.an.instanceOf(StatusService);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateStatus", async () => {
|
||||
beforeEach(() => {
|
||||
// statusService.insertCheck = sinon.stub().resolves;
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
it("should throw an error if an error occurs", async () => {
|
||||
const error = new Error("Test error");
|
||||
statusService.db.getMonitorById = sinon.stub().throws(error);
|
||||
try {
|
||||
await statusService.updateStatus({ monitorId: "test", status: true });
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("Test error");
|
||||
}
|
||||
// expect(statusService.insertCheck.calledOnce).to.be.true;
|
||||
});
|
||||
it("should return {statusChanged: false} if status hasn't changed", async () => {
|
||||
statusService.db.getMonitorById = sinon.stub().returns({ status: true });
|
||||
const result = await statusService.updateStatus({
|
||||
monitorId: "test",
|
||||
status: true,
|
||||
});
|
||||
expect(result).to.deep.equal({ statusChanged: false });
|
||||
// expect(statusService.insertCheck.calledOnce).to.be.true;
|
||||
});
|
||||
it("should return {statusChanged: true} if status has changed from down to up", async () => {
|
||||
statusService.db.getMonitorById = sinon
|
||||
.stub()
|
||||
.returns({ status: false, save: sinon.stub() });
|
||||
const result = await statusService.updateStatus({
|
||||
monitorId: "test",
|
||||
status: true,
|
||||
});
|
||||
expect(result.statusChanged).to.be.true;
|
||||
expect(result.monitor.status).to.be.true;
|
||||
expect(result.prevStatus).to.be.false;
|
||||
// expect(statusService.insertCheck.calledOnce).to.be.true;
|
||||
});
|
||||
it("should return {statusChanged: true} if status has changed from up to down", async () => {
|
||||
statusService.db.getMonitorById = sinon
|
||||
.stub()
|
||||
.returns({ status: true, save: sinon.stub() });
|
||||
const result = await statusService.updateStatus({
|
||||
monitorId: "test",
|
||||
status: false,
|
||||
});
|
||||
expect(result.statusChanged).to.be.true;
|
||||
expect(result.monitor.status).to.be.false;
|
||||
expect(result.prevStatus).to.be.true;
|
||||
// expect(statusService.insertCheck.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildCheck", () => {
|
||||
it("should build a check object", () => {
|
||||
const check = statusService.buildCheck({
|
||||
monitorId: "test",
|
||||
type: "test",
|
||||
status: true,
|
||||
responseTime: 100,
|
||||
code: 200,
|
||||
message: "Test message",
|
||||
payload: { test: "test" },
|
||||
});
|
||||
expect(check.monitorId).to.equal("test");
|
||||
expect(check.status).to.be.true;
|
||||
expect(check.statusCode).to.equal(200);
|
||||
expect(check.responseTime).to.equal(100);
|
||||
expect(check.message).to.equal("Test message");
|
||||
});
|
||||
it("should build a check object for pagespeed type", () => {
|
||||
const check = statusService.buildCheck({
|
||||
monitorId: "test",
|
||||
type: "pagespeed",
|
||||
status: true,
|
||||
responseTime: 100,
|
||||
code: 200,
|
||||
message: "Test message",
|
||||
payload: {
|
||||
lighthouseResult: {
|
||||
categories: {
|
||||
accessibility: { score: 1 },
|
||||
"best-practices": { score: 1 },
|
||||
performance: { score: 1 },
|
||||
seo: { score: 1 },
|
||||
},
|
||||
audits: {
|
||||
"cumulative-layout-shift": { score: 1 },
|
||||
"speed-index": { score: 1 },
|
||||
"first-contentful-paint": { score: 1 },
|
||||
"largest-contentful-paint": { score: 1 },
|
||||
"total-blocking-time": { score: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(check.monitorId).to.equal("test");
|
||||
expect(check.status).to.be.true;
|
||||
expect(check.statusCode).to.equal(200);
|
||||
expect(check.responseTime).to.equal(100);
|
||||
expect(check.message).to.equal("Test message");
|
||||
expect(check.accessibility).to.equal(100);
|
||||
expect(check.bestPractices).to.equal(100);
|
||||
expect(check.performance).to.equal(100);
|
||||
expect(check.seo).to.equal(100);
|
||||
expect(check.audits).to.deep.equal({
|
||||
cls: { score: 1 },
|
||||
si: { score: 1 },
|
||||
fcp: { score: 1 },
|
||||
lcp: { score: 1 },
|
||||
tbt: { score: 1 },
|
||||
});
|
||||
});
|
||||
it("should build a check object for pagespeed type with missing data", () => {
|
||||
const check = statusService.buildCheck({
|
||||
monitorId: "test",
|
||||
type: "pagespeed",
|
||||
status: true,
|
||||
responseTime: 100,
|
||||
code: 200,
|
||||
message: "Test message",
|
||||
payload: {
|
||||
lighthouseResult: {
|
||||
categories: {},
|
||||
audits: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(check.monitorId).to.equal("test");
|
||||
expect(check.status).to.be.true;
|
||||
expect(check.statusCode).to.equal(200);
|
||||
expect(check.responseTime).to.equal(100);
|
||||
expect(check.message).to.equal("Test message");
|
||||
expect(check.accessibility).to.equal(0);
|
||||
expect(check.bestPractices).to.equal(0);
|
||||
expect(check.performance).to.equal(0);
|
||||
expect(check.seo).to.equal(0);
|
||||
expect(check.audits).to.deep.equal({
|
||||
cls: 0,
|
||||
si: 0,
|
||||
fcp: 0,
|
||||
lcp: 0,
|
||||
tbt: 0,
|
||||
});
|
||||
});
|
||||
it("should build a check for hardware type", () => {
|
||||
const check = statusService.buildCheck({
|
||||
monitorId: "test",
|
||||
type: "hardware",
|
||||
status: true,
|
||||
responseTime: 100,
|
||||
code: 200,
|
||||
message: "Test message",
|
||||
payload: { cpu: "cpu", memory: "memory", disk: "disk", host: "host" },
|
||||
});
|
||||
expect(check.monitorId).to.equal("test");
|
||||
expect(check.status).to.be.true;
|
||||
expect(check.statusCode).to.equal(200);
|
||||
expect(check.responseTime).to.equal(100);
|
||||
expect(check.message).to.equal("Test message");
|
||||
expect(check.cpu).to.equal("cpu");
|
||||
expect(check.memory).to.equal("memory");
|
||||
expect(check.disk).to.equal("disk");
|
||||
expect(check.host).to.equal("host");
|
||||
});
|
||||
});
|
||||
describe("insertCheck", () => {
|
||||
it("should log an error if one is thrown", async () => {
|
||||
const testError = new Error("Test error");
|
||||
statusService.db.createCheck = sinon.stub().throws(testError);
|
||||
try {
|
||||
await statusService.insertCheck({ monitorId: "test" });
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal(testError.message);
|
||||
}
|
||||
expect(statusService.logger.error.calledOnce).to.be.true;
|
||||
});
|
||||
it("should insert a check into the database", async () => {
|
||||
await statusService.insertCheck({ monitorId: "test", type: "http" });
|
||||
expect(statusService.db.createCheck.calledOnce).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -197,7 +197,13 @@ const createMonitorBodyValidation = joi.object({
|
||||
url: joi.string().required(),
|
||||
isActive: joi.boolean(),
|
||||
interval: joi.number(),
|
||||
thresholds: joi.object().keys({
|
||||
usage_cpu: joi.number(),
|
||||
usage_memory: joi.number(),
|
||||
usage_disk: joi.number(),
|
||||
}),
|
||||
notifications: joi.array().items(joi.object()),
|
||||
secret: joi.string(),
|
||||
});
|
||||
|
||||
const editMonitorBodyValidation = joi.object({
|
||||
@@ -205,6 +211,7 @@ const editMonitorBodyValidation = joi.object({
|
||||
description: joi.string(),
|
||||
interval: joi.number(),
|
||||
notifications: joi.array().items(joi.object()),
|
||||
secret: joi.string(),
|
||||
});
|
||||
|
||||
const pauseMonitorParamValidation = joi.object({
|
||||
|
||||
@@ -12,6 +12,15 @@ icon: sign-posts-wrench
|
||||
|
||||
---
|
||||
|
||||
## Quickstart for users (remote server) <a href="#user-quickstart" id="user-quickstart"></a>
|
||||
|
||||
1. Download our [Docker compose file](https://github.com/bluewave-labs/bluewave-uptime/blob/develop/Docker/dist/docker-compose.yaml)
|
||||
2. Edit the `UPTIME_APP_API_BASE_URL` variable in the docker-compose file to point to your remote server.
|
||||
3. Run `docker compose up` to start the application
|
||||
4. Now the application is running at `http://<remote_server_ip>`
|
||||
|
||||
---
|
||||
|
||||
## Quickstart for developers <a href="#dev-quickstart" id="dev-quickstart"></a>
|
||||
|
||||
{% hint style="info" %}
|
||||
|
||||
@@ -26,10 +26,12 @@ Here you can setup the following: 
|
||||
|
||||
**Email settings:** Set your host email settings here. These settings are used for sending system emails.
|
||||
|
||||
Server settings: Several server settings can be done here. Alternatively, you can add a pagespeed API key to bypass Google's limitations (albeit they are generous about it)
|
||||
**Server settings:** Several server settings can be done here. Alternatively, you can add a pagespeed API key to bypass Google's limitations (albeit they are generous about it)
|
||||
|
||||
Select a number for jwt TTL and a unit for it (days or hours), the combined result
|
||||
would be in vercel/ms time format, e.g "99d", this will be sent to server to persist
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
\
|
||||
|
||||
Reference in New Issue
Block a user