mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-01-14 13:49:40 -06:00
563 lines
15 KiB
JavaScript
563 lines
15 KiB
JavaScript
const Monitor = require("../../../models/Monitor");
|
|
const Check = require("../../../models/Check");
|
|
const PageSpeedCheck = require("../../../models/PageSpeedCheck");
|
|
const { errorMessages } = require("../../../utils/messages");
|
|
const Notification = require("../../../models/Notification");
|
|
const { NormalizeData } = require("../../../utils/dataUtils");
|
|
|
|
/**
|
|
* Get all monitors
|
|
* @async
|
|
* @param {Express.Request} req
|
|
* @param {Express.Response} res
|
|
* @returns {Promise<Array<Monitor>>}
|
|
* @throws {Error}
|
|
*/
|
|
const getAllMonitors = async (req, res) => {
|
|
try {
|
|
const monitors = await Monitor.find();
|
|
return monitors;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function to calculate uptime duration based on the most recent check.
|
|
* @param {Array} checks Array of check objects.
|
|
* @returns {number} Uptime duration in ms.
|
|
*/
|
|
const calculateUptimeDuration = (checks) => {
|
|
if (!checks || checks.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
const latestCheck = new Date(checks[0].createdAt);
|
|
let latestDownCheck = 0;
|
|
|
|
for (let i = 0; i < checks.length; i++) {
|
|
if (checks[i].status === false) {
|
|
latestDownCheck = new Date(checks[i].createdAt);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If no down check is found, uptime is from the last check to now
|
|
if (latestDownCheck === 0) {
|
|
return Date.now() - new Date(checks[checks.length - 1].createdAt);
|
|
}
|
|
|
|
// Otherwise the uptime is from the last check to the last down check
|
|
return latestCheck - latestDownCheck;
|
|
};
|
|
|
|
/**
|
|
* Helper function to get duration since last check
|
|
* @param {Array} checks Array of check objects.
|
|
* @returns {number} Timestamp of the most recent check.
|
|
*/
|
|
const getLastChecked = (checks) => {
|
|
if (!checks || checks.length === 0) {
|
|
return 0; // Handle case when no checks are available
|
|
}
|
|
// Data is sorted newest->oldest, so last check is the most recent
|
|
return new Date() - new Date(checks[0].createdAt);
|
|
};
|
|
|
|
/**
|
|
* Helper function to get latestResponseTime
|
|
* @param {Array} checks Array of check objects.
|
|
* @returns {number} Timestamp of the most recent check.
|
|
*/
|
|
const getLatestResponseTime = (checks) => {
|
|
if (!checks || checks.length === 0) {
|
|
return 0;
|
|
}
|
|
return checks[0].responseTime;
|
|
};
|
|
|
|
/**
|
|
* Helper function to get average 24h response time
|
|
* @param {Array} checks Array of check objects.
|
|
* @returns {number} Timestamp of the most recent check.
|
|
*/
|
|
const getAverageResponseTime24Hours = (checks) => {
|
|
if (!checks || checks.length === 0) {
|
|
return 0;
|
|
}
|
|
const aggResponseTime = checks.reduce((sum, check) => {
|
|
return sum + check.responseTime;
|
|
}, 0);
|
|
return aggResponseTime / checks.length;
|
|
};
|
|
|
|
/**
|
|
* Helper function to get precentage 24h uptime
|
|
* @param {Array} checks Array of check objects.
|
|
* @returns {number} Timestamp of the most recent check.
|
|
*/
|
|
|
|
const getUptimePercentage = (checks) => {
|
|
if (!checks || checks.length === 0) {
|
|
return 0;
|
|
}
|
|
const upCount = checks.reduce((count, check) => {
|
|
return check.status === true ? count + 1 : count;
|
|
}, 0);
|
|
return (upCount / checks.length) * 100;
|
|
};
|
|
|
|
/**
|
|
* Helper function to get all incidents
|
|
* @param {Array} checks Array of check objects.
|
|
* @returns {number} Timestamp of the most recent check.
|
|
*/
|
|
|
|
const getIncidents = (checks) => {
|
|
if (!checks || checks.length === 0) {
|
|
return 0; // Handle case when no checks are available
|
|
}
|
|
return checks.reduce((acc, check) => {
|
|
return check.status === false ? (acc += 1) : acc;
|
|
}, 0);
|
|
};
|
|
/**
|
|
* Helper function to get all incidents
|
|
* @param {Array} checks Array of check objects.
|
|
* @returns {Array<Boolean>} Array of booleans representing up/down.
|
|
*/
|
|
const getStatusBarValues = (monitor, checks) => {
|
|
const checksIn60Mins = Math.floor((60 * 60 * 1000) / monitor.interval);
|
|
const noBlankChecks = checksIn60Mins - checks.length;
|
|
|
|
const statusBarValues = checks.map((check) => {
|
|
return {
|
|
status: check.status,
|
|
responseTime: check.responseTime,
|
|
value: 75,
|
|
};
|
|
});
|
|
|
|
for (let i = 0; i < noBlankChecks; i++) {
|
|
statusBarValues.push({ status: undefined, responseTime: 0, value: 75 });
|
|
}
|
|
return statusBarValues.reverse();
|
|
};
|
|
|
|
/**
|
|
* Get aggregate monitor stats for charts
|
|
* @async
|
|
* @param {Express.Request} req
|
|
* @param {Express.Response} res
|
|
* @returns {Promise<Monitor>}
|
|
* @throws {Error}
|
|
*/
|
|
const getMonitorAggregateStats = async (monitorId, 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)),
|
|
};
|
|
|
|
try {
|
|
const startDate = startDates[dateRange];
|
|
if (!startDate) {
|
|
throw new Error("Invalid date range specified");
|
|
}
|
|
const endDate = new Date();
|
|
|
|
const checks = await Check.find({
|
|
monitorId: monitorId,
|
|
createdAt: {
|
|
$gte: startDate,
|
|
$lte: endDate,
|
|
},
|
|
});
|
|
|
|
let groupedChecks;
|
|
// Group checks by hour if range is day
|
|
if (dateRange === "day") {
|
|
groupedChecks = checks.reduce((acc, check) => {
|
|
const hour = new Date(check.createdAt).getHours();
|
|
if (!acc[hour]) {
|
|
acc[hour] = { hour, checks: [] };
|
|
}
|
|
acc[hour].checks.push(check);
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
// Group checks by day if range is week or month
|
|
else {
|
|
groupedChecks = checks.reduce((acc, check) => {
|
|
const day = new Date(check.createdAt).toISOString().split("T")[0]; // Extract the date part
|
|
if (!acc[day]) {
|
|
acc[day] = { day, checks: [] };
|
|
}
|
|
acc[day].checks.push(check);
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
// Map grouped checks to stats
|
|
stats = Object.values(groupedChecks).map((group) => {
|
|
const totalChecks = group.checks.length;
|
|
const totalIncidents = group.checks.filter(
|
|
(check) => check.status === false
|
|
).length;
|
|
const avgResponseTime =
|
|
group.checks.reduce((sum, check) => sum + check.responseTime, 0) /
|
|
totalChecks;
|
|
|
|
return {
|
|
hour: group.hour,
|
|
day: group.day,
|
|
totalChecks,
|
|
totalIncidents,
|
|
avgResponseTime,
|
|
};
|
|
});
|
|
|
|
return stats;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get stats by monitor ID
|
|
* @async
|
|
* @param {Express.Request} req
|
|
* @param {Express.Response} res
|
|
* @returns {Promise<Monitor>}
|
|
* @throws {Error}
|
|
*/
|
|
const getMonitorStatsById = async (req) => {
|
|
const filterLookup = {
|
|
hour: new Date(new Date().getTime() - 60 * 60 * 1000),
|
|
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)),
|
|
};
|
|
|
|
try {
|
|
const { monitorId } = req.params;
|
|
let { status, limit, sortOrder, dateRange, numToDisplay, normalize } =
|
|
req.query;
|
|
|
|
// This effectively removes limit, returning all checks
|
|
if (limit === undefined) limit = 0;
|
|
|
|
// Default sort order is newest -> oldest
|
|
sortOrder = sortOrder === "asc" ? 1 : -1;
|
|
|
|
// Get monitor
|
|
const monitor = await Monitor.findById(monitorId);
|
|
if (monitor === null || monitor === undefined) {
|
|
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
|
|
}
|
|
|
|
// Determine if this is a pagespeed monitor or an http/ping monitor
|
|
let model =
|
|
monitor.type === "http" || monitor.type === "ping"
|
|
? Check
|
|
: PageSpeedCheck;
|
|
|
|
// Build monitor stats object
|
|
const monitorStats = {
|
|
...monitor.toObject(),
|
|
};
|
|
|
|
// Start building query
|
|
const checksQuery = { monitorId: monitor._id };
|
|
|
|
// Get all checks
|
|
const checksAll = await model.find(checksQuery).sort({
|
|
createdAt: sortOrder,
|
|
});
|
|
|
|
if (monitor.type === "http" || monitor.type === "ping") {
|
|
const checksQuery24Hours = {
|
|
...checksQuery,
|
|
createdAt: { $gte: filterLookup.day },
|
|
};
|
|
const checksQuery7Days = {
|
|
...checksQuery,
|
|
createdAt: { $gte: filterLookup.week },
|
|
};
|
|
const checksQuery30Days = {
|
|
...checksQuery,
|
|
createdAt: { $gte: filterLookup.month },
|
|
};
|
|
const checksQuery60Mins = {
|
|
...checksQuery,
|
|
createdAt: { $gte: filterLookup.hour },
|
|
};
|
|
|
|
const [checks24Hours, checks7Days, checks30Days, checks60Mins] =
|
|
await Promise.all([
|
|
model.find(checksQuery24Hours).sort({ createdAt: sortOrder }),
|
|
model.find(checksQuery7Days).sort({ createdAt: sortOrder }),
|
|
model.find(checksQuery30Days).sort({ createdAt: sortOrder }),
|
|
model.find(checksQuery60Mins).sort({ createdAt: sortOrder }),
|
|
]);
|
|
|
|
// HTTP/PING Specific stats
|
|
monitorStats.avgResponseTime24hours =
|
|
getAverageResponseTime24Hours(checks24Hours);
|
|
monitorStats.uptime7Days = getUptimePercentage(checks7Days);
|
|
monitorStats.uptime24Hours = getUptimePercentage(checks24Hours);
|
|
monitorStats.uptime30Days = getUptimePercentage(checks30Days);
|
|
monitorStats.statusBar = getStatusBarValues(monitor, checks60Mins);
|
|
}
|
|
|
|
//Get checks for dateRange
|
|
if (status !== undefined) {
|
|
checksQuery.status = status;
|
|
}
|
|
|
|
// Filter checks by "day", "week", or "month"
|
|
if (dateRange !== undefined) {
|
|
checksQuery.createdAt = { $gte: filterLookup[dateRange] };
|
|
}
|
|
|
|
let dateRangeChecks = await model
|
|
.find(checksQuery)
|
|
.sort({
|
|
createdAt: sortOrder,
|
|
})
|
|
.limit(limit);
|
|
|
|
const incidents = getIncidents(dateRangeChecks);
|
|
|
|
// If more than numToDisplay checks, pick every nth check
|
|
if (
|
|
numToDisplay !== undefined &&
|
|
dateRangeChecks &&
|
|
dateRangeChecks.length > numToDisplay
|
|
) {
|
|
const n = Math.ceil(dateRangeChecks.length / numToDisplay);
|
|
dateRangeChecks = dateRangeChecks.filter((_, index) => index % n === 0);
|
|
}
|
|
|
|
// Normalize checks if requested
|
|
if (normalize !== undefined) {
|
|
dateRangeChecks = NormalizeData(dateRangeChecks, 1, 100);
|
|
}
|
|
|
|
// Add common stats and stats that depend on the dateRange
|
|
monitorStats.uptimeDuration = calculateUptimeDuration(checksAll);
|
|
monitorStats.lastChecked = getLastChecked(checksAll);
|
|
monitorStats.latestResponseTime = getLatestResponseTime(checksAll);
|
|
monitorStats.incidents = incidents;
|
|
monitorStats.checks = dateRangeChecks;
|
|
return monitorStats;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get a monitor by ID
|
|
* @async
|
|
* @param {Express.Request} req
|
|
* @param {Express.Response} res
|
|
* @returns {Promise<Monitor>}
|
|
* @throws {Error}
|
|
*/
|
|
const getMonitorById = async (monitorId) => {
|
|
try {
|
|
const monitor = await Monitor.findById(monitorId);
|
|
if (monitor === null || monitor === undefined) {
|
|
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
|
|
}
|
|
// Get notifications
|
|
const notifications = await Notification.find({
|
|
monitorId: monitorId,
|
|
});
|
|
monitor.notifications = notifications;
|
|
const monitorWithNotifications = await monitor.save();
|
|
return monitorWithNotifications;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get monitors by TeamID
|
|
* @async
|
|
* @param {Express.Request} req
|
|
* @param {Express.Response} res
|
|
* @returns {Promise<Array<Monitor>>}
|
|
* @throws {Error}
|
|
*/
|
|
const getMonitorsByTeamId = async (req, res) => {
|
|
try {
|
|
let { limit, type, status, sortOrder, normalize, page, rowsPerPage } =
|
|
req.query || {};
|
|
const monitorQuery = { teamId: req.params.teamId };
|
|
const monitorsCount = await Monitor.countDocuments(monitorQuery);
|
|
|
|
// Pagination
|
|
let skip = 0;
|
|
if (page && rowsPerPage) {
|
|
skip = page * rowsPerPage;
|
|
}
|
|
|
|
if (type !== undefined) {
|
|
const types = Array.isArray(type) ? type : [type];
|
|
monitorQuery.type = { $in: types };
|
|
}
|
|
|
|
// Default sort order is newest -> oldest
|
|
if (sortOrder === "asc") {
|
|
sortOrder = 1;
|
|
} else if (sortOrder === "desc") {
|
|
sortOrder = -1;
|
|
} else sortOrder = -1;
|
|
|
|
// This effectively removes limit, returning all checks
|
|
if (limit === undefined) limit = 0;
|
|
|
|
const monitors = await Monitor.find(monitorQuery)
|
|
.skip(skip)
|
|
.limit(rowsPerPage);
|
|
// 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 =
|
|
monitor.type === "http" || monitor.type === "ping"
|
|
? Check
|
|
: PageSpeedCheck;
|
|
|
|
// Checks are order newest -> oldest
|
|
let checks = await model
|
|
.find(checksQuery)
|
|
.sort({
|
|
createdAt: sortOrder,
|
|
})
|
|
.limit(limit);
|
|
|
|
//Normalize checks if requested
|
|
if (normalize !== undefined) {
|
|
checks = NormalizeData(checks, 10, 100);
|
|
}
|
|
return { ...monitor.toObject(), checks };
|
|
})
|
|
);
|
|
return { monitors: monitorsWithChecks, monitorCount: monitorsCount };
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create a monitor
|
|
* @async
|
|
* @param {Express.Request} req
|
|
* @param {Express.Response} res
|
|
* @returns {Promise<Monitor>}
|
|
* @throws {Error}
|
|
*/
|
|
const createMonitor = async (req, res) => {
|
|
try {
|
|
const monitor = new Monitor({ ...req.body });
|
|
// Remove notifications fom monitor as they aren't needed here
|
|
monitor.notifications = undefined;
|
|
await monitor.save();
|
|
return monitor;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Delete a monitor by ID
|
|
* @async
|
|
* @param {Express.Request} req
|
|
* @param {Express.Response} res
|
|
* @returns {Promise<Monitor>}
|
|
* @throws {Error}
|
|
*/
|
|
const deleteMonitor = async (req, res) => {
|
|
const monitorId = req.params.monitorId;
|
|
try {
|
|
const monitor = await Monitor.findByIdAndDelete(monitorId);
|
|
if (!monitor) {
|
|
throw new Error(errorMessages.DB_FIND_MONTIOR_BY_ID(monitorId));
|
|
}
|
|
return monitor;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* DELETE ALL MONITORS (TEMP)
|
|
*/
|
|
|
|
const deleteAllMonitors = async (req, res) => {
|
|
try {
|
|
const deletedCount = await Monitor.deleteMany({});
|
|
return deletedCount.deletedCount;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Delete all monitors associated with a user ID
|
|
* @async
|
|
* @param {string} userId - The ID of the user whose monitors are to be deleted.
|
|
* @returns {Promise} A promise that resolves when the operation is complete.
|
|
*/
|
|
const deleteMonitorsByUserId = async (userId) => {
|
|
try {
|
|
const result = await Monitor.deleteMany({ userId: userId });
|
|
return result;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edit a monitor by ID
|
|
* @async
|
|
* @param {Express.Request} req
|
|
* @param {Express.Response} res
|
|
* @returns {Promise<Monitor>}
|
|
* @throws {Error}
|
|
*/
|
|
const editMonitor = async (candidateId, candidateMonitor) => {
|
|
candidateMonitor.notifications = undefined;
|
|
|
|
try {
|
|
const editedMonitor = await Monitor.findByIdAndUpdate(
|
|
candidateId,
|
|
candidateMonitor,
|
|
{ new: true }
|
|
);
|
|
return editedMonitor;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getAllMonitors,
|
|
getMonitorAggregateStats,
|
|
getMonitorStatsById,
|
|
getMonitorById,
|
|
getMonitorsByTeamId,
|
|
createMonitor,
|
|
deleteMonitor,
|
|
deleteAllMonitors,
|
|
deleteMonitorsByUserId,
|
|
editMonitor,
|
|
};
|