Merge branch 'develop' into fix/be/refactor-auth-controller
This commit is contained in:
42 files changed
+1625
-428
No files matched your search
Generated
+19
-19
@@ -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",
|
||||
@@ -39,7 +39,7 @@
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "^5.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",
|
||||
@@ -3555,22 +3555,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-hooks": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
|
||||
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz",
|
||||
"integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
|
||||
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react-refresh": {
|
||||
"version": "0.4.13",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.13.tgz",
|
||||
"integrity": "sha512-f1EppwrpJRWmqDTyvAyomFVDYRtrS7iTEqv3nokETnMiMzs2SSTmKRTACce4O2p4jYyowiSMvpdwC/RLcMFhuQ==",
|
||||
"version": "0.4.14",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.14.tgz",
|
||||
"integrity": "sha512-aXvzCTK7ZBv1e7fahFuR3Z/fyQQSIQ711yPgYRj+Oj64tyTgO4iQIDmYXDBqvSWQ/FA4OSCsXOStlF+noU0/NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
|
||||
+3
-3
@@ -17,8 +17,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",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"prettier": "^3.3.3",
|
||||
"vite": "^5.2.0"
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ import { useSelector } from "react-redux";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { getAppSettings } from "./Features/Settings/settingsSlice";
|
||||
import { getAppSettings, updateAppSettings } from "./Features/Settings/settingsSlice";
|
||||
import { logger } from "./Utils/Logger"; // Import the logger
|
||||
import { networkService } from "./main";
|
||||
function App() {
|
||||
|
||||
@@ -12,6 +12,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
|
||||
aria-describedby={ariaDescribedBy}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
onClick={(e)=>e.stopPropagation()}
|
||||
>
|
||||
<Stack
|
||||
gap={theme.spacing(2)}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -181,6 +181,8 @@ function Sidebar() {
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={theme.spacing(4)}
|
||||
onClick={() => window.open("https://github.com/bluewave-labs/bluewave-uptime", "_blank", "noreferrer")}
|
||||
sx={{ cursor: "pointer" }}
|
||||
>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -87,48 +87,48 @@ class NetworkService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
* Check the endpoint resolution
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {Object} config.monitorURL - The monitor url to be sent in the request body.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async checkEndpointResolution(config) {
|
||||
const { authToken, monitorURL } = config;
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (monitorURL) params.append("monitorURL", monitorURL);
|
||||
|
||||
return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
* Check the endpoint resolution
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {Object} config.monitorURL - The monitor url to be sent in the request body.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async checkEndpointResolution(config) {
|
||||
const { authToken, monitorURL } = config;
|
||||
const params = new URLSearchParams();
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
* Gets monitors and summary of stats by TeamID
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {string} config.teamId - Team ID
|
||||
* @param {Array<string>} config.types - Array of monitor types
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async getMonitorsAndSummaryByTeamId(config) {
|
||||
const params = new URLSearchParams();
|
||||
if (monitorURL) params.append("monitorURL", monitorURL);
|
||||
|
||||
return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
* Gets monitors and summary of stats by TeamID
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {string} config.teamId - Team ID
|
||||
* @param {Array<string>} config.types - Array of monitor types
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async getMonitorsAndSummaryByTeamId(config) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (config.types) {
|
||||
config.types.forEach((type) => {
|
||||
|
||||
@@ -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 };
|
||||
@@ -128,10 +128,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
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
const PORT = 5000;
|
||||
|
||||
const connectDbAndRunServer = async (app, db) => {
|
||||
try {
|
||||
await db.connect();
|
||||
app.listen(PORT, () => {
|
||||
console.log(`server started on port:${PORT}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Failed to connect to DB");
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
export { connectDbAndRunServer };
|
||||
@@ -79,10 +79,10 @@ const registerUser = async (req, res, next) => {
|
||||
}
|
||||
|
||||
const newUser = await req.db.insertUser({ ...req.body }, req.file);
|
||||
|
||||
logger.info(successMessages.AUTH_CREATE_USER, {
|
||||
logger.info({
|
||||
message: successMessages.AUTH_CREATE_USER,
|
||||
service: SERVICE_NAME,
|
||||
userId: newUser._id,
|
||||
details: newUser._id,
|
||||
});
|
||||
|
||||
const userForToken = { ...newUser._doc };
|
||||
@@ -102,9 +102,11 @@ const registerUser = async (req, res, next) => {
|
||||
"Welcome to Uptime Monitor"
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error("Error sending welcome email", {
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
method: "registerUser",
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -53,9 +53,11 @@ const issueInvitation = async (req, res, next) => {
|
||||
"Welcome to Uptime Monitor"
|
||||
)
|
||||
.catch((error) => {
|
||||
logger.error("Error sending invite email", {
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
method: "issueInvitation",
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -312,14 +312,12 @@ const deleteMonitor = async (req, res, next) => {
|
||||
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await req.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
{
|
||||
method: "deleteMonitor",
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
}
|
||||
);
|
||||
logger.error({
|
||||
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteMonitor",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
return res.status(200).json({ success: true, msg: successMessages.MONITOR_DELETE });
|
||||
} catch (error) {
|
||||
@@ -352,12 +350,12 @@ const deleteAllMonitors = async (req, res, next) => {
|
||||
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await req.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
{
|
||||
method: "deleteAllMonitors",
|
||||
}
|
||||
);
|
||||
logger.error({
|
||||
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteAllMonitors",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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 },
|
||||
@@ -37,6 +37,10 @@ const HardwareCheckSchema = mongoose.Schema(
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
index: true,
|
||||
},
|
||||
cpu: {
|
||||
type: cpuSchema,
|
||||
default: () => ({}),
|
||||
@@ -46,7 +50,7 @@ const HardwareCheckSchema = mongoose.Schema(
|
||||
default: () => ({}),
|
||||
},
|
||||
disk: {
|
||||
type: [discSchema],
|
||||
type: [diskSchema],
|
||||
default: () => [],
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import mongoose from "mongoose";
|
||||
import Notification from "./Notification.js";
|
||||
|
||||
const MonitorSchema = mongoose.Schema(
|
||||
{
|
||||
@@ -48,6 +47,14 @@ 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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import mongoose from "mongoose";
|
||||
import logger from "../../utils/logger.js";
|
||||
const AuditSchema = mongoose.Schema({
|
||||
id: { type: String, required: true },
|
||||
title: { type: String, required: true },
|
||||
@@ -90,19 +91,22 @@ PageSpeedCheck.pre("save", async function (next) {
|
||||
const monitor = await mongoose.model("Monitor").findById(this.monitorId);
|
||||
if (monitor && monitor.status !== this.status) {
|
||||
if (monitor.status === true && this.status === false) {
|
||||
// TODO issue alert
|
||||
console.log("Monitor went down");
|
||||
logger.info({ message: "Monitor went down", monitorId: monitor._id });
|
||||
}
|
||||
|
||||
if (monitor.status === false && this.status === true) {
|
||||
// TODO issue alert
|
||||
console.log("Monitor went up");
|
||||
logger.info({ message: "Monitor went up", monitorId: monitor._id });
|
||||
}
|
||||
monitor.status = this.status;
|
||||
await monitor.save();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: "PageSpeedCheck",
|
||||
method: "pre-save",
|
||||
stack: error.stack,
|
||||
});
|
||||
} finally {
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import mongoose from "mongoose";
|
||||
import bcrypt from "bcrypt";
|
||||
import logger from "../../utils/logger.js";
|
||||
|
||||
const UserSchema = mongoose.Schema(
|
||||
{
|
||||
@@ -81,7 +82,9 @@ UserSchema.methods.comparePassword = async function (submittedPassword) {
|
||||
const User = mongoose.model("User", UserSchema);
|
||||
|
||||
User.init().then(() => {
|
||||
console.log("User model initialized");
|
||||
logger.info({
|
||||
message: "User model initialized",
|
||||
});
|
||||
});
|
||||
|
||||
export default mongoose.model("User", UserSchema);
|
||||
@@ -1,6 +1,8 @@
|
||||
import mongoose from "mongoose";
|
||||
import UserModel from "../models/User.js";
|
||||
import AppSettings from "../models/AppSettings.js";
|
||||
import logger from "../../utils/logger.js";
|
||||
const SERVICE_NAME = "MongoDB";
|
||||
|
||||
//****************************************
|
||||
// DB Connection
|
||||
@@ -17,14 +19,34 @@ const connect = async () => {
|
||||
appSettings = new AppSettings({});
|
||||
await appSettings.save();
|
||||
}
|
||||
|
||||
console.log("Connected to MongoDB");
|
||||
logger.info({ message: "Connected to MongoDB" });
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to MongoDB");
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "connect",
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
try {
|
||||
logger.info({ message: "Disconnecting from MongoDB" });
|
||||
await mongoose.disconnect();
|
||||
logger.info({ message: "Disconnected from MongoDB" });
|
||||
return;
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "disconnect",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkSuperadmin = async (req, res) => {
|
||||
try {
|
||||
const superAdmin = await UserModel.findOne({ role: "superadmin" });
|
||||
@@ -148,6 +170,7 @@ import { getAppSettings, updateAppSettings } from "./modules/settingsModule.js";
|
||||
|
||||
export default {
|
||||
connect,
|
||||
disconnect,
|
||||
insertUser,
|
||||
getUserByEmail,
|
||||
updateUser,
|
||||
|
||||
@@ -30,9 +30,11 @@ const createCheck = async (checkData) => {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
|
||||
if (!monitor) {
|
||||
logger.error("Monitor not found", {
|
||||
logger.error({
|
||||
message: "Monitor not found",
|
||||
service: SERVICE_NAME,
|
||||
monitorId,
|
||||
method: "createCheck",
|
||||
details: `monitor ID: ${monitorId}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -77,7 +79,11 @@ const getChecksCount = async (req) => {
|
||||
checksQuery.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
console.log("default");
|
||||
logger.warn({
|
||||
message: "invalid filter",
|
||||
service: SERVICE_NAME,
|
||||
method: "getChecksCount",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -110,7 +116,7 @@ const getChecks = async (req) => {
|
||||
if (dateRange !== undefined) {
|
||||
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
|
||||
}
|
||||
// Fitler checks by status
|
||||
// Filter checks by status
|
||||
if (filter !== undefined) {
|
||||
checksQuery.status = false;
|
||||
switch (filter) {
|
||||
@@ -122,7 +128,11 @@ const getChecks = async (req) => {
|
||||
checksQuery.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
console.log("default");
|
||||
logger.warn({
|
||||
message: "invalid filter",
|
||||
service: SERVICE_NAME,
|
||||
method: "getChecks",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -170,7 +180,11 @@ const getTeamChecks = async (req) => {
|
||||
checksQuery.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
console.log("default");
|
||||
logger.warn({
|
||||
message: "invalid filter",
|
||||
service: SERVICE_NAME,
|
||||
method: "getTeamChecks",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -248,9 +262,11 @@ const updateChecksTTL = async (teamId, ttl) => {
|
||||
try {
|
||||
await Check.collection.dropIndex("expiry_1");
|
||||
} catch (error) {
|
||||
logger.error("Failed to drop index", {
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "updateChecksTTL",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,6 @@ const deleteMaintenanceWindowByUserId = async (userId) => {
|
||||
};
|
||||
|
||||
const editMaintenanceWindowById = async (maintenanceWindowId, maintenanceWindowData) => {
|
||||
console.log(maintenanceWindowData);
|
||||
try {
|
||||
const editedMaintenanceWindow = MaintenanceWindow.findByIdAndUpdate(
|
||||
maintenanceWindowId,
|
||||
|
||||
+42
-57
@@ -17,9 +17,11 @@ import maintenanceWindowRouter from "./routes/maintenanceWindowRoute.js";
|
||||
import settingsRouter from "./routes/settingsRoute.js";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { connectDbAndRunServer } from "./configs/db.js";
|
||||
import queueRouter from "./routes/queueRoute.js";
|
||||
|
||||
//JobQueue service and dependencies
|
||||
import JobQueue from "./service/jobQueue.js";
|
||||
import { Queue, Worker } from "bullmq";
|
||||
|
||||
//Network service and dependencies
|
||||
import NetworkService from "./service/networkService.js";
|
||||
@@ -48,31 +50,11 @@ const __dirname = path.dirname(__filename);
|
||||
const openApiSpec = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, "openapi.json"), "utf8")
|
||||
);
|
||||
|
||||
const PORT = 5000;
|
||||
|
||||
// Need to wrap server setup in a function to handle async nature of JobQueue
|
||||
const startApp = async () => {
|
||||
// **************************
|
||||
// Here is where we can swap out DBs easily. Spin up a mongoDB instance and try it out.
|
||||
// Simply comment out the FakeDB and uncomment the MongoDB or vice versa.
|
||||
// We can easily swap between any type of data source as long as the methods are implemented
|
||||
//
|
||||
// FakeDB
|
||||
// const db = require("./db/FakeDb");
|
||||
//
|
||||
// MongoDB
|
||||
// const db = require("./db/MongoDB");
|
||||
//
|
||||
// **************************
|
||||
// const DB_TYPE = {
|
||||
// MongoDB: async () => (await import("./db/mongo/MongoDB.js")).default,
|
||||
// FakedDB: async () => (await import("./db/FakeDb.js")).default,
|
||||
// };
|
||||
|
||||
// const db = DB_TYPE[process.env.DB_TYPE]
|
||||
// ? DB_TYPE[process.env.DB_TYPE]()
|
||||
// : require("./db/FakeDb");
|
||||
|
||||
// const db = DB_TYPE.MongoDB();
|
||||
|
||||
const app = express();
|
||||
|
||||
// middlewares
|
||||
@@ -82,12 +64,8 @@ const startApp = async () => {
|
||||
);
|
||||
app.use(express.json());
|
||||
app.use(helmet());
|
||||
// **************************
|
||||
// Make DB accessible anywhere we have a Request object
|
||||
// By adding the DB to the request object, we can access it in any route
|
||||
// Thus we do not need to import it in every route file, and we can easily swap out DBs as there is only one place to change it
|
||||
// Same applies for JobQueue and emailService
|
||||
// **************************
|
||||
|
||||
// Add db, jobQueue, emailService, and settingsService to request object for easy access
|
||||
app.use((req, res, next) => {
|
||||
req.db = db;
|
||||
req.jobQueue = jobQueue;
|
||||
@@ -111,27 +89,15 @@ const startApp = async () => {
|
||||
//health check
|
||||
app.use("/api/v1/healthy", (req, res) => {
|
||||
try {
|
||||
logger.info("Checking Health of the server.");
|
||||
logger.info({ message: "Checking Health of the server." });
|
||||
return res.status(200).json({ message: "Healthy" });
|
||||
} catch (error) {
|
||||
logger.error(error.message);
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.use("/api/v1/mail", async (req, res) => {
|
||||
try {
|
||||
const id = await req.emailService.buildAndSendEmail(
|
||||
"welcomeEmailTemplate",
|
||||
{
|
||||
name: "Alex",
|
||||
},
|
||||
"ajhollid@gmail.com",
|
||||
"Welcome"
|
||||
);
|
||||
res.status(200).json({ success: true, msg: "Email sent", data: id });
|
||||
} catch (error) {
|
||||
logger.error(error.message);
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "healthCheck",
|
||||
stack: error.stack,
|
||||
});
|
||||
return res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
@@ -143,7 +109,10 @@ const startApp = async () => {
|
||||
app.use(handleErrors);
|
||||
|
||||
// Create services
|
||||
await connectDbAndRunServer(app, db);
|
||||
await db.connect();
|
||||
app.listen(PORT, () => {
|
||||
logger.info({ message: `server started on port:${PORT}` });
|
||||
});
|
||||
const settingsService = new SettingsService(AppSettings);
|
||||
|
||||
await settingsService.loadSettings();
|
||||
@@ -157,22 +126,32 @@ const startApp = async () => {
|
||||
logger
|
||||
);
|
||||
const networkService = new NetworkService(db, emailService, axios, ping, logger, http);
|
||||
const jobQueue = await JobQueue.createJobQueue(db, networkService, settingsService);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker
|
||||
);
|
||||
|
||||
const cleanup = async () => {
|
||||
if (cleaningUp) {
|
||||
console.log("Already cleaning up");
|
||||
logger.warn({ message: "Already cleaning up" });
|
||||
return;
|
||||
}
|
||||
cleaningUp = true;
|
||||
try {
|
||||
console.log("Shutting down gracefully");
|
||||
logger.info({ message: "shutting down gracefully" });
|
||||
await jobQueue.obliterate();
|
||||
console.log("Finished cleanup");
|
||||
await db.disconnect();
|
||||
logger.info({ message: "shut down gracefully" });
|
||||
} catch (error) {
|
||||
logger.error(errorMessages.JOB_QUEUE_DELETE_JOB, {
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
errorMsg: error.message,
|
||||
method: "cleanup",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
process.exit(0);
|
||||
@@ -183,5 +162,11 @@ const startApp = async () => {
|
||||
};
|
||||
|
||||
startApp().catch((error) => {
|
||||
console.log(error);
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "startApp",
|
||||
stack: error.stack,
|
||||
});
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -5,9 +5,11 @@ const handleErrors = (error, req, res, next) => {
|
||||
const status = error.status || 500;
|
||||
const message = error.message || errorMessages.FRIENDLY_ERROR;
|
||||
const service = error.service || errorMessages.UNKNOWN_SERVICE;
|
||||
logger.error(error.message, {
|
||||
logger.error({
|
||||
message: message,
|
||||
service: service,
|
||||
method: error.method,
|
||||
stack: error.stack,
|
||||
});
|
||||
res.status(status).json({ success: false, msg: message });
|
||||
};
|
||||
|
||||
@@ -10,8 +10,10 @@ const verifyOwnership = (Model, paramName) => {
|
||||
const doc = await Model.findById(documentId);
|
||||
//If the document is not found, return a 404 error
|
||||
if (!doc) {
|
||||
logger.error(errorMessages.VERIFY_OWNER_NOT_FOUND, {
|
||||
logger.error({
|
||||
message: errorMessages.VERIFY_OWNER_NOT_FOUND,
|
||||
service: SERVICE_NAME,
|
||||
method: "verifyOwnership",
|
||||
});
|
||||
const error = new Error(errorMessages.VERIFY_OWNER_NOT_FOUND);
|
||||
error.status = 404;
|
||||
|
||||
@@ -37,8 +37,12 @@ const verifySuperAdmin = (req, res, next) => {
|
||||
|
||||
jwt.verify(parsedToken, jwtSecret, (err, decoded) => {
|
||||
if (err) {
|
||||
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
|
||||
logger.error({
|
||||
message: err.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "verifySuperAdmin",
|
||||
stack: err.stack,
|
||||
details: errorMessages.INVALID_AUTH_TOKEN,
|
||||
});
|
||||
return res
|
||||
.status(401)
|
||||
@@ -46,8 +50,11 @@ const verifySuperAdmin = (req, res, next) => {
|
||||
}
|
||||
|
||||
if (decoded.role.includes("superadmin") === false) {
|
||||
logger.error(errorMessages.INVALID_AUTH_TOKEN, {
|
||||
logger.error({
|
||||
message: errorMessages.INVALID_AUTH_TOKEN,
|
||||
service: SERVICE_NAME,
|
||||
method: "verifySuperAdmin",
|
||||
stack: err.stack,
|
||||
});
|
||||
return res.status(401).json({ success: false, msg: errorMessages.UNAUTHORIZED });
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -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",
|
||||
|
||||
@@ -44,9 +44,11 @@ class EmailService {
|
||||
const templateContent = this.fs.readFileSync(templatePath, "utf8");
|
||||
return this.compile(templateContent);
|
||||
} catch (error) {
|
||||
this.logger.error("Error loading Email templates", {
|
||||
error,
|
||||
service: this.SERVICE_NAME,
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "loadTemplate",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -63,6 +65,7 @@ class EmailService {
|
||||
serverIsDownTemplate: this.loadTemplate("serverIsDown"),
|
||||
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
|
||||
passwordResetTemplate: this.loadTemplate("passwordReset"),
|
||||
thresholdViolatedTemplate: this.loadTemplate("thresholdViolated"),
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -102,9 +105,11 @@ class EmailService {
|
||||
const html = await this.mjml2html(mjml);
|
||||
return html.html;
|
||||
} catch (error) {
|
||||
this.logger.error("Error building Email HTML", {
|
||||
error,
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "buildAndSendEmail",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -118,9 +123,11 @@ class EmailService {
|
||||
});
|
||||
return info;
|
||||
} catch (error) {
|
||||
this.logger.error("Error sending Email", {
|
||||
error,
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "sendEmail",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
+60
-45
@@ -1,8 +1,5 @@
|
||||
import { Queue, Worker, Job } from "bullmq";
|
||||
const QUEUE_NAME = "monitors";
|
||||
|
||||
const JOBS_PER_WORKER = 5;
|
||||
import logger from "../utils/logger.js";
|
||||
import { errorMessages, successMessages } from "../utils/messages.js";
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
/**
|
||||
@@ -19,11 +16,13 @@ class JobQueue {
|
||||
* @param {SettingsService} settingsService - The settings service
|
||||
* @throws {Error}
|
||||
*/
|
||||
constructor(settingsService) {
|
||||
const { redisHost, redisPort } = settingsService.getSettings();
|
||||
constructor(settingsService, logger, Queue, Worker) {
|
||||
const settings = settingsService.getSettings() || {};
|
||||
|
||||
const { redisHost = "127.0.0.1", redisPort = 6379 } = settings;
|
||||
const connection = {
|
||||
host: redisHost || "127.0.0.1",
|
||||
port: redisPort || 6379,
|
||||
host: redisHost,
|
||||
port: redisPort,
|
||||
};
|
||||
this.connection = connection;
|
||||
this.queue = new Queue(QUEUE_NAME, {
|
||||
@@ -33,6 +32,8 @@ class JobQueue {
|
||||
this.db = null;
|
||||
this.networkService = null;
|
||||
this.settingsService = settingsService;
|
||||
this.logger = logger;
|
||||
this.Worker = Worker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,8 +43,15 @@ class JobQueue {
|
||||
* @returns {Promise<JobQueue>} - Returns a new JobQueue
|
||||
*
|
||||
*/
|
||||
static async createJobQueue(db, networkService, settingsService) {
|
||||
const queue = new JobQueue(settingsService);
|
||||
static async createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
Queue,
|
||||
Worker
|
||||
) {
|
||||
const queue = new JobQueue(settingsService, logger, Queue, Worker);
|
||||
try {
|
||||
queue.db = db;
|
||||
queue.networkService = networkService;
|
||||
@@ -69,7 +77,7 @@ class JobQueue {
|
||||
* @returns {Worker} The newly created worker
|
||||
*/
|
||||
createWorker() {
|
||||
const worker = new Worker(
|
||||
const worker = new this.Worker(
|
||||
QUEUE_NAME,
|
||||
async (job) => {
|
||||
try {
|
||||
@@ -96,20 +104,22 @@ class JobQueue {
|
||||
}
|
||||
return acc;
|
||||
}, false);
|
||||
|
||||
if (!maintenanceWindowActive) {
|
||||
await this.networkService.getStatus(job);
|
||||
} else {
|
||||
logger.info(`Monitor ${monitorId} is in maintenance window`, {
|
||||
this.logger.info({
|
||||
message: `Monitor ${monitorId} is in maintenance window`,
|
||||
service: SERVICE_NAME,
|
||||
monitorId,
|
||||
method: "createWorker",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error processing job ${job.id}: ${error.message}`, {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
jobId: job.id,
|
||||
error: error,
|
||||
method: "createWorker",
|
||||
details: `Error processing job ${job.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -169,11 +179,9 @@ class JobQueue {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (workerStats.load > JOBS_PER_WORKER) {
|
||||
// Find out how many more jobs we have than current workers can handle
|
||||
const excessJobs = workerStats.jobs.length - this.workers.length * JOBS_PER_WORKER;
|
||||
|
||||
// Divide by jobs/worker to find out how many workers to add
|
||||
const workersToAdd = Math.ceil(excessJobs / JOBS_PER_WORKER);
|
||||
for (let i = 0; i < workersToAdd; i++) {
|
||||
@@ -188,19 +196,19 @@ class JobQueue {
|
||||
const workerCapacity = this.workers.length * JOBS_PER_WORKER;
|
||||
const excessCapacity = workerCapacity - workerStats.jobs.length;
|
||||
// Calculate how many workers to remove
|
||||
const workersToRemove = Math.floor(excessCapacity / JOBS_PER_WORKER);
|
||||
if (this.workers.length > 5) {
|
||||
for (let i = 0; i < workersToRemove; i++) {
|
||||
const worker = this.workers.pop();
|
||||
try {
|
||||
await worker.close();
|
||||
} catch (error) {
|
||||
// Catch the error instead of throwing it
|
||||
logger.error(errorMessages.JOB_QUEUE_WORKER_CLOSE, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
}
|
||||
}
|
||||
let workersToRemove = Math.floor(excessCapacity / JOBS_PER_WORKER); // Make sure there are always at least 5
|
||||
while (workersToRemove > 0 && this.workers.length > 5) {
|
||||
const worker = this.workers.pop();
|
||||
workersToRemove--;
|
||||
await worker.close().catch((error) => {
|
||||
// Catch the error instead of throwing it
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "scaleWorkers",
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -252,7 +260,7 @@ class JobQueue {
|
||||
*/
|
||||
async addJob(jobName, payload) {
|
||||
try {
|
||||
console.log("Adding job", payload?.url ?? "No URL");
|
||||
this.logger.info({ message: `Adding job ${payload?.url ?? "No URL"}` });
|
||||
// Execute job immediately
|
||||
await this.queue.add(jobName, payload);
|
||||
await this.queue.add(jobName, payload, {
|
||||
@@ -278,20 +286,24 @@ class JobQueue {
|
||||
*/
|
||||
async deleteJob(monitor) {
|
||||
try {
|
||||
const deleted = await this.queue.removeRepeatable(monitor._id, {
|
||||
const wasDeleted = await this.queue.removeRepeatable(monitor._id, {
|
||||
every: monitor.interval,
|
||||
});
|
||||
if (deleted) {
|
||||
logger.info(successMessages.JOB_QUEUE_DELETE_JOB, {
|
||||
if (wasDeleted === true) {
|
||||
this.logger.info({
|
||||
message: successMessages.JOB_QUEUE_DELETE_JOB,
|
||||
service: SERVICE_NAME,
|
||||
jobId: monitor.id,
|
||||
method: "deleteJob",
|
||||
details: `Deleted job ${monitor._id}`,
|
||||
});
|
||||
const workerStats = await this.getWorkerStats();
|
||||
await this.scaleWorkers(workerStats);
|
||||
} else {
|
||||
logger.error(errorMessages.JOB_QUEUE_DELETE_JOB, {
|
||||
this.logger.error({
|
||||
message: errorMessages.JOB_QUEUE_DELETE_JOB,
|
||||
service: SERVICE_NAME,
|
||||
jobId: monitor.id,
|
||||
method: "deleteJob",
|
||||
details: `Failed to delete job ${monitor._id}`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -311,11 +323,16 @@ class JobQueue {
|
||||
delayed: await this.queue.getDelayedCount(),
|
||||
repeatableJobs: (await this.queue.getRepeatableJobs()).length,
|
||||
};
|
||||
this.logger.info({
|
||||
message: metrics,
|
||||
});
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
logger.error("Failed to retrieve job queue metrics", {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
errorMsg: error.message,
|
||||
method: "getMetrics",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -327,7 +344,7 @@ class JobQueue {
|
||||
async obliterate() {
|
||||
try {
|
||||
let metrics = await this.getMetrics();
|
||||
console.log(metrics);
|
||||
this.logger.info({ message: metrics });
|
||||
await this.queue.pause();
|
||||
const jobs = await this.getJobs();
|
||||
|
||||
@@ -343,10 +360,8 @@ class JobQueue {
|
||||
|
||||
await this.queue.obliterate();
|
||||
metrics = await this.getMetrics();
|
||||
console.log(metrics);
|
||||
logger.info(successMessages.JOB_QUEUE_OBLITERATE, {
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
this.logger.info({ message: metrics });
|
||||
this.logger.info({ message: successMessages.JOB_QUEUE_OBLITERATE });
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
|
||||
@@ -34,32 +34,34 @@ class NetworkService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the notification process for a monitor.
|
||||
* Handles notifications for a given monitor configuration.
|
||||
*
|
||||
* @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) {
|
||||
* @param {Object} config - The configuration object for the notification.
|
||||
* @param {Object} config.monitor - The monitor object containing the monitor ID.
|
||||
* @param {string} config.template - The email template to be used.
|
||||
* @param {Object} config.context - The context for the email template.
|
||||
* @param {string} config.subject - The subject of the email.
|
||||
*/
|
||||
async handleNotification(config) {
|
||||
try {
|
||||
let template = isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
let status = isAlive === true ? "up" : "down";
|
||||
|
||||
const notifications = await this.db.getNotificationsByMonitorId(monitor._id);
|
||||
const notifications = await this.db.getNotificationsByMonitorId(config.monitor._id);
|
||||
for (const notification of notifications) {
|
||||
if (notification.type === "email") {
|
||||
await this.emailService.buildAndSendEmail(
|
||||
template,
|
||||
{ monitorName: monitor.name, monitorUrl: monitor.url },
|
||||
config.template,
|
||||
config.context,
|
||||
notification.address,
|
||||
`Monitor ${monitor.name} is ${status}`
|
||||
config.subject
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error.message, {
|
||||
method: "handleNotification",
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
monitorId: monitor._id,
|
||||
method: "handleNotification",
|
||||
details: `notification error for monitor: ${monitor._id}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -69,9 +71,10 @@ class NetworkService {
|
||||
*
|
||||
* @param {Object} job - The job object containing job details.
|
||||
* @param {boolean} isAlive - The status of the monitor (true if up, false if down).
|
||||
* @param {Object} [hardwareData] - The hardware data for the monitor.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleStatusUpdate(job, isAlive) {
|
||||
async handleStatusUpdate(job, isAlive, hardwareData) {
|
||||
let monitor;
|
||||
const { _id } = job.data;
|
||||
|
||||
@@ -79,37 +82,96 @@ class NetworkService {
|
||||
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 === null || monitor === undefined) {
|
||||
this.logger.error(`Null Monitor: ${_id}`, {
|
||||
method: "handleStatusUpdate",
|
||||
service: this.SERVICE_NAME,
|
||||
jobId: job.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
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);
|
||||
const config = {
|
||||
monitor: monitor,
|
||||
template: isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate",
|
||||
context: { monitorName: monitor.name, monitorUrl: monitor.url },
|
||||
subject: `Monitor ${monitor.name} is ${isAlive === true ? "up" : "down"}`,
|
||||
};
|
||||
this.handleNotification(config);
|
||||
}
|
||||
}
|
||||
if (monitor.type === this.TYPE_HARDWARE) {
|
||||
this.handleHardwareUpdate(monitor, hardwareData);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error.message, {
|
||||
method: "handleStatusUpdate",
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
jobId: job.id,
|
||||
method: "handleStatusUpdate",
|
||||
stack: error.stack,
|
||||
details: `status update error for monitor: ${_id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleHardwareUpdate(monitor, hardwareData) {
|
||||
//Get Thresholds
|
||||
const thresholds = monitor?.thresholds;
|
||||
if (thresholds === undefined) {
|
||||
return;
|
||||
}
|
||||
//Get Values
|
||||
const cpuUsage = hardwareData?.cpu?.usage_percent;
|
||||
const memoryUsage = hardwareData?.memory?.usage_percent;
|
||||
const diskUsage = hardwareData?.disk[0]?.usage_percent;
|
||||
|
||||
// Return early if no values
|
||||
if (cpuUsage === undefined && memoryUsage === undefined && diskUsage === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return early if all values are below thresholds
|
||||
if (
|
||||
cpuUsage < thresholds.usage_cpu &&
|
||||
memoryUsage < thresholds.usage_memory &&
|
||||
diskUsage < thresholds.usage_disk
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let context = {
|
||||
message: "Threshold(s) exceeded:",
|
||||
};
|
||||
if (cpuUsage > thresholds.usage_cpu) {
|
||||
context.cpu = `CPU USAGE: ${cpuUsage * 100}% > ${thresholds.usage_cpu * 100}%`;
|
||||
}
|
||||
|
||||
if (memoryUsage > thresholds.usage_memory) {
|
||||
context.memory = `MEMORY USAGE: ${memoryUsage * 100}% > ${thresholds.usage_memory * 100}%`;
|
||||
}
|
||||
|
||||
if (diskUsage > thresholds.usage_disk) {
|
||||
context.disk = `DISK USAGE: ${diskUsage * 100}% > ${thresholds.usage_disk * 100}%`;
|
||||
}
|
||||
|
||||
const config = {
|
||||
monitor: monitor,
|
||||
template: "thresholdViolatedTemplate",
|
||||
context: context,
|
||||
subject: `Threshold Violated for ${monitor.name}`,
|
||||
};
|
||||
this.handleNotification(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures the response time of an asynchronous operation.
|
||||
* @param {Function} operation - An asynchronous operation to measure.
|
||||
@@ -308,13 +370,13 @@ class NetworkService {
|
||||
frequency: 266,
|
||||
temperature: null,
|
||||
free_percent: null,
|
||||
usage_percent: null,
|
||||
usage_percent: 1,
|
||||
},
|
||||
memory: {
|
||||
total_bytes: 4,
|
||||
available_bytes: 4,
|
||||
used_bytes: 2,
|
||||
usage_percent: 0.5,
|
||||
usage_percent: 1,
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
@@ -322,7 +384,7 @@ class NetworkService {
|
||||
write_speed_bytes: 3,
|
||||
total_bytes: 10,
|
||||
free_bytes: 2,
|
||||
usage_percent: 0.8,
|
||||
usage_percent: 1,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
@@ -369,7 +431,7 @@ class NetworkService {
|
||||
};
|
||||
this.logAndStoreCheck(nullData, this.db.createHardwareCheck);
|
||||
} finally {
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
this.handleStatusUpdate(job, isAlive, hardwareData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,11 +453,12 @@ class NetworkService {
|
||||
case this.TYPE_HARDWARE:
|
||||
return await this.handleHardware(job);
|
||||
default:
|
||||
this.logger.error(`Unsupported type: ${job.data.type}`, {
|
||||
this.logger.error({
|
||||
message: `Unsupported type: ${job.data.type}`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "getStatus",
|
||||
jobId: job.id,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -417,11 +480,12 @@ class NetworkService {
|
||||
}
|
||||
throw new Error();
|
||||
} catch (error) {
|
||||
this.logger.error(`Error writing check for ${data.monitorId}`, {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "logAndStoreCheck",
|
||||
monitorId: data.monitorId,
|
||||
error: error,
|
||||
details: `Error writing check for ${data.monitorId}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -182,7 +182,7 @@ describe("Auth Controller - registerUser", () => {
|
||||
req.emailService.buildAndSendEmail.rejects(new Error("emailService error"));
|
||||
await registerUser(req, res, next);
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[1].error).to.equal("emailService error");
|
||||
expect(logger.error.firstCall.args[0].message).to.equal("emailService error");
|
||||
});
|
||||
it("should return a success message and data if all operations are successful", async () => {
|
||||
const user = { _id: "123" };
|
||||
|
||||
@@ -534,14 +534,9 @@ describe("Monitor Controller - deleteMonitor", () => {
|
||||
req.jobQueue.deleteJob.rejects(error);
|
||||
await deleteMonitor(req, res, next);
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[0]).to.equal(
|
||||
expect(logger.error.firstCall.args[0].message).to.equal(
|
||||
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`
|
||||
);
|
||||
expect(logger.error.firstCall.args[1]).to.deep.equal({
|
||||
method: "deleteMonitor",
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
it("should log an error if deleteChecks throws an error", async () => {
|
||||
const error = new Error("Checks error");
|
||||
@@ -550,14 +545,9 @@ describe("Monitor Controller - deleteMonitor", () => {
|
||||
req.db.deleteChecks.rejects(error);
|
||||
await deleteMonitor(req, res, next);
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[0]).to.equal(
|
||||
expect(logger.error.firstCall.args[0].message).to.equal(
|
||||
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`
|
||||
);
|
||||
expect(logger.error.firstCall.args[1]).to.deep.equal({
|
||||
method: "deleteMonitor",
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
it("should log an error if deletePageSpeedChecksByMonitorId throws an error", async () => {
|
||||
const error = new Error("PageSpeed error");
|
||||
@@ -566,14 +556,9 @@ describe("Monitor Controller - deleteMonitor", () => {
|
||||
req.db.deletePageSpeedChecksByMonitorId.rejects(error);
|
||||
await deleteMonitor(req, res, next);
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[0]).to.equal(
|
||||
expect(logger.error.firstCall.args[0].message).to.equal(
|
||||
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`
|
||||
);
|
||||
expect(logger.error.firstCall.args[1]).to.deep.equal({
|
||||
method: "deleteMonitor",
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
it("should log an error if deleteNotificationsByMonitorId throws an error", async () => {
|
||||
const error = new Error("Notifications error");
|
||||
@@ -582,14 +567,9 @@ describe("Monitor Controller - deleteMonitor", () => {
|
||||
req.db.deleteNotificationsByMonitorId.rejects(error);
|
||||
await deleteMonitor(req, res, next);
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[0]).to.equal(
|
||||
expect(logger.error.firstCall.args[0].message).to.equal(
|
||||
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`
|
||||
);
|
||||
expect(logger.error.firstCall.args[1]).to.deep.equal({
|
||||
method: "deleteMonitor",
|
||||
service: SERVICE_NAME,
|
||||
error: error.message,
|
||||
});
|
||||
});
|
||||
it("should return success message if all operations succeed", async () => {
|
||||
const monitor = { name: "test_monitor", _id: "123" };
|
||||
@@ -664,18 +644,7 @@ describe("Monitor Controller - deleteAllMonitors", () => {
|
||||
expect(next.firstCall.args[0]).to.be.an("error");
|
||||
expect(next.firstCall.args[0].message).to.equal("DB error");
|
||||
});
|
||||
it("should log an error if deleteJob throws an error", async () => {
|
||||
const monitors = [{ name: "test_monitor", _id: "123" }];
|
||||
req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
|
||||
req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 });
|
||||
const error = new Error("Job error");
|
||||
req.jobQueue.deleteJob.rejects(error);
|
||||
await deleteAllMonitors(req, res, next);
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[0]).to.deep.equal(
|
||||
`Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
|
||||
);
|
||||
});
|
||||
|
||||
it("should log an error if deleteChecks throws an error", async () => {
|
||||
const monitors = [{ name: "test_monitor", _id: "123" }];
|
||||
req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
|
||||
@@ -684,7 +653,7 @@ describe("Monitor Controller - deleteAllMonitors", () => {
|
||||
req.db.deleteChecks.rejects(error);
|
||||
await deleteAllMonitors(req, res, next);
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[0]).to.deep.equal(
|
||||
expect(logger.error.firstCall.args[0].message).to.equal(
|
||||
`Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
|
||||
);
|
||||
});
|
||||
@@ -696,7 +665,7 @@ describe("Monitor Controller - deleteAllMonitors", () => {
|
||||
req.db.deletePageSpeedChecksByMonitorId.rejects(error);
|
||||
await deleteAllMonitors(req, res, next);
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[0]).to.deep.equal(
|
||||
expect(logger.error.firstCall.args[0].message).to.equal(
|
||||
`Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
|
||||
);
|
||||
});
|
||||
@@ -708,7 +677,7 @@ describe("Monitor Controller - deleteAllMonitors", () => {
|
||||
req.db.deleteNotificationsByMonitorId.rejects(error);
|
||||
await deleteAllMonitors(req, res, next);
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[0]).to.deep.equal(
|
||||
expect(logger.error.firstCall.args[0].message).to.equal(
|
||||
`Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -93,14 +93,7 @@ describe("EmailService - Constructor", () => {
|
||||
loggerMock
|
||||
);
|
||||
expect(loggerMock.error.called).to.be.true;
|
||||
const errorCalls = loggerMock.error.getCalls();
|
||||
const errorCall = errorCalls.find(
|
||||
(call) => call.args[0] === "Error loading Email templates"
|
||||
);
|
||||
expect(errorCall).to.not.be.undefined;
|
||||
expect(emailService.settingsService).to.equal(settingsServiceMock);
|
||||
expect(emailService.templateLookup.welcomeEmailTemplate).to.be.undefined;
|
||||
expect(emailService.templateLookup.employeeActivationTemplate).to.be.undefined;
|
||||
expect(loggerMock.error.firstCall.args[0].message).to.equal("File read error");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -182,7 +175,7 @@ describe("EmailService - buildAndSendEmail", () => {
|
||||
"Welcome"
|
||||
);
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.getCall(0).args[0]).to.equal("Error building Email HTML");
|
||||
expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error");
|
||||
});
|
||||
|
||||
it("should log error if sending email fails", async () => {
|
||||
@@ -194,7 +187,7 @@ describe("EmailService - buildAndSendEmail", () => {
|
||||
"Welcome"
|
||||
);
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.getCall(0).args[0]).to.equal("Error sending Email");
|
||||
expect(loggerMock.error.getCall(0).args[0].message).to.equal("SMTP error");
|
||||
});
|
||||
|
||||
it("should log error if both building HTML and sending email fail", async () => {
|
||||
@@ -210,8 +203,8 @@ describe("EmailService - buildAndSendEmail", () => {
|
||||
|
||||
expect(messageId).to.be.undefined;
|
||||
expect(loggerMock.error.calledTwice).to.be.true;
|
||||
expect(loggerMock.error.getCall(0).args[0]).to.equal("Error building Email HTML");
|
||||
expect(loggerMock.error.getCall(1).args[0]).to.equal("Error sending Email");
|
||||
expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error");
|
||||
expect(loggerMock.error.getCall(1).args[0].message).to.equal("SMTP error");
|
||||
});
|
||||
|
||||
it("should log an error if buildHtml fails", async () => {});
|
||||
|
||||
@@ -0,0 +1,741 @@
|
||||
import sinon from "sinon";
|
||||
import JobQueue from "../../service/jobQueue.js";
|
||||
import { log } from "console";
|
||||
|
||||
class QueueStub {
|
||||
constructor(queueName, options) {
|
||||
this.queueName = queueName;
|
||||
this.options = options;
|
||||
this.workers = [];
|
||||
this.jobs = [];
|
||||
}
|
||||
|
||||
// Add any methods that are expected to be called on the Queue instance
|
||||
add(job) {
|
||||
this.jobs.push(job);
|
||||
}
|
||||
|
||||
removeRepeatable(id) {
|
||||
const removedJob = this.jobs.find((job) => job.data._id === id);
|
||||
this.jobs = this.jobs.filter((job) => job.data._id !== id);
|
||||
if (removedJob) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getRepeatableJobs() {
|
||||
return this.jobs;
|
||||
}
|
||||
async getJobs() {
|
||||
return this.jobs;
|
||||
}
|
||||
}
|
||||
|
||||
class WorkerStub {
|
||||
constructor(QUEUE_NAME, workerTask) {
|
||||
this.queueName = QUEUE_NAME;
|
||||
this.workerTask = async () => workerTask({ data: { _id: 1 } });
|
||||
}
|
||||
|
||||
async close() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
describe("JobQueue", () => {
|
||||
let settingsService, logger, db, networkService;
|
||||
|
||||
beforeEach(() => {
|
||||
settingsService = { getSettings: sinon.stub() };
|
||||
logger = { error: sinon.stub(), info: sinon.stub() };
|
||||
db = {
|
||||
getAllMonitors: sinon.stub().returns([]),
|
||||
getMaintenanceWindowsByMonitorId: sinon.stub().returns([]),
|
||||
};
|
||||
networkService = { getStatus: sinon.stub() };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
describe("createJobQueue", () => {
|
||||
it("should create a new JobQueue and add jobs for active monitors", async () => {
|
||||
db.getAllMonitors.returns([
|
||||
{ id: 1, isActive: true },
|
||||
{ id: 2, isActive: true },
|
||||
]);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
// There should be double the jobs, as one is meant to be instantly executed
|
||||
// And one is meant to be enqueued
|
||||
expect(jobQueue.queue.jobs.length).to.equal(4);
|
||||
});
|
||||
|
||||
it("should reject with an error if an error occurs", async () => {
|
||||
db.getAllMonitors.throws("Error");
|
||||
try {
|
||||
await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("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";
|
||||
error.method = "otherMethod";
|
||||
db.getAllMonitors.throws(error);
|
||||
|
||||
try {
|
||||
await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("otherService");
|
||||
expect(error.method).to.equal("otherMethod");
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("Constructor", () => {
|
||||
it("should construct a new JobQueue with default port and host if not provided", () => {
|
||||
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", () => {
|
||||
settingsService.getSettings.returns({ redisHost: "localhost", redisPort: 1234 });
|
||||
const jobQueue = new JobQueue(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 () => {
|
||||
db.getMaintenanceWindowsByMonitorId.throws("Error");
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const worker = jobQueue.createWorker();
|
||||
await worker.workerTask();
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
});
|
||||
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 () => {
|
||||
db.getMaintenanceWindowsByMonitorId.returns([
|
||||
{
|
||||
active: true,
|
||||
start: new Date(Date.now() - 1000).toISOString(),
|
||||
end: new Date(Date.now() + 1000).toISOString(),
|
||||
repeat: 0,
|
||||
},
|
||||
]);
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const worker = jobQueue.createWorker();
|
||||
await worker.workerTask();
|
||||
expect(networkService.getStatus.calledOnce).to.be.false;
|
||||
});
|
||||
it("worker should handle a maintenanceWindow that is active, has a repeat, but is not in maintenance zone", async () => {
|
||||
db.getMaintenanceWindowsByMonitorId.returns([
|
||||
{
|
||||
active: true,
|
||||
start: new Date(Date.now() - 10000).toISOString(),
|
||||
end: new Date(Date.now() + 5000).toISOString(),
|
||||
repeat: 10000,
|
||||
},
|
||||
]);
|
||||
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;
|
||||
});
|
||||
});
|
||||
describe("getWorkerStats", () => {
|
||||
it("should throw an error if getRepeatable Jobs fails", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.getRepeatableJobs = async () => {
|
||||
throw new Error("Error");
|
||||
};
|
||||
try {
|
||||
const stats = await jobQueue.getWorkerStats();
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("JobQueue");
|
||||
expect(error.method).to.equal("getWorkerStats");
|
||||
}
|
||||
});
|
||||
it("should throw an error if getRepeatable Jobs fails but respect existing error data", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.queue.getRepeatableJobs = async () => {
|
||||
const error = new Error("Existing Error");
|
||||
error.service = "otherService";
|
||||
error.method = "otherMethod";
|
||||
throw error;
|
||||
};
|
||||
try {
|
||||
await jobQueue.getWorkerStats();
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("otherService");
|
||||
expect(error.method).to.equal("otherMethod");
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("scaleWorkers", () => {
|
||||
it("should scale workers to 5 if no workers", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
expect(jobQueue.workers.length).to.equal(5);
|
||||
});
|
||||
it("should scale workers up", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
jobQueue.scaleWorkers({
|
||||
load: 100,
|
||||
jobs: Array.from({ length: 100 }, (_, i) => i + 1),
|
||||
});
|
||||
expect(jobQueue.workers.length).to.equal(20);
|
||||
});
|
||||
it("should scale workers down, even with error of worker.close fails", async () => {
|
||||
WorkerStub.prototype.close = async () => {
|
||||
throw new Error("Error");
|
||||
};
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
await jobQueue.scaleWorkers({
|
||||
load: 100,
|
||||
jobs: Array.from({ length: 100 }, (_, i) => i + 1),
|
||||
});
|
||||
|
||||
const res = await jobQueue.scaleWorkers({
|
||||
load: 0,
|
||||
jobs: [],
|
||||
});
|
||||
expect(jobQueue.workers.length).to.equal(5);
|
||||
});
|
||||
it("should scale workers down", async () => {
|
||||
WorkerStub.prototype.close = async () => {
|
||||
return true;
|
||||
};
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
await jobQueue.scaleWorkers({
|
||||
load: 40,
|
||||
jobs: Array.from({ length: 40 }, (_, i) => i + 1),
|
||||
});
|
||||
|
||||
const res = await jobQueue.scaleWorkers({
|
||||
load: 0,
|
||||
jobs: [],
|
||||
});
|
||||
expect(jobQueue.workers.length).to.equal(5);
|
||||
});
|
||||
it("should return false if scaling doesn't happen", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const res = await jobQueue.scaleWorkers({ load: 5 });
|
||||
expect(jobQueue.workers.length).to.equal(5);
|
||||
expect(res).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("getJobs", async () => {
|
||||
it("should return jobs", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
const jobs = await jobQueue.getJobs();
|
||||
expect(jobs.length).to.equal(0);
|
||||
});
|
||||
it("should throw an error if getRepeatableJobs fails", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
try {
|
||||
jobQueue.queue.getRepeatableJobs = async () => {
|
||||
throw new Error("error");
|
||||
};
|
||||
|
||||
await jobQueue.getJobs(true);
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("JobQueue");
|
||||
expect(error.method).to.equal("getJobs");
|
||||
}
|
||||
});
|
||||
it("should throw an error if getRepeatableJobs fails but respect existing error data", async () => {
|
||||
const jobQueue = await JobQueue.createJobQueue(
|
||||
db,
|
||||
networkService,
|
||||
settingsService,
|
||||
logger,
|
||||
QueueStub,
|
||||
WorkerStub
|
||||
);
|
||||
try {
|
||||
jobQueue.queue.getRepeatableJobs = async () => {
|
||||
const error = new Error("Existing error");
|
||||
error.service = "otherService";
|
||||
error.method = "otherMethod";
|
||||
throw error;
|
||||
};
|
||||
|
||||
await jobQueue.getJobs(true);
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("otherService");
|
||||
expect(error.method).to.equal("otherMethod");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getJobStats", async () => {
|
||||
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" }];
|
||||
};
|
||||
const jobStats = await jobQueue.getJobStats();
|
||||
expect(jobStats).to.deep.equal({
|
||||
jobs: [{ url: "test", state: "completed" }],
|
||||
workers: 5,
|
||||
});
|
||||
});
|
||||
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 [
|
||||
{
|
||||
data: { url: "test" },
|
||||
getState: async () => {
|
||||
throw new Error("Mapping Error");
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
try {
|
||||
await jobQueue.getJobStats();
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("Mapping Error");
|
||||
expect(error.service).to.equal("JobQueue");
|
||||
expect(error.method).to.equal("getJobStats");
|
||||
}
|
||||
});
|
||||
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 [
|
||||
{
|
||||
data: { url: "test" },
|
||||
getState: async () => {
|
||||
const error = new Error("Mapping Error");
|
||||
error.service = "otherService";
|
||||
error.method = "otherMethod";
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
try {
|
||||
await jobQueue.getJobStats();
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("Mapping Error");
|
||||
expect(error.service).to.equal("otherService");
|
||||
expect(error.method).to.equal("otherMethod");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("addJob", async () => {
|
||||
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");
|
||||
};
|
||||
try {
|
||||
await jobQueue.addJob("test", { url: "test" });
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("Error adding job");
|
||||
expect(error.service).to.equal("JobQueue");
|
||||
expect(error.method).to.equal("addJob");
|
||||
}
|
||||
});
|
||||
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";
|
||||
error.method = "otherMethod";
|
||||
throw error;
|
||||
};
|
||||
try {
|
||||
await jobQueue.addJob("test", { url: "test" });
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("Error adding job");
|
||||
expect(error.service).to.equal("otherService");
|
||||
expect(error.method).to.equal("otherMethod");
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("deleteJob", async () => {
|
||||
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 };
|
||||
const job = { data: monitor };
|
||||
jobQueue.queue.jobs = [job];
|
||||
await jobQueue.deleteJob(monitor);
|
||||
// expect(jobQueue.queue.jobs.length).to.equal(0);
|
||||
// expect(logger.info.calledOnce).to.be.true;
|
||||
// expect(jobQueue.getWorkerStats.calledOnce).to.be.true;
|
||||
// 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 };
|
||||
const job = { data: monitor };
|
||||
jobQueue.queue.jobs = [job];
|
||||
await jobQueue.deleteJob({ id_: 2 });
|
||||
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;
|
||||
};
|
||||
|
||||
try {
|
||||
await jobQueue.deleteJob({ _id: 1 });
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("removeRepeatable error");
|
||||
expect(error.service).to.equal("JobQueue");
|
||||
expect(error.method).to.equal("deleteJob");
|
||||
}
|
||||
});
|
||||
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";
|
||||
error.method = "otherMethod";
|
||||
throw error;
|
||||
};
|
||||
|
||||
try {
|
||||
await jobQueue.deleteJob({ _id: 1 });
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("removeRepeatable error");
|
||||
expect(error.service).to.equal("otherService");
|
||||
expect(error.method).to.equal("otherMethod");
|
||||
}
|
||||
});
|
||||
});
|
||||
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;
|
||||
jobQueue.queue.getFailedCount = async () => 4;
|
||||
jobQueue.queue.getDelayedCount = async () => 5;
|
||||
jobQueue.queue.getRepeatableJobs = async () => [1, 2, 3];
|
||||
const metrics = await jobQueue.getMetrics();
|
||||
expect(metrics).to.deep.equal({
|
||||
waiting: 1,
|
||||
active: 2,
|
||||
completed: 3,
|
||||
failed: 4,
|
||||
delayed: 5,
|
||||
repeatableJobs: 3,
|
||||
});
|
||||
});
|
||||
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");
|
||||
};
|
||||
await jobQueue.getMetrics();
|
||||
expect(logger.error.calledOnce).to.be.true;
|
||||
expect(logger.error.firstCall.args[0].message).to.equal("Error");
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
jobQueue.queue.remove = async () => true;
|
||||
jobQueue.queue.obliterate = async () => true;
|
||||
const obliteration = await jobQueue.obliterate();
|
||||
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");
|
||||
};
|
||||
|
||||
try {
|
||||
await jobQueue.obliterate();
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("JobQueue");
|
||||
expect(error.method).to.equal("obliterate");
|
||||
}
|
||||
});
|
||||
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";
|
||||
error.method = "otherMethod";
|
||||
throw error;
|
||||
};
|
||||
|
||||
try {
|
||||
await jobQueue.obliterate();
|
||||
} catch (error) {
|
||||
expect(error.service).to.equal("otherService");
|
||||
expect(error.method).to.equal("otherMethod");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -105,13 +105,7 @@ describe("NetworkService - handleNotification", () => {
|
||||
await networkService.handleNotification(monitor, true);
|
||||
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(
|
||||
loggerMock.error.calledWith("Database error", {
|
||||
method: "handleNotification",
|
||||
service: networkService.SERVICE_NAME,
|
||||
monitorId: monitor._id,
|
||||
})
|
||||
).to.be.true;
|
||||
expect(loggerMock.error.firstCall.args[0].message).to.equal("Database error");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -160,13 +154,13 @@ describe("NetworkService - handleStatusUpdate", function () {
|
||||
expect(res).to.be.undefined;
|
||||
});
|
||||
|
||||
it("should log an error if monitor is null", async function () {
|
||||
dbMock.getMonitorById.resolves(null);
|
||||
it("should log an error if finding monitor fails", async function () {
|
||||
dbMock.getMonitorById.throws(new Error("Database error"));
|
||||
|
||||
await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true);
|
||||
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith("Null Monitor: monitor1")).to.be.true;
|
||||
expect(loggerMock.error.firstCall.args[0].message).to.equal("Database error");
|
||||
});
|
||||
|
||||
it("should update monitor status if different", async function () {
|
||||
@@ -196,7 +190,7 @@ describe("NetworkService - handleStatusUpdate", function () {
|
||||
await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true);
|
||||
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith("Save error")).to.be.true;
|
||||
expect(loggerMock.error.firstCall.args[0].message).to.equal("Save error");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,43 +302,36 @@ describe("networkService - handlePing", () => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should handle a successful ping response", async function () {
|
||||
it("should handle a successful ping response", async () => {
|
||||
const response = { alive: true };
|
||||
const responseTime = 0;
|
||||
pingMock.promise.probe.resolves(response);
|
||||
pingMock.promise.probe.returns(response);
|
||||
logAndStoreCheckStub.resolves();
|
||||
await networkService.handlePing(job);
|
||||
expect(
|
||||
logAndStoreCheckStub.calledOnceWith(
|
||||
{
|
||||
monitorId: job.data._id,
|
||||
status: true,
|
||||
responseTime,
|
||||
message: successMessages.PING_SUCCESS,
|
||||
},
|
||||
networkService.db.createCheck
|
||||
)
|
||||
logAndStoreCheckStub.calledOnceWith({
|
||||
monitorId: job.data._id,
|
||||
status: response.alive,
|
||||
responseTime,
|
||||
message: successMessages.PING_SUCCESS,
|
||||
})
|
||||
).to.be.true;
|
||||
expect(handleStatusUpdateStub.calledOnceWith(job, true)).to.be.true;
|
||||
});
|
||||
it("should handle a successful ping response and isAlive === false", async function () {
|
||||
it("should handle a successful response and isAlive === false", async function () {
|
||||
const response = { alive: false };
|
||||
const responseTime = 0;
|
||||
pingMock.promise.probe.resolves(response);
|
||||
logAndStoreCheckStub.resolves();
|
||||
|
||||
await networkService.handlePing(job);
|
||||
console.log(logAndStoreCheckStub.getCall(0).args[0]);
|
||||
expect(
|
||||
logAndStoreCheckStub.calledOnceWith(
|
||||
{
|
||||
monitorId: job.data._id,
|
||||
status: false,
|
||||
responseTime,
|
||||
message: errorMessages.PING_CANNOT_RESOLVE,
|
||||
},
|
||||
networkService.db.createCheck
|
||||
)
|
||||
logAndStoreCheckStub.calledOnceWith({
|
||||
monitorId: job.data._id,
|
||||
status: false,
|
||||
responseTime,
|
||||
message: errorMessages.PING_CANNOT_RESOLVE,
|
||||
})
|
||||
).to.be.true;
|
||||
expect(handleStatusUpdateStub.calledOnceWith(job, false)).to.be.true;
|
||||
});
|
||||
@@ -801,7 +788,9 @@ describe("NetworkService - getStatus", () => {
|
||||
const result = await networkService.getStatus(job);
|
||||
expect(result).to.be.false;
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith(`Unsupported type: unknown`)).to.be.true;
|
||||
expect(loggerMock.error.firstCall.args[0].message).to.equal(
|
||||
"Unsupported type: unknown"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -830,12 +819,14 @@ describe("NetworkService - logAndStoreCheck", async () => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should log an error if `writeToDb` throws an error", async () => {
|
||||
it("should log an error if writeToDb throws an error", async () => {
|
||||
writeToDB.rejects(new Error("Database error"));
|
||||
await networkService.logAndStoreCheck(data, writeToDB);
|
||||
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be
|
||||
.true;
|
||||
expect(loggerMock.error.firstCall.args[0].details).to.equal(
|
||||
`Error writing check for ${data.monitorId}`
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the status of the inserted check if successful", async () => {
|
||||
@@ -848,14 +839,17 @@ describe("NetworkService - logAndStoreCheck", async () => {
|
||||
writeToDB.resolves(null);
|
||||
await networkService.logAndStoreCheck(data, writeToDB);
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be
|
||||
.true;
|
||||
expect(loggerMock.error.firstCall.args[0].details).to.equal(
|
||||
`Error writing check for ${data.monitorId}`
|
||||
);
|
||||
});
|
||||
it("should thrown an error if the check is not inserted (undefined)", async () => {
|
||||
writeToDB.resolves(undefined);
|
||||
await networkService.logAndStoreCheck(data, writeToDB);
|
||||
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be
|
||||
.true;
|
||||
expect(loggerMock.error.firstCall.args[0].details).to.equal(
|
||||
`Error writing check for ${data.monitorId}`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -84,7 +84,6 @@ describe("SettingsService", () => {
|
||||
try {
|
||||
await settingsService.loadSettings();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
expect(error.message).to.equal("Test error");
|
||||
expect(error.service).to.equal("OTHER_SERVICE");
|
||||
expect(error.method).to.equal("otherMethod");
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import sinon from "sinon";
|
||||
import logger from "../../utils/logger.js";
|
||||
import { Logger } from "../../utils/logger.js";
|
||||
import winston from "winston";
|
||||
|
||||
describe("Logger", () => {
|
||||
let infoStub, warnStub, errorStub;
|
||||
|
||||
beforeEach(() => {
|
||||
infoStub = sinon.stub(logger.logger, "info");
|
||||
warnStub = sinon.stub(logger.logger, "warn");
|
||||
errorStub = sinon.stub(logger.logger, "error");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
let createLoggerStub;
|
||||
|
||||
beforeEach(function () {
|
||||
createLoggerStub = sinon.stub(winston, "createLogger");
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should convert message to JSON string if it is an object", function () {
|
||||
const logMessage = { key: "value" };
|
||||
const expectedMessage = JSON.stringify(logMessage, null, 2);
|
||||
|
||||
createLoggerStub.callsFake((config) => {
|
||||
const consoleTransport = config.transports[0];
|
||||
const logEntry = {
|
||||
level: "info",
|
||||
message: logMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const formattedMessage = consoleTransport.format.transform(logEntry);
|
||||
expect(formattedMessage).to.include(expectedMessage);
|
||||
return { log: sinon.spy() };
|
||||
});
|
||||
|
||||
const logger = new Logger();
|
||||
logger.logger.info(logMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe("info", () => {
|
||||
it("should log an informational message", () => {
|
||||
const config = {
|
||||
message: "Info message",
|
||||
service: "TestService",
|
||||
method: "TestMethod",
|
||||
details: { key: "value" },
|
||||
};
|
||||
|
||||
logger.info(config);
|
||||
|
||||
expect(infoStub.calledOnce).to.be.true;
|
||||
expect(
|
||||
infoStub.calledWith(config.message, {
|
||||
service: config.service,
|
||||
method: config.method,
|
||||
details: config.details,
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("warn", () => {
|
||||
it("should log a warning message", () => {
|
||||
const config = {
|
||||
message: "Warning message",
|
||||
service: "TestService",
|
||||
method: "TestMethod",
|
||||
details: { key: "value" },
|
||||
};
|
||||
|
||||
logger.warn(config);
|
||||
|
||||
expect(warnStub.calledOnce).to.be.true;
|
||||
expect(
|
||||
warnStub.calledWith(config.message, {
|
||||
service: config.service,
|
||||
method: config.method,
|
||||
details: config.details,
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("error", () => {
|
||||
it("should log an error message", () => {
|
||||
const config = {
|
||||
message: "Error message",
|
||||
service: "TestService",
|
||||
method: "TestMethod",
|
||||
details: { key: "value" },
|
||||
stack: "Error stack trace",
|
||||
};
|
||||
|
||||
logger.error(config);
|
||||
|
||||
expect(errorStub.calledOnce).to.be.true;
|
||||
expect(
|
||||
errorStub.calledWith(config.message, {
|
||||
service: config.service,
|
||||
method: config.method,
|
||||
details: config.details,
|
||||
stack: config.stack,
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
+89
-21
@@ -1,23 +1,91 @@
|
||||
import winston from "winston";
|
||||
/**
|
||||
* @module
|
||||
* @example
|
||||
* logger.info("Registered a new user!")
|
||||
* logger.warn("User not found!")
|
||||
* logger.error("Cannot save")
|
||||
* @example
|
||||
* "Specify service and ID in the log if applicable."
|
||||
* logger.error("Descriptive Message",{"service":"monitor","monitorId":"123456"})
|
||||
* logger.error("Incorrect Credentials",{"service":"Auth","userId":"654321"})
|
||||
* logger.error("User not found!",{"service":"Auth"})
|
||||
*/
|
||||
const logger = winston.createLogger({
|
||||
level: "info",
|
||||
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({ filename: "app.log" }),
|
||||
],
|
||||
});
|
||||
import { createLogger, format, transports } from "winston";
|
||||
|
||||
class Logger {
|
||||
constructor() {
|
||||
const consoleFormat = format.printf(
|
||||
({ level, message, service, method, details, timestamp }) => {
|
||||
if (message instanceof Object) {
|
||||
message = JSON.stringify(message, null, 2);
|
||||
}
|
||||
let msg = `${timestamp} ${level}:`;
|
||||
service && (msg += ` [${service}]`);
|
||||
method && (msg += `(${method})`);
|
||||
message && (msg += ` ${message}`);
|
||||
details && (msg += ` (details: ${details})`);
|
||||
return msg;
|
||||
}
|
||||
);
|
||||
|
||||
this.logger = createLogger({
|
||||
level: "info",
|
||||
format: format.combine(format.timestamp()),
|
||||
transports: [
|
||||
new transports.Console({
|
||||
format: format.combine(
|
||||
format.colorize(),
|
||||
format.prettyPrint(),
|
||||
format.json(),
|
||||
consoleFormat
|
||||
),
|
||||
}),
|
||||
new transports.File({
|
||||
format: format.combine(format.json()),
|
||||
filename: "app.log",
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Logs an informational message.
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.message - The message to log.
|
||||
* @param {string} config.service - The service name.
|
||||
* @param {string} config.method - The method name.
|
||||
* @param {Object} config.details - Additional details.
|
||||
*/
|
||||
info(config) {
|
||||
this.logger.info(config.message, {
|
||||
service: config.service,
|
||||
method: config.method,
|
||||
details: config.details,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning message.
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.message - The message to log.
|
||||
* @param {string} config.service - The service name.
|
||||
* @param {string} config.method - The method name.
|
||||
* @param {Object} config.details - Additional details.
|
||||
*/
|
||||
warn(config) {
|
||||
this.logger.warn(config.message, {
|
||||
service: config.service,
|
||||
method: config.method,
|
||||
details: config.details,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error message.
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.message - The message to log.
|
||||
* @param {string} config.service - The service name.
|
||||
* @param {string} config.method - The method name.
|
||||
* @param {Object} config.details - Additional details.
|
||||
*/
|
||||
error(config) {
|
||||
this.logger.error(config.message, {
|
||||
service: config.service,
|
||||
method: config.method,
|
||||
details: config.details,
|
||||
stack: config.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const logger = new Logger();
|
||||
export { Logger };
|
||||
|
||||
export default logger;
|
||||
@@ -222,6 +222,11 @@ 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()).min(1),
|
||||
});
|
||||
|
||||
|
||||
@@ -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