mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-20 16:49:46 -06:00
Merge branch 'develop' into 1067-fe-hardware-monitoring-create-hardware-monitor
This commit is contained in:
24
Client/package-lock.json
generated
24
Client/package-lock.json
generated
@@ -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.22.1",
|
||||
"@mui/x-date-pickers": "7.22.1",
|
||||
"@mui/x-data-grid": "7.22.2",
|
||||
"@mui/x-date-pickers": "7.22.2",
|
||||
"@reduxjs/toolkit": "2.3.0",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
@@ -1400,9 +1400,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-charts": {
|
||||
"version": "7.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.1.tgz",
|
||||
"integrity": "sha512-zgr8CN4yLen5puqaX7Haj5+AoVG7E13HHsIiDoEAuQvuFDF0gKTxTTdLSKXqhd1qJUIIzJaztZtrr3YCVrENqw==",
|
||||
"version": "7.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.22.2.tgz",
|
||||
"integrity": "sha512-0Y2du4Ed7gOT53l8vVJ4vKT+Jz4Dh/iHnLy8TtL3+XhbPH9Ndu9Q30WwyyzOn84yt37hSUru/njQ1BWaSvVPHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1458,9 +1458,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-data-grid": {
|
||||
"version": "7.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.1.tgz",
|
||||
"integrity": "sha512-YHF96MEv7ACG/VuiycZjEAPH7cZLNuV2+bi/MyR1t/e6E6LTolYFykvjSFq+Imz1mYbW4+9mEvrHZsIKL5KKIQ==",
|
||||
"version": "7.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.22.2.tgz",
|
||||
"integrity": "sha512-yfy2s5A6tbajQZiEdsba49T4FYb9F0WPrzbbG30dl1+sIiX4ZRX7ma44UIDGPZrsZv8xkkE+p8qeJxZ7OaMteA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1495,9 +1495,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/x-date-pickers": {
|
||||
"version": "7.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.1.tgz",
|
||||
"integrity": "sha512-VBgicE+7PvJrdHSL6HyieHT6a/0dENH8RaMIM2VwUFrGoZzvik50WNwY5U+Hip1BwZLIEvlqtNRQIIj6kgBR6Q==",
|
||||
"version": "7.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.22.2.tgz",
|
||||
"integrity": "sha512-1KHSlIlnSoY3oHm820By8X344pIdGYqPvCCvfVHrEeeIQ/pHdxDD8tjZFWkFl4Jgm9oVFK90fMcqNZAzc+WaCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.7",
|
||||
@@ -1525,7 +1525,7 @@
|
||||
"dayjs": "^1.10.7",
|
||||
"luxon": "^3.0.2",
|
||||
"moment": "^2.29.4",
|
||||
"moment-hijri": "^2.1.2",
|
||||
"moment-hijri": "^2.1.2 || ^3.0.0",
|
||||
"moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
|
||||
"react": "^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0"
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"@mui/lab": "^5.0.0-alpha.170",
|
||||
"@mui/material": "^5.15.16",
|
||||
"@mui/x-charts": "^7.5.1",
|
||||
"@mui/x-data-grid": "7.22.1",
|
||||
"@mui/x-date-pickers": "7.22.1",
|
||||
"@mui/x-data-grid": "7.22.2",
|
||||
"@mui/x-date-pickers": "7.22.2",
|
||||
"@reduxjs/toolkit": "2.3.0",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"all": true,
|
||||
"include": ["controllers/*.js", "utils/*.js", "service/*.js"],
|
||||
"include": [
|
||||
"controllers/*.js",
|
||||
"utils/*.js",
|
||||
"service/*.js",
|
||||
"db/mongo/modules/monitorModule.js"
|
||||
],
|
||||
"exclude": ["**/*.test.js"],
|
||||
"reporter": ["html", "text", "lcov"],
|
||||
"sourceMap": false,
|
||||
|
||||
@@ -45,6 +45,28 @@ const getAllMonitors = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns all monitors with uptime stats for 1,7,30, and 90 days
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @param {function} next
|
||||
* @returns {Promise<Express.Response>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getAllMonitorsWithUptimeStats = async (req, res, next) => {
|
||||
try {
|
||||
const monitors = await req.db.getAllMonitorsWithUptimeStats();
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_GET_ALL,
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats"));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns monitor stats for monitor with matching ID
|
||||
* @async
|
||||
@@ -495,6 +517,7 @@ const addDemoMonitors = async (req, res, next) => {
|
||||
|
||||
export {
|
||||
getAllMonitors,
|
||||
getAllMonitorsWithUptimeStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
|
||||
@@ -99,6 +99,7 @@ import {
|
||||
|
||||
import {
|
||||
getAllMonitors,
|
||||
getAllMonitorsWithUptimeStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
@@ -187,6 +188,7 @@ export default {
|
||||
resetPassword,
|
||||
checkSuperadmin,
|
||||
getAllMonitors,
|
||||
getAllMonitorsWithUptimeStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
|
||||
@@ -48,7 +48,6 @@ const createCheck = async (checkData) => {
|
||||
}
|
||||
|
||||
await monitor.save();
|
||||
|
||||
return check;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -105,7 +104,7 @@ const getChecks = async (req) => {
|
||||
const { monitorId } = req.params;
|
||||
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
|
||||
// Default limit to 0 if not provided
|
||||
limit = limit === "undefined" ? 0 : limit;
|
||||
limit = limit === undefined ? 0 : limit;
|
||||
|
||||
// Default sort order is newest -> oldest
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
@@ -155,58 +154,64 @@ const getChecks = async (req) => {
|
||||
};
|
||||
|
||||
const getTeamChecks = async (req) => {
|
||||
const { teamId } = req.params;
|
||||
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
|
||||
try {
|
||||
const { teamId } = req.params;
|
||||
let { sortOrder, limit, dateRange, filter, page, rowsPerPage } = req.query;
|
||||
|
||||
// Get monitorIDs
|
||||
const userMonitors = await Monitor.find({ teamId: teamId }).select("_id");
|
||||
// Get monitorIDs
|
||||
const userMonitors = await Monitor.find({ teamId: teamId }).select("_id");
|
||||
|
||||
//Build check query
|
||||
// Default limit to 0 if not provided
|
||||
limit = limit === undefined ? 0 : limit;
|
||||
// Default sort order is newest -> oldest
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
//Build check query
|
||||
// Default limit to 0 if not provided
|
||||
limit = limit === undefined ? 0 : limit;
|
||||
// Default sort order is newest -> oldest
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
const checksQuery = { monitorId: { $in: userMonitors } };
|
||||
const checksQuery = { monitorId: { $in: userMonitors } };
|
||||
|
||||
if (filter !== undefined) {
|
||||
checksQuery.status = false;
|
||||
switch (filter) {
|
||||
case "all":
|
||||
break;
|
||||
case "down":
|
||||
break;
|
||||
case "resolve":
|
||||
checksQuery.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
logger.warn({
|
||||
message: "invalid filter",
|
||||
service: SERVICE_NAME,
|
||||
method: "getTeamChecks",
|
||||
});
|
||||
break;
|
||||
if (filter !== undefined) {
|
||||
checksQuery.status = false;
|
||||
switch (filter) {
|
||||
case "all":
|
||||
break;
|
||||
case "down":
|
||||
break;
|
||||
case "resolve":
|
||||
checksQuery.statusCode = 5000;
|
||||
break;
|
||||
default:
|
||||
logger.warn({
|
||||
message: "invalid filter",
|
||||
service: SERVICE_NAME,
|
||||
method: "getTeamChecks",
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dateRange !== undefined) {
|
||||
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
|
||||
}
|
||||
|
||||
// Skip and limit for pagination
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
const checksCount = await Check.countDocuments(checksQuery);
|
||||
|
||||
const checks = await Check.find(checksQuery)
|
||||
.skip(skip)
|
||||
.limit(rowsPerPage)
|
||||
.sort({ createdAt: sortOrder })
|
||||
.select(["monitorId", "status", "responseTime", "statusCode", "message"]);
|
||||
return { checksCount, checks };
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getTeamChecks";
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (dateRange !== undefined) {
|
||||
checksQuery.createdAt = { $gte: dateRangeLookup[dateRange] };
|
||||
}
|
||||
|
||||
// Skip and limit for pagination
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
const checksCount = await Check.countDocuments(checksQuery);
|
||||
|
||||
const checks = await Check.find(checksQuery)
|
||||
.skip(skip)
|
||||
.limit(rowsPerPage)
|
||||
.sort({ createdAt: sortOrder })
|
||||
.select(["monitorId", "status", "responseTime", "statusCode", "message"]);
|
||||
return { checksCount, checks };
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@ const SERVICE_NAME = "monitorModule";
|
||||
const CHECK_MODEL_LOOKUP = {
|
||||
http: Check,
|
||||
ping: Check,
|
||||
pageSpeed: PageSpeedCheck,
|
||||
pagespeed: PageSpeedCheck,
|
||||
hardware: HardwareCheck,
|
||||
};
|
||||
|
||||
@@ -43,6 +43,53 @@ const getAllMonitors = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all monitors with uptime stats for 1,7,30, and 90 days
|
||||
* @async
|
||||
* @param {Express.Request} req
|
||||
* @param {Express.Response} res
|
||||
* @returns {Promise<Array<Monitor>>}
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getAllMonitorsWithUptimeStats = async () => {
|
||||
const timeRanges = {
|
||||
1: new Date(Date.now() - 24 * 60 * 60 * 1000),
|
||||
7: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||
30: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
||||
90: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
|
||||
};
|
||||
|
||||
try {
|
||||
const monitors = await Monitor.find();
|
||||
const monitorsWithStats = await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
const model = CHECK_MODEL_LOOKUP[monitor.type];
|
||||
|
||||
const uptimeStats = await Promise.all(
|
||||
Object.entries(timeRanges).map(async ([days, startDate]) => {
|
||||
const checks = await model.find({
|
||||
monitorId: monitor._id,
|
||||
createdAt: { $gte: startDate },
|
||||
});
|
||||
return [days, getUptimePercentage(checks)];
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
...monitor.toObject(),
|
||||
...Object.fromEntries(uptimeStats),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return monitorsWithStats;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getAllMonitorsWithUptimeStats";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Function to calculate uptime duration based on the most recent check.
|
||||
* @param {Array} checks Array of check objects.
|
||||
@@ -52,11 +99,10 @@ const calculateUptimeDuration = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const latestCheck = new Date(checks[0].createdAt);
|
||||
let latestDownCheck = 0;
|
||||
|
||||
for (let i = checks.length; i <= 0; i--) {
|
||||
for (let i = checks.length - 1; i >= 0; i--) {
|
||||
if (checks[i].status === false) {
|
||||
latestDownCheck = new Date(checks[i].createdAt);
|
||||
break;
|
||||
@@ -94,7 +140,8 @@ const getLatestResponseTime = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return checks[0].responseTime;
|
||||
|
||||
return checks[0]?.responseTime ?? 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -106,14 +153,19 @@ const getAverageResponseTime = (checks) => {
|
||||
if (!checks || checks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const aggResponseTime = checks.reduce((sum, check) => {
|
||||
|
||||
const validChecks = checks.filter((check) => typeof check.responseTime === "number");
|
||||
if (validChecks.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const aggResponseTime = validChecks.reduce((sum, check) => {
|
||||
return sum + check.responseTime;
|
||||
}, 0);
|
||||
return aggResponseTime / checks.length;
|
||||
return aggResponseTime / validChecks.length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to get precentage 24h uptime
|
||||
* Helper function to get percentage 24h uptime
|
||||
* @param {Array} checks Array of check objects.
|
||||
* @returns {number} Timestamp of the most recent check.
|
||||
*/
|
||||
@@ -143,6 +195,112 @@ const getIncidents = (checks) => {
|
||||
}, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get date range parameters
|
||||
* @param {string} dateRange - 'day' | 'week' | 'month'
|
||||
* @returns {Object} Start and end dates
|
||||
*/
|
||||
const getDateRange = (dateRange) => {
|
||||
const startDates = {
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
};
|
||||
return {
|
||||
start: startDates[dateRange],
|
||||
end: new Date(),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get checks for a monitor
|
||||
* @param {string} monitorId - Monitor ID
|
||||
* @param {Object} model - Check model to use
|
||||
* @param {Object} dateRange - Date range parameters
|
||||
* @param {number} sortOrder - Sort order (1 for ascending, -1 for descending)
|
||||
* @returns {Promise<Object>} All checks and date-ranged checks
|
||||
*/
|
||||
const getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => {
|
||||
const [checksAll, checksForDateRange] = await Promise.all([
|
||||
model.find({ monitorId }).sort({ createdAt: sortOrder }),
|
||||
model
|
||||
.find({
|
||||
monitorId,
|
||||
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
|
||||
})
|
||||
.sort({ createdAt: sortOrder }),
|
||||
]);
|
||||
return { checksAll, checksForDateRange };
|
||||
};
|
||||
|
||||
/**
|
||||
* Process checks for display
|
||||
* @param {Array} checks - Checks to process
|
||||
* @param {number} numToDisplay - Number of checks to display
|
||||
* @param {boolean} normalize - Whether to normalize the data
|
||||
* @returns {Array} Processed checks
|
||||
*/
|
||||
const processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => {
|
||||
let processedChecks = checks;
|
||||
if (numToDisplay && checks.length > numToDisplay) {
|
||||
const n = Math.ceil(checks.length / numToDisplay);
|
||||
processedChecks = checks.filter((_, index) => index % n === 0);
|
||||
}
|
||||
return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get time-grouped checks based on date range
|
||||
* @param {Array} checks Array of check objects
|
||||
* @param {string} dateRange 'day' | 'week' | 'month'
|
||||
* @returns {Object} Grouped checks by time period
|
||||
*/
|
||||
const groupChecksByTime = (checks, dateRange) => {
|
||||
return checks.reduce((acc, check) => {
|
||||
// Validate the date
|
||||
const checkDate = new Date(check.createdAt);
|
||||
if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
const time =
|
||||
dateRange === "day"
|
||||
? checkDate.setMinutes(0, 0, 0)
|
||||
: checkDate.toISOString().split("T")[0];
|
||||
|
||||
if (!acc[time]) {
|
||||
acc[time] = { time, checks: [] };
|
||||
}
|
||||
acc[time].checks.push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate aggregate stats for a group of checks
|
||||
* @param {Object} group Group of checks
|
||||
* @returns {Object} Stats for the group
|
||||
*/
|
||||
const calculateGroupStats = (group) => {
|
||||
const totalChecks = group.checks.length;
|
||||
|
||||
const checksWithResponseTime = group.checks.filter(
|
||||
(check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime)
|
||||
);
|
||||
|
||||
return {
|
||||
time: group.time,
|
||||
uptimePercentage: getUptimePercentage(group.checks),
|
||||
totalChecks,
|
||||
totalIncidents: group.checks.filter((check) => !check.status).length,
|
||||
avgResponseTime:
|
||||
checksWithResponseTime.length > 0
|
||||
? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) /
|
||||
checksWithResponseTime.length
|
||||
: 0,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get stats by monitor ID
|
||||
* @async
|
||||
@@ -152,129 +310,53 @@ const getIncidents = (checks) => {
|
||||
* @throws {Error}
|
||||
*/
|
||||
const getMonitorStatsById = async (req) => {
|
||||
const startDates = {
|
||||
day: new Date(new Date().setDate(new Date().getDate() - 1)),
|
||||
week: new Date(new Date().setDate(new Date().getDate() - 7)),
|
||||
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
|
||||
};
|
||||
const endDate = new Date();
|
||||
try {
|
||||
// Get monitor
|
||||
const { monitorId } = req.params;
|
||||
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
|
||||
|
||||
// Get monitor, if we can't find it, abort with error
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
|
||||
}
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
// Default sort order is newest -> oldest
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
let model = CHECK_MODEL_LOOKUP[monitor.type];
|
||||
// Get query params
|
||||
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
|
||||
const sort = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
// Get Checks for monitor in date range requested
|
||||
const model = CHECK_MODEL_LOOKUP[monitor.type];
|
||||
const dates = getDateRange(dateRange);
|
||||
const { checksAll, checksForDateRange } = await getMonitorChecks(
|
||||
monitorId,
|
||||
model,
|
||||
dates,
|
||||
sort
|
||||
);
|
||||
|
||||
// Build monitor stats
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
uptimeDuration: calculateUptimeDuration(checksAll),
|
||||
lastChecked: getLastChecked(checksAll),
|
||||
latestResponseTime: getLatestResponseTime(checksAll),
|
||||
periodIncidents: getIncidents(checksForDateRange),
|
||||
periodTotalChecks: checksForDateRange.length,
|
||||
checks: processChecksForDisplay(
|
||||
NormalizeData,
|
||||
checksForDateRange,
|
||||
numToDisplay,
|
||||
normalize
|
||||
),
|
||||
};
|
||||
|
||||
// Build checks query
|
||||
const checksQuery = { monitorId: monitor._id };
|
||||
|
||||
// Get all checks
|
||||
const checksAll = await model.find(checksQuery).sort({
|
||||
createdAt: sortOrder,
|
||||
});
|
||||
|
||||
const checksQueryForDateRange = {
|
||||
...checksQuery,
|
||||
createdAt: {
|
||||
$gte: startDates[dateRange],
|
||||
$lte: endDate,
|
||||
},
|
||||
};
|
||||
|
||||
const checksForDateRange = await model
|
||||
.find(checksQueryForDateRange)
|
||||
.sort({ createdAt: sortOrder });
|
||||
|
||||
if (monitor.type === "http" || monitor.type === "ping") {
|
||||
// HTTP/PING Specific stats
|
||||
monitorStats.periodAvgResponseTime = getAverageResponseTime(checksForDateRange);
|
||||
monitorStats.periodUptime = getUptimePercentage(checksForDateRange);
|
||||
|
||||
// Aggregate data
|
||||
let groupedChecks;
|
||||
// Group checks by hour if range is day
|
||||
if (dateRange === "day") {
|
||||
groupedChecks = checksForDateRange.reduce((acc, check) => {
|
||||
const time = new Date(check.createdAt);
|
||||
time.setMinutes(0, 0, 0);
|
||||
if (!acc[time]) {
|
||||
acc[time] = { time, checks: [] };
|
||||
}
|
||||
acc[time].checks.push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
} else {
|
||||
groupedChecks = checksForDateRange.reduce((acc, check) => {
|
||||
const time = new Date(check.createdAt).toISOString().split("T")[0]; // Extract the date part
|
||||
if (!acc[time]) {
|
||||
acc[time] = { time, checks: [] };
|
||||
}
|
||||
acc[time].checks.push(check);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Map grouped checks to stats
|
||||
const aggregateData = Object.values(groupedChecks).map((group) => {
|
||||
const totalChecks = group.checks.length;
|
||||
const uptimePercentage = getUptimePercentage(group.checks);
|
||||
const totalIncidents = group.checks.filter(
|
||||
(check) => check.status === false
|
||||
).length;
|
||||
const avgResponseTime =
|
||||
group.checks.reduce((sum, check) => sum + check.responseTime, 0) / totalChecks;
|
||||
|
||||
return {
|
||||
time: group.time,
|
||||
uptimePercentage,
|
||||
totalChecks,
|
||||
totalIncidents,
|
||||
avgResponseTime,
|
||||
};
|
||||
});
|
||||
monitorStats.aggregateData = aggregateData;
|
||||
const groupedChecks = groupChecksByTime(checksForDateRange, dateRange);
|
||||
monitorStats.aggregateData = Object.values(groupedChecks).map(calculateGroupStats);
|
||||
}
|
||||
|
||||
monitorStats.periodIncidents = getIncidents(checksForDateRange);
|
||||
monitorStats.periodTotalChecks = checksForDateRange.length;
|
||||
|
||||
// If more than numToDisplay checks, pick every nth check
|
||||
|
||||
let nthChecks = checksForDateRange;
|
||||
|
||||
if (
|
||||
numToDisplay !== undefined &&
|
||||
checksForDateRange &&
|
||||
checksForDateRange.length > numToDisplay
|
||||
) {
|
||||
const n = Math.ceil(checksForDateRange.length / numToDisplay);
|
||||
nthChecks = checksForDateRange.filter((_, index) => index % n === 0);
|
||||
}
|
||||
|
||||
// Normalize checks if requested
|
||||
if (normalize !== undefined) {
|
||||
const normailzedChecks = NormalizeData(nthChecks, 1, 100);
|
||||
monitorStats.checks = normailzedChecks;
|
||||
} else {
|
||||
monitorStats.checks = nthChecks;
|
||||
}
|
||||
|
||||
monitorStats.uptimeDuration = calculateUptimeDuration(checksAll);
|
||||
monitorStats.lastChecked = getLastChecked(checksAll);
|
||||
monitorStats.latestResponseTime = getLatestResponseTime(checksAll);
|
||||
|
||||
return monitorStats;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
@@ -303,12 +385,12 @@ const getMonitorById = async (monitorId) => {
|
||||
const notifications = await Notification.find({
|
||||
monitorId: monitorId,
|
||||
});
|
||||
const updatedMonitor = await Monitor.findByIdAndUpdate(
|
||||
monitorId,
|
||||
{ notifications },
|
||||
{ new: true }
|
||||
).populate("notifications");
|
||||
return updatedMonitor;
|
||||
|
||||
// Update monitor with notifications and save
|
||||
monitor.notifications = notifications;
|
||||
await monitor.save();
|
||||
|
||||
return monitor;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMonitorById";
|
||||
@@ -371,10 +453,12 @@ const getMonitorsByTeamId = async (req, res) => {
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
} = req.query || {};
|
||||
} = req.query;
|
||||
|
||||
const monitorQuery = { teamId: req.params.teamId };
|
||||
|
||||
if (type !== undefined) {
|
||||
monitorQuery.type = type;
|
||||
monitorQuery.type = Array.isArray(type) ? { $in: type } : type;
|
||||
}
|
||||
// Add filter if provided
|
||||
// $options: "i" makes the search case-insensitive
|
||||
@@ -384,29 +468,13 @@ const getMonitorsByTeamId = async (req, res) => {
|
||||
{ url: { $regex: filter, $options: "i" } },
|
||||
];
|
||||
}
|
||||
const monitorsCount = await Monitor.countDocuments(monitorQuery);
|
||||
const monitorCount = await Monitor.countDocuments(monitorQuery);
|
||||
|
||||
// Pagination
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
|
||||
|
||||
if (type !== undefined) {
|
||||
const types = Array.isArray(type) ? type : [type];
|
||||
monitorQuery.type = { $in: types };
|
||||
}
|
||||
|
||||
// Default sort order is newest -> oldest
|
||||
if (checkOrder === "asc") {
|
||||
checkOrder = 1;
|
||||
} else checkOrder = -1;
|
||||
|
||||
// Sort order for monitors
|
||||
let sort = {};
|
||||
if (field !== undefined && order !== undefined) {
|
||||
sort[field] = order === "asc" ? 1 : -1;
|
||||
}
|
||||
// Build Sort option
|
||||
const sort = field ? { [field]: order === "asc" ? 1 : -1 } : {};
|
||||
|
||||
const monitors = await Monitor.find(monitorQuery)
|
||||
.skip(skip)
|
||||
@@ -415,29 +483,22 @@ const getMonitorsByTeamId = async (req, res) => {
|
||||
|
||||
// Early return if limit is set to -1, indicating we don't want any checks
|
||||
if (limit === "-1") {
|
||||
return { monitors, monitorCount: monitorsCount };
|
||||
return { monitors, monitorCount };
|
||||
}
|
||||
|
||||
// This effectively removes limit, returning all checks
|
||||
if (limit === undefined) limit = 0;
|
||||
|
||||
// Map each monitor to include its associated checks
|
||||
const monitorsWithChecks = await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
const checksQuery = { monitorId: monitor._id };
|
||||
if (status !== undefined) {
|
||||
checksQuery.status = status;
|
||||
}
|
||||
|
||||
let model = CHECK_MODEL_LOOKUP[monitor.type];
|
||||
|
||||
// Checks are order newest -> oldest
|
||||
let checks = await model
|
||||
.find(checksQuery)
|
||||
.sort({
|
||||
createdAt: checkOrder,
|
||||
.find({
|
||||
monitorId: monitor._id,
|
||||
...(status && { status }),
|
||||
})
|
||||
.limit(limit);
|
||||
.sort({ createdAt: checkOrder === "asc" ? 1 : -1 })
|
||||
|
||||
.limit(limit || 0);
|
||||
|
||||
//Normalize checks if requested
|
||||
if (normalize !== undefined) {
|
||||
@@ -446,7 +507,7 @@ const getMonitorsByTeamId = async (req, res) => {
|
||||
return { ...monitor.toObject(), checks };
|
||||
})
|
||||
);
|
||||
return { monitors: monitorsWithChecks, monitorCount: monitorsCount };
|
||||
return { monitors: monitorsWithChecks, monitorCount };
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMonitorsByTeamId";
|
||||
@@ -580,6 +641,7 @@ const addDemoMonitors = async (userId, teamId) => {
|
||||
|
||||
export {
|
||||
getAllMonitors,
|
||||
getAllMonitorsWithUptimeStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
@@ -591,3 +653,18 @@ export {
|
||||
editMonitor,
|
||||
addDemoMonitors,
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
export {
|
||||
calculateUptimeDuration,
|
||||
getLastChecked,
|
||||
getLatestResponseTime,
|
||||
getAverageResponseTime,
|
||||
getUptimePercentage,
|
||||
getIncidents,
|
||||
getDateRange,
|
||||
getMonitorChecks,
|
||||
processChecksForDisplay,
|
||||
groupChecksByTime,
|
||||
calculateGroupStats,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ import cors from "cors";
|
||||
import logger from "./utils/logger.js";
|
||||
import { verifyJWT } from "./middleware/verifyJWT.js";
|
||||
import { handleErrors } from "./middleware/handleErrors.js";
|
||||
import { errorMessages } from "./utils/messages.js";
|
||||
import authRouter from "./routes/authRoute.js";
|
||||
import inviteRouter from "./routes/inviteRoute.js";
|
||||
import monitorRouter from "./routes/monitorRoute.js";
|
||||
@@ -45,8 +44,9 @@ import NotificationService from "./service/notificationService.js";
|
||||
|
||||
import db from "./db/mongo/MongoDB.js";
|
||||
const SERVICE_NAME = "Server";
|
||||
const SHUTDOWN_TIMEOUT = 0;
|
||||
|
||||
let cleaningUp = false;
|
||||
let isShuttingDown = false;
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@@ -113,11 +113,11 @@ const startApp = async () => {
|
||||
|
||||
// Create services
|
||||
await db.connect();
|
||||
app.listen(PORT, () => {
|
||||
const server = app.listen(PORT, () => {
|
||||
logger.info({ message: `server started on port:${PORT}` });
|
||||
});
|
||||
const settingsService = new SettingsService(AppSettings);
|
||||
|
||||
const settingsService = new SettingsService(AppSettings);
|
||||
await settingsService.loadSettings();
|
||||
const emailService = new EmailService(
|
||||
settingsService,
|
||||
@@ -142,30 +142,39 @@ const startApp = async () => {
|
||||
Worker
|
||||
);
|
||||
|
||||
const cleanup = async () => {
|
||||
if (cleaningUp) {
|
||||
logger.warn({ message: "Already cleaning up" });
|
||||
const shutdown = async () => {
|
||||
if (isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
cleaningUp = true;
|
||||
isShuttingDown = true;
|
||||
logger.info({ message: "Attempting graceful shutdown" });
|
||||
setTimeout(() => {
|
||||
logger.error({
|
||||
message: "Could not shut down in time, forcing shutdown",
|
||||
service: SERVICE_NAME,
|
||||
method: "shutdown",
|
||||
});
|
||||
process.exit(1);
|
||||
}, SHUTDOWN_TIMEOUT);
|
||||
try {
|
||||
logger.info({ message: "shutting down gracefully" });
|
||||
server.close();
|
||||
await jobQueue.obliterate();
|
||||
await db.disconnect();
|
||||
logger.info({ message: "shut down gracefully" });
|
||||
logger.info({ message: "Graceful shutdown complete" });
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "cleanup",
|
||||
method: "shutdown",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
process.on("SIGUSR2", cleanup);
|
||||
process.on("SIGINT", cleanup);
|
||||
process.on("SIGTERM", cleanup);
|
||||
|
||||
process.on("SIGUSR2", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
};
|
||||
|
||||
startApp().catch((error) => {
|
||||
|
||||
@@ -22,16 +22,12 @@
|
||||
"variables": {
|
||||
"PORT": {
|
||||
"description": "API Port",
|
||||
"enum": [
|
||||
"5000"
|
||||
],
|
||||
"enum": ["5000"],
|
||||
"default": "5000"
|
||||
},
|
||||
"API_PATH": {
|
||||
"description": "API Base Path",
|
||||
"enum": [
|
||||
"api/v1"
|
||||
],
|
||||
"enum": ["api/v1"],
|
||||
"default": "api/v1"
|
||||
}
|
||||
}
|
||||
@@ -42,9 +38,7 @@
|
||||
"variables": {
|
||||
"API_PATH": {
|
||||
"description": "API Base Path",
|
||||
"enum": [
|
||||
"api/v1"
|
||||
],
|
||||
"enum": ["api/v1"],
|
||||
"default": "api/v1"
|
||||
}
|
||||
}
|
||||
@@ -55,16 +49,12 @@
|
||||
"variables": {
|
||||
"PORT": {
|
||||
"description": "API Port",
|
||||
"enum": [
|
||||
"5000"
|
||||
],
|
||||
"enum": ["5000"],
|
||||
"default": "5000"
|
||||
},
|
||||
"API_PATH": {
|
||||
"description": "API Base Path",
|
||||
"enum": [
|
||||
"api/v1"
|
||||
],
|
||||
"enum": ["api/v1"],
|
||||
"default": "api/v1"
|
||||
}
|
||||
}
|
||||
@@ -99,9 +89,7 @@
|
||||
"paths": {
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Register a new user",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -137,23 +125,8 @@
|
||||
},
|
||||
"role": {
|
||||
"type": "array",
|
||||
"enum": [
|
||||
[
|
||||
"user"
|
||||
],
|
||||
[
|
||||
"admin"
|
||||
],
|
||||
[
|
||||
"superadmin"
|
||||
],
|
||||
[
|
||||
"Demo"
|
||||
]
|
||||
],
|
||||
"default": [
|
||||
"superadmin"
|
||||
]
|
||||
"enum": [["user"], ["admin"], ["superadmin"], ["Demo"]],
|
||||
"default": ["superadmin"]
|
||||
},
|
||||
"teamId": {
|
||||
"type": "string",
|
||||
@@ -200,19 +173,14 @@
|
||||
},
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Login with credentials",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"password"
|
||||
],
|
||||
"required": ["email", "password"],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
@@ -263,9 +231,7 @@
|
||||
},
|
||||
"/auth/refresh": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Generates a new auth token if the refresh token is valid.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -334,9 +300,7 @@
|
||||
},
|
||||
"/auth/user/{userId}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Change user information",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -397,9 +361,7 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Delete user",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -452,9 +414,7 @@
|
||||
},
|
||||
"/auth/users/superadmin": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Checks to see if an admin account exists",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -497,9 +457,7 @@
|
||||
},
|
||||
"/auth/users": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Get all users",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -542,18 +500,14 @@
|
||||
},
|
||||
"/auth/recovery/request": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Request a recovery token",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email"
|
||||
],
|
||||
"required": ["email"],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
@@ -600,18 +554,14 @@
|
||||
},
|
||||
"/auth/recovery/validate": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Validate recovery token",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"recoveryToken"
|
||||
],
|
||||
"required": ["recoveryToken"],
|
||||
"properties": {
|
||||
"recoveryToken": {
|
||||
"type": "string"
|
||||
@@ -657,19 +607,14 @@
|
||||
},
|
||||
"/auth/recovery/reset": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"tags": ["auth"],
|
||||
"description": "Password reset",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"recoveryToken",
|
||||
"password"
|
||||
],
|
||||
"required": ["recoveryToken", "password"],
|
||||
"properties": {
|
||||
"recoveryToken": {
|
||||
"type": "string"
|
||||
@@ -718,19 +663,14 @@
|
||||
},
|
||||
"/invite": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"invite"
|
||||
],
|
||||
"tags": ["invite"],
|
||||
"description": "Request an invitation",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"email",
|
||||
"role"
|
||||
],
|
||||
"required": ["email", "role"],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
@@ -784,18 +724,14 @@
|
||||
},
|
||||
"/invite/verify": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"invite"
|
||||
],
|
||||
"tags": ["invite"],
|
||||
"description": "Request an invitation",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"token"
|
||||
],
|
||||
"required": ["token"],
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
@@ -846,9 +782,7 @@
|
||||
},
|
||||
"/monitors": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Get all monitors",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -889,9 +823,7 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Create a new monitor",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -941,9 +873,7 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Delete all monitors",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -984,11 +914,42 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/monitors/uptime": {
|
||||
"get": {
|
||||
"tags": ["monitors"],
|
||||
"description": "Get all monitors with uptime stats for 1, 7, 30, and 90 days",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/monitors/resolution/url": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Check DNS resolution for a given URL",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1053,9 +1014,7 @@
|
||||
},
|
||||
"/monitors/{monitorId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Get monitor by id",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1106,9 +1065,7 @@
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Update monitor by id",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1168,9 +1125,7 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Delete monitor by id",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1223,9 +1178,7 @@
|
||||
},
|
||||
"/monitors/stats/{monitorId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Get monitor stats",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1278,9 +1231,7 @@
|
||||
},
|
||||
"/monitors/certificate/{monitorId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Get monitor certificate",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1333,9 +1284,7 @@
|
||||
},
|
||||
"/monitors/team/summary/{teamId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Get monitors and summary by teamId",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1352,11 +1301,7 @@
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"enum": [
|
||||
"http",
|
||||
"ping",
|
||||
"pagespeed"
|
||||
]
|
||||
"enum": ["http", "ping", "pagespeed"]
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1401,9 +1346,7 @@
|
||||
},
|
||||
"/monitors/team/{teamId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Get monitors by teamId",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1430,10 +1373,7 @@
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
]
|
||||
"enum": ["asc", "desc"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1452,11 +1392,7 @@
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"http",
|
||||
"ping",
|
||||
"pagespeed"
|
||||
]
|
||||
"enum": ["http", "ping", "pagespeed"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1500,11 +1436,7 @@
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"http",
|
||||
"ping",
|
||||
"pagespeed"
|
||||
]
|
||||
"enum": ["http", "ping", "pagespeed"]
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1549,9 +1481,7 @@
|
||||
},
|
||||
"/monitors/pause/{monitorId}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Pause monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1604,9 +1534,7 @@
|
||||
},
|
||||
"/monitors/demo": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"tags": ["monitors"],
|
||||
"description": "Create a demo monitor",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -1658,9 +1586,7 @@
|
||||
},
|
||||
"/checks/{monitorId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"tags": ["checks"],
|
||||
"description": "Get all checks for a monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1711,9 +1637,7 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"tags": ["checks"],
|
||||
"description": "Create a new check",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1773,9 +1697,7 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"tags": ["checks"],
|
||||
"description": "Delete all checks for a monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1828,9 +1750,7 @@
|
||||
},
|
||||
"/checks/team/{teamId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"tags": ["checks"],
|
||||
"description": "Get all checks for a team",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1881,9 +1801,7 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"tags": ["checks"],
|
||||
"description": "Delete all checks for a team",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1936,9 +1854,7 @@
|
||||
},
|
||||
"/checks/team/ttl": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"tags": ["checks"],
|
||||
"description": "Update check TTL",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -1990,9 +1906,7 @@
|
||||
},
|
||||
"/maintenance-window/monitor/{monitorId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"maintenance-window"
|
||||
],
|
||||
"tags": ["maintenance-window"],
|
||||
"description": "Get maintenance window for monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -2043,9 +1957,7 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"maintenance-window"
|
||||
],
|
||||
"tags": ["maintenance-window"],
|
||||
"description": "Create maintenance window for monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -2107,9 +2019,7 @@
|
||||
},
|
||||
"/maintenance-window/user/{userId}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"maintenance-window"
|
||||
],
|
||||
"tags": ["maintenance-window"],
|
||||
"description": "Get maintenance window for user",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -2162,9 +2072,7 @@
|
||||
},
|
||||
"/queue/jobs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"queue"
|
||||
],
|
||||
"tags": ["queue"],
|
||||
"description": "Get all jobs in queue",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -2205,9 +2113,7 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"queue"
|
||||
],
|
||||
"tags": ["queue"],
|
||||
"description": "Create a new job. Useful for testing scaling workers",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -2250,9 +2156,7 @@
|
||||
},
|
||||
"/queue/metrics": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"queue"
|
||||
],
|
||||
"tags": ["queue"],
|
||||
"description": "Get queue metrics",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -2295,9 +2199,7 @@
|
||||
},
|
||||
"/queue/obliterate": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"queue"
|
||||
],
|
||||
"tags": ["queue"],
|
||||
"description": "Obliterate job queue",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -2377,14 +2279,7 @@
|
||||
},
|
||||
"UserUpdateRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"password",
|
||||
"role",
|
||||
"teamId"
|
||||
],
|
||||
"required": ["firstName", "lastName", "email", "password", "role", "teamId"],
|
||||
"properties": {
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
@@ -2406,23 +2301,8 @@
|
||||
},
|
||||
"role": {
|
||||
"type": "array",
|
||||
"enum": [
|
||||
[
|
||||
"user"
|
||||
],
|
||||
[
|
||||
"admin"
|
||||
],
|
||||
[
|
||||
"superadmin"
|
||||
],
|
||||
[
|
||||
"Demo"
|
||||
]
|
||||
],
|
||||
"default": [
|
||||
"superadmin"
|
||||
]
|
||||
"enum": [["user"], ["admin"], ["superadmin"], ["Demo"]],
|
||||
"default": ["superadmin"]
|
||||
},
|
||||
"deleteProfileImage": {
|
||||
"type": "boolean"
|
||||
@@ -2431,14 +2311,7 @@
|
||||
},
|
||||
"CreateMonitorBody": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"userId",
|
||||
"teamId",
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"required": ["userId", "teamId", "name", "description", "type", "url"],
|
||||
"properties": {
|
||||
"_id": {
|
||||
"type": "string"
|
||||
@@ -2457,11 +2330,7 @@
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"http",
|
||||
"ping",
|
||||
"pagespeed"
|
||||
]
|
||||
"enum": ["http", "ping", "pagespeed"]
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
@@ -2502,13 +2371,7 @@
|
||||
},
|
||||
"CreateCheckBody": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"monitorId",
|
||||
"status",
|
||||
"responseTime",
|
||||
"statusCode",
|
||||
"message"
|
||||
],
|
||||
"required": ["monitorId", "status", "responseTime", "statusCode", "message"],
|
||||
"properties": {
|
||||
"monitorId": {
|
||||
"type": "string"
|
||||
@@ -2529,9 +2392,7 @@
|
||||
},
|
||||
"UpdateCheckTTLBody": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"ttl"
|
||||
],
|
||||
"required": ["ttl"],
|
||||
"properties": {
|
||||
"ttl": {
|
||||
"type": "integer"
|
||||
@@ -2540,13 +2401,7 @@
|
||||
},
|
||||
"CreateMaintenanceWindowBody": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"userId",
|
||||
"active",
|
||||
"oneTime",
|
||||
"start",
|
||||
"end"
|
||||
],
|
||||
"required": ["userId", "active", "oneTime", "start", "end"],
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "string"
|
||||
|
||||
42
Server/package-lock.json
generated
42
Server/package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"@sendgrid/mail": "^8.1.3",
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.25.0",
|
||||
"bullmq": "5.25.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
@@ -1164,9 +1164,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.25.0",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.0.tgz",
|
||||
"integrity": "sha512-QBbtabDUgdztalbYbrCc5NFwrRUKOZyiAkVFPhdrBGFsdraPq5SrXx6WP7U1stKj3hYYp1IuW+n3wuksYGITvw==",
|
||||
"version": "5.25.4",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.25.4.tgz",
|
||||
"integrity": "sha512-f9M5qfFOg9hdoMWmux9x9rZm9ZUPTMFfdDMO2zRsi7IOzgvZ0UxB6oTk77PlC9YSDYoufAgBw82xU1nwvnsKSA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.6.0",
|
||||
@@ -3467,9 +3467,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/logform": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz",
|
||||
"integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
|
||||
"integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.6.0",
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
@@ -4369,9 +4370,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.0.tgz",
|
||||
"integrity": "sha512-KluvgwnQB1GPOYZZXUHJRjS1TW6xxwTlf/YgjWExuuNanIe3W7VcR7dDXQVCIRk8L7NYge8EnoTcu2grWtN+XQ==",
|
||||
"version": "8.8.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.1.tgz",
|
||||
"integrity": "sha512-l7DgeY1szT98+EKU8GYnga5WnyatAu+kOQ2VlVX1Mxif6A0Umt0YkSiksCiyGxzx8SPhGe9a53ND1GD4yVDrPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.7.0",
|
||||
@@ -6673,34 +6674,35 @@
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.16.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.16.0.tgz",
|
||||
"integrity": "sha512-xz7+cyGN5M+4CmmD4Npq1/4T+UZaz7HaeTlAruFUTjk79CNMq+P6H30vlE4z0qfqJ01VHYQwd7OZo03nYm/+lg==",
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "^1.6.0",
|
||||
"@dabh/diagnostics": "^2.0.2",
|
||||
"async": "^3.2.3",
|
||||
"is-stream": "^2.0.0",
|
||||
"logform": "^2.6.0",
|
||||
"logform": "^2.7.0",
|
||||
"one-time": "^1.0.0",
|
||||
"readable-stream": "^3.4.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"stack-trace": "0.0.x",
|
||||
"triple-beam": "^1.3.0",
|
||||
"winston-transport": "^4.7.0"
|
||||
"winston-transport": "^4.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/winston-transport": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz",
|
||||
"integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==",
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
|
||||
"integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"logform": "^2.3.2",
|
||||
"readable-stream": "^3.6.0",
|
||||
"logform": "^2.7.0",
|
||||
"readable-stream": "^3.6.2",
|
||||
"triple-beam": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"@sendgrid/mail": "^8.1.3",
|
||||
"axios": "^1.7.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "5.25.0",
|
||||
"bullmq": "5.25.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Router } from "express";
|
||||
import {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
checkEndpointResolution,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
editMonitor,
|
||||
pauseMonitor,
|
||||
addDemoMonitors,
|
||||
getAllMonitors,
|
||||
getAllMonitorsWithUptimeStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
checkEndpointResolution,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
editMonitor,
|
||||
pauseMonitor,
|
||||
addDemoMonitors,
|
||||
} from "../controllers/monitorController.js";
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
|
||||
@@ -20,26 +21,23 @@ import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
|
||||
const router = Router();
|
||||
|
||||
router.get("/", getAllMonitors);
|
||||
router.get("/uptime", getAllMonitorsWithUptimeStats);
|
||||
router.get("/stats/:monitorId", getMonitorStatsById);
|
||||
router.get("/certificate/:monitorId", (req, res, next) => {
|
||||
getMonitorCertificate(req, res, next, fetchMonitorCertificate);
|
||||
getMonitorCertificate(req, res, next, fetchMonitorCertificate);
|
||||
});
|
||||
router.get("/:monitorId", getMonitorById);
|
||||
router.get("/team/summary/:teamId", getMonitorsAndSummaryByTeamId);
|
||||
router.get("/team/:teamId", getMonitorsByTeamId);
|
||||
|
||||
router.get(
|
||||
"/resolution/url",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
checkEndpointResolution
|
||||
)
|
||||
|
||||
router.delete(
|
||||
"/:monitorId",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
deleteMonitor
|
||||
"/resolution/url",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
checkEndpointResolution
|
||||
);
|
||||
|
||||
router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), deleteMonitor);
|
||||
|
||||
router.post("/", isAllowed(["admin", "superadmin"]), createMonitor);
|
||||
|
||||
router.put("/:monitorId", isAllowed(["admin", "superadmin"]), editMonitor);
|
||||
|
||||
@@ -36,10 +36,10 @@ class JobQueue {
|
||||
host: redisHost,
|
||||
port: redisPort,
|
||||
};
|
||||
this.connection = connection;
|
||||
this.queue = new Queue(QUEUE_NAME, {
|
||||
connection,
|
||||
});
|
||||
this.connection = connection;
|
||||
this.workers = [];
|
||||
this.db = null;
|
||||
this.networkService = null;
|
||||
@@ -406,9 +406,6 @@ class JobQueue {
|
||||
delayed: await this.queue.getDelayedCount(),
|
||||
repeatableJobs: (await this.queue.getRepeatableJobs()).length,
|
||||
};
|
||||
this.logger.info({
|
||||
message: metrics,
|
||||
});
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
@@ -424,17 +421,20 @@ class JobQueue {
|
||||
* @async
|
||||
* @returns {Promise<boolean>} - Returns true if obliteration is successful
|
||||
*/
|
||||
|
||||
async obliterate() {
|
||||
try {
|
||||
let metrics = await this.getMetrics();
|
||||
this.logger.info({ message: metrics });
|
||||
this.logger.info({ message: "Attempting to obliterate job queue..." });
|
||||
await this.queue.pause();
|
||||
const jobs = await this.getJobs();
|
||||
|
||||
// Remove all repeatable jobs
|
||||
for (const job of jobs) {
|
||||
await this.queue.removeRepeatableByKey(job.key);
|
||||
await this.queue.remove(job.id);
|
||||
}
|
||||
|
||||
// Close workers
|
||||
await Promise.all(
|
||||
this.workers.map(async (worker) => {
|
||||
await worker.close();
|
||||
@@ -442,9 +442,13 @@ class JobQueue {
|
||||
);
|
||||
|
||||
await this.queue.obliterate();
|
||||
metrics = await this.getMetrics();
|
||||
this.logger.info({ message: metrics });
|
||||
this.logger.info({ message: successMessages.JOB_QUEUE_OBLITERATE });
|
||||
const metrics = await this.getMetrics();
|
||||
this.logger.info({
|
||||
message: successMessages.JOB_QUEUE_OBLITERATE,
|
||||
service: SERVICE_NAME,
|
||||
method: "obliterate",
|
||||
details: metrics,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
getAllMonitors,
|
||||
getAllMonitorsWithUptimeStats,
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
@@ -61,6 +62,47 @@ describe("Monitor Controller - getAllMonitors", () => {
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
describe("Monitor Controller - getAllMonitorsWithUptimeStats", () => {
|
||||
let req, res, next;
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
params: {},
|
||||
query: {},
|
||||
body: {},
|
||||
db: {
|
||||
getAllMonitorsWithUptimeStats: sinon.stub(),
|
||||
},
|
||||
};
|
||||
res = {
|
||||
status: sinon.stub().returnsThis(),
|
||||
json: sinon.stub(),
|
||||
};
|
||||
next = sinon.stub();
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
it("should reject with an error if DB operations fail", async () => {
|
||||
req.db.getAllMonitorsWithUptimeStats.throws(new Error("DB error"));
|
||||
await getAllMonitorsWithUptimeStats(req, res, next);
|
||||
expect(next.firstCall.args[0]).to.be.an("error");
|
||||
expect(next.firstCall.args[0].message).to.equal("DB error");
|
||||
});
|
||||
|
||||
it("should return success message and data if all operations succeed", async () => {
|
||||
const data = [{ monitor: "data" }];
|
||||
req.db.getAllMonitorsWithUptimeStats.returns(data);
|
||||
await getAllMonitorsWithUptimeStats(req, res, next);
|
||||
expect(res.status.firstCall.args[0]).to.equal(200);
|
||||
expect(
|
||||
res.json.calledOnceWith({
|
||||
success: true,
|
||||
msg: successMessages.MONITOR_GET_ALL,
|
||||
data: data,
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Monitor Controller - getMonitorStatsById", () => {
|
||||
let req, res, next;
|
||||
|
||||
565
Server/tests/db/checkModule.test.js
Normal file
565
Server/tests/db/checkModule.test.js
Normal file
@@ -0,0 +1,565 @@
|
||||
import sinon from "sinon";
|
||||
import {
|
||||
createCheck,
|
||||
getChecksCount,
|
||||
getChecks,
|
||||
getTeamChecks,
|
||||
deleteChecks,
|
||||
deleteChecksByTeamId,
|
||||
updateChecksTTL,
|
||||
} from "../../db/mongo/modules/checkModule.js";
|
||||
import Check from "../../db/models/Check.js";
|
||||
import Monitor from "../../db/models/Monitor.js";
|
||||
import User from "../../db/models/User.js";
|
||||
import logger from "../../utils/logger.js";
|
||||
|
||||
describe("checkModule", () => {
|
||||
describe("createCheck", () => {
|
||||
let checkCountDocumentsStub, checkSaveStub, monitorFindByIdStub, monitorSaveStub;
|
||||
const mockMonitor = {
|
||||
_id: "123",
|
||||
uptimePercentage: 0.5,
|
||||
status: true,
|
||||
save: () => this,
|
||||
};
|
||||
const mockCheck = { active: true };
|
||||
beforeEach(() => {
|
||||
checkSaveStub = sinon.stub(Check.prototype, "save");
|
||||
checkCountDocumentsStub = sinon.stub(Check, "countDocuments");
|
||||
monitorFindByIdStub = sinon.stub(Monitor, "findById");
|
||||
monitorSaveStub = sinon.stub(Monitor.prototype, "save");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should return undefined early if no monitor is found", async () => {
|
||||
monitorFindByIdStub.returns(null);
|
||||
const check = await createCheck({ monitorId: "123" });
|
||||
expect(check).to.be.undefined;
|
||||
});
|
||||
|
||||
it("should return a check", async () => {
|
||||
monitorFindByIdStub.returns(mockMonitor);
|
||||
checkSaveStub.returns(mockCheck);
|
||||
monitorSaveStub.returns(mockMonitor);
|
||||
const check = await createCheck({ monitorId: "123", status: true });
|
||||
expect(check).to.deep.equal(mockCheck);
|
||||
});
|
||||
it("should return a check if status is down", async () => {
|
||||
mockMonitor.status = false;
|
||||
monitorFindByIdStub.returns(mockMonitor);
|
||||
checkSaveStub.returns(mockCheck);
|
||||
monitorSaveStub.returns(mockMonitor);
|
||||
const check = await createCheck({ monitorId: "123", status: false });
|
||||
expect(check).to.deep.equal(mockCheck);
|
||||
});
|
||||
it("should return a check if uptimePercentage is undefined", async () => {
|
||||
mockMonitor.uptimePercentage = undefined;
|
||||
monitorFindByIdStub.returns(mockMonitor);
|
||||
checkSaveStub.returns(mockCheck);
|
||||
monitorSaveStub.returns(mockMonitor);
|
||||
const check = await createCheck({ monitorId: "123", status: true });
|
||||
expect(check).to.deep.equal(mockCheck);
|
||||
});
|
||||
it("should return a check if uptimePercentage is undefined and status is down", async () => {
|
||||
mockMonitor.uptimePercentage = undefined;
|
||||
monitorFindByIdStub.returns(mockMonitor);
|
||||
checkSaveStub.returns(mockCheck);
|
||||
monitorSaveStub.returns(mockMonitor);
|
||||
const check = await createCheck({ monitorId: "123", status: false });
|
||||
expect(check).to.deep.equal(mockCheck);
|
||||
});
|
||||
it("should monitor save error", async () => {
|
||||
const err = new Error("Save Error");
|
||||
monitorSaveStub.throws(err);
|
||||
try {
|
||||
await createCheck({ monitorId: "123" });
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
it("should handle errors", async () => {
|
||||
const err = new Error("DB Error");
|
||||
checkCountDocumentsStub.throws(err);
|
||||
try {
|
||||
await createCheck({ monitorId: "123" });
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("getChecksCount", () => {
|
||||
let checkCountDocumentStub;
|
||||
|
||||
beforeEach(() => {
|
||||
checkCountDocumentStub = sinon.stub(Check, "countDocuments");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
checkCountDocumentStub.restore();
|
||||
});
|
||||
|
||||
it("should return count with basic monitorId query", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: {},
|
||||
};
|
||||
checkCountDocumentStub.resolves(5);
|
||||
|
||||
const result = await getChecksCount(req);
|
||||
|
||||
expect(result).to.equal(5);
|
||||
expect(checkCountDocumentStub.calledOnce).to.be.true;
|
||||
expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
|
||||
monitorId: "test123",
|
||||
});
|
||||
});
|
||||
|
||||
it("should include dateRange in query when provided", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { dateRange: "day" },
|
||||
};
|
||||
checkCountDocumentStub.resolves(3);
|
||||
|
||||
const result = await getChecksCount(req);
|
||||
|
||||
expect(result).to.equal(3);
|
||||
expect(checkCountDocumentStub.firstCall.args[0]).to.have.property("createdAt");
|
||||
expect(checkCountDocumentStub.firstCall.args[0].createdAt).to.have.property("$gte");
|
||||
});
|
||||
|
||||
it('should handle "all" filter correctly', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "all" },
|
||||
};
|
||||
checkCountDocumentStub.resolves(2);
|
||||
|
||||
const result = await getChecksCount(req);
|
||||
|
||||
expect(result).to.equal(2);
|
||||
expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
|
||||
monitorId: "test123",
|
||||
status: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle "down" filter correctly', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "down" },
|
||||
};
|
||||
checkCountDocumentStub.resolves(2);
|
||||
|
||||
const result = await getChecksCount(req);
|
||||
|
||||
expect(result).to.equal(2);
|
||||
expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
|
||||
monitorId: "test123",
|
||||
status: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle "resolve" filter correctly', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "resolve" },
|
||||
};
|
||||
checkCountDocumentStub.resolves(1);
|
||||
|
||||
const result = await getChecksCount(req);
|
||||
|
||||
expect(result).to.equal(1);
|
||||
expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
|
||||
monitorId: "test123",
|
||||
status: false,
|
||||
statusCode: 5000,
|
||||
});
|
||||
});
|
||||
it("should handle unknown filter correctly", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "unknown" },
|
||||
};
|
||||
checkCountDocumentStub.resolves(1);
|
||||
|
||||
const result = await getChecksCount(req);
|
||||
|
||||
expect(result).to.equal(1);
|
||||
expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({
|
||||
monitorId: "test123",
|
||||
status: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should combine dateRange and filter in query", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: {
|
||||
dateRange: "week",
|
||||
filter: "down",
|
||||
},
|
||||
};
|
||||
checkCountDocumentStub.resolves(4);
|
||||
|
||||
const result = await getChecksCount(req);
|
||||
|
||||
expect(result).to.equal(4);
|
||||
expect(checkCountDocumentStub.firstCall.args[0]).to.have.all.keys(
|
||||
"monitorId",
|
||||
"createdAt",
|
||||
"status"
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("getChecks", () => {
|
||||
let checkFindStub, monitorFindStub;
|
||||
|
||||
beforeEach(() => {
|
||||
checkFindStub = sinon.stub(Check, "find").returns({
|
||||
skip: sinon.stub().returns({
|
||||
limit: sinon.stub().returns({
|
||||
sort: sinon.stub().returns([{ id: 1 }, { id: 2 }]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should return checks with basic monitorId query", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await getChecks(req);
|
||||
|
||||
expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
it("should return checks with limit query", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { limit: 10 },
|
||||
};
|
||||
|
||||
const result = await getChecks(req);
|
||||
|
||||
expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
|
||||
it("should handle pagination correctly", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: {
|
||||
page: 2,
|
||||
rowsPerPage: 10,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await getChecks(req);
|
||||
|
||||
expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
|
||||
it("should handle dateRange filter", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { dateRange: "week" },
|
||||
};
|
||||
const result = await getChecks(req);
|
||||
|
||||
expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
|
||||
it('should handle "all" filter', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "all" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getChecks(req);
|
||||
expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
it('should handle "down" filter', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "down" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getChecks(req);
|
||||
expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
|
||||
it('should handle "resolve" filter', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "resolve" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getChecks(req);
|
||||
expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
it('should handle "unknown" filter', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "unknown" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getChecks(req);
|
||||
expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
|
||||
it("should handle ascending sort order", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { sortOrder: "asc" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getChecks(req);
|
||||
expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
|
||||
it("should handle error case", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: {},
|
||||
};
|
||||
|
||||
checkFindStub.throws(new Error("Database error"));
|
||||
|
||||
try {
|
||||
await getChecks(req);
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("Database error");
|
||||
expect(error.service).to.equal("checkModule");
|
||||
expect(error.method).to.equal("getChecks");
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("getTeamChecks", () => {
|
||||
let checkFindStub, checkCountDocumentsStub, monitorFindStub;
|
||||
const mockMonitors = [{ _id: "123" }];
|
||||
beforeEach(() => {
|
||||
monitorFindStub = sinon.stub(Monitor, "find").returns({
|
||||
select: sinon.stub().returns(mockMonitors),
|
||||
});
|
||||
checkCountDocumentsStub = sinon.stub(Check, "countDocuments").returns(2);
|
||||
checkFindStub = sinon.stub(Check, "find").returns({
|
||||
skip: sinon.stub().returns({
|
||||
limit: sinon.stub().returns({
|
||||
sort: sinon.stub().returns({
|
||||
select: sinon.stub().returns([{ id: 1 }, { id: 2 }]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should return checks with basic monitorId query", async () => {
|
||||
const req = {
|
||||
params: { teamId: "test123" },
|
||||
query: {},
|
||||
};
|
||||
|
||||
const result = await getTeamChecks(req);
|
||||
expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
|
||||
});
|
||||
|
||||
it("should handle pagination correctly", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { limit: 1, page: 2, rowsPerPage: 10 },
|
||||
};
|
||||
|
||||
const result = await getTeamChecks(req);
|
||||
expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
|
||||
});
|
||||
|
||||
it("should handle dateRange filter", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { dateRange: "week" },
|
||||
};
|
||||
const result = await getTeamChecks(req);
|
||||
expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
|
||||
});
|
||||
|
||||
it('should handle "all" filter', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "all" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getTeamChecks(req);
|
||||
expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
|
||||
});
|
||||
it('should handle "down" filter', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "down" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getTeamChecks(req);
|
||||
expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
|
||||
});
|
||||
|
||||
it('should handle "resolve" filter', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "resolve" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getTeamChecks(req);
|
||||
expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
|
||||
});
|
||||
it('should handle "unknown" filter', async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { filter: "unknown" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getTeamChecks(req);
|
||||
expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
|
||||
});
|
||||
|
||||
it("should handle ascending sort order", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: { sortOrder: "asc" },
|
||||
};
|
||||
|
||||
await getChecks(req);
|
||||
const result = await getTeamChecks(req);
|
||||
expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] });
|
||||
});
|
||||
|
||||
it("should handle error case", async () => {
|
||||
const req = {
|
||||
params: { monitorId: "test123" },
|
||||
query: {},
|
||||
};
|
||||
|
||||
checkFindStub.throws(new Error("Database error"));
|
||||
|
||||
try {
|
||||
await getTeamChecks(req);
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("Database error");
|
||||
expect(error.service).to.equal("checkModule");
|
||||
expect(error.method).to.equal("getTeamChecks");
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("deleteChecks", () => {
|
||||
let checkDeleteManyStub;
|
||||
beforeEach(() => {
|
||||
checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 });
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should return a value if a check is deleted", async () => {
|
||||
const result = await deleteChecks("123");
|
||||
expect(result).to.equal(1);
|
||||
});
|
||||
it("should handle an error", async () => {
|
||||
checkDeleteManyStub.throws(new Error("Database error"));
|
||||
try {
|
||||
await deleteChecks("123");
|
||||
} catch (error) {
|
||||
expect(error.message).to.equal("Database error");
|
||||
expect(error.method).to.equal("deleteChecks");
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("deleteChecksByTeamId", () => {
|
||||
let mockMonitors = [{ _id: 123, save: () => this }];
|
||||
let monitorFindStub, monitorSaveStub, checkDeleteManyStub;
|
||||
beforeEach(() => {
|
||||
monitorSaveStub = sinon.stub(Monitor.prototype, "save");
|
||||
monitorFindStub = sinon.stub(Monitor, "find").returns(mockMonitors);
|
||||
checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 });
|
||||
});
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
it("should return a deleted count", async () => {
|
||||
const result = await deleteChecksByTeamId("123");
|
||||
expect(result).to.equal(1);
|
||||
});
|
||||
it("should handle errors", async () => {
|
||||
const err = new Error("DB Error");
|
||||
monitorFindStub.throws(err);
|
||||
try {
|
||||
const result = await deleteChecksByTeamId("123");
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe("updateChecksTTL", () => {
|
||||
let userUpdateManyStub;
|
||||
let loggerStub;
|
||||
beforeEach(() => {
|
||||
loggerStub = sinon.stub(logger, "error");
|
||||
userUpdateManyStub = sinon.stub(User, "updateMany");
|
||||
Check.collection = { dropIndex: sinon.stub(), createIndex: sinon.stub() };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should return undefined", async () => {
|
||||
const result = await updateChecksTTL("123", 10);
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
|
||||
it("should log an error if dropIndex throws an error", async () => {
|
||||
const err = new Error("Drop Index Error");
|
||||
Check.collection.dropIndex.throws(err);
|
||||
await updateChecksTTL("123", 10);
|
||||
expect(loggerStub.calledOnce).to.be.true;
|
||||
expect(loggerStub.firstCall.args[0].message).to.equal(err.message);
|
||||
});
|
||||
|
||||
it("should throw an error if createIndex throws an error", async () => {
|
||||
const err = new Error("Create Index Error");
|
||||
Check.collection.createIndex.throws(err);
|
||||
try {
|
||||
await updateChecksTTL("123", 10);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
it("should throw an error if User.updateMany throws an error", async () => {
|
||||
const err = new Error("Update Many Error");
|
||||
userUpdateManyStub.throws(err);
|
||||
try {
|
||||
await updateChecksTTL("123", 10);
|
||||
} catch (error) {
|
||||
expect(error).to.deep.equal(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
1770
Server/tests/db/monitorModule.test.js
Normal file
1770
Server/tests/db/monitorModule.test.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -98,6 +98,7 @@ describe("StatusService", () => {
|
||||
expect(check.responseTime).to.equal(100);
|
||||
expect(check.message).to.equal("Test message");
|
||||
});
|
||||
|
||||
it("should build a check object for pagespeed type", () => {
|
||||
const check = statusService.buildCheck({
|
||||
monitorId: "test",
|
||||
@@ -193,6 +194,26 @@ describe("StatusService", () => {
|
||||
expect(check.disk).to.equal("disk");
|
||||
expect(check.host).to.equal("host");
|
||||
});
|
||||
it("should build a check for hardware type with missing data", () => {
|
||||
const check = statusService.buildCheck({
|
||||
monitorId: "test",
|
||||
type: "hardware",
|
||||
status: true,
|
||||
responseTime: 100,
|
||||
code: 200,
|
||||
message: "Test message",
|
||||
payload: {},
|
||||
});
|
||||
expect(check.monitorId).to.equal("test");
|
||||
expect(check.status).to.be.true;
|
||||
expect(check.statusCode).to.equal(200);
|
||||
expect(check.responseTime).to.equal(100);
|
||||
expect(check.message).to.equal("Test message");
|
||||
expect(check.cpu).to.deep.equal({});
|
||||
expect(check.memory).to.deep.equal({});
|
||||
expect(check.disk).to.deep.equal({});
|
||||
expect(check.host).to.deep.equal({});
|
||||
});
|
||||
});
|
||||
describe("insertCheck", () => {
|
||||
it("should log an error if one is thrown", async () => {
|
||||
|
||||
@@ -7,6 +7,10 @@ class Logger {
|
||||
if (message instanceof Object) {
|
||||
message = JSON.stringify(message, null, 2);
|
||||
}
|
||||
|
||||
if (details instanceof Object) {
|
||||
details = JSON.stringify(details, null, 2);
|
||||
}
|
||||
let msg = `${timestamp} ${level}:`;
|
||||
service && (msg += ` [${service}]`);
|
||||
method && (msg += `(${method})`);
|
||||
|
||||
Reference in New Issue
Block a user