import Monitor from "../../models/Monitor.js"; import MonitorStats from "../../models/MonitorStats.js"; import Check from "../../models/Check.js"; import PageSpeedCheck from "../../models/PageSpeedCheck.js"; import HardwareCheck from "../../models/HardwareCheck.js"; import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js"; import Notification from "../../models/Notification.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js"; import ServiceRegistry from "../../../service/serviceRegistry.js"; import StringService from "../../../service/stringService.js"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { ObjectId } from "mongodb"; import { buildUptimeDetailsPipeline, buildHardwareDetailsPipeline, buildMonitorStatsPipeline, buildMonitorSummaryByTeamIdPipeline, buildMonitorsByTeamIdPipeline, buildMonitorsAndSummaryByTeamIdPipeline, buildMonitorsWithChecksByTeamIdPipeline, buildFilteredMonitorsByTeamIdPipeline, buildDePINDetailsByDateRange, buildDePINLatestChecks, } from "./monitorModuleQueries.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json"); const demoMonitors = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8")); const SERVICE_NAME = "monitorModule"; const CHECK_MODEL_LOOKUP = { http: Check, ping: Check, docker: Check, port: Check, pagespeed: PageSpeedCheck, hardware: HardwareCheck, }; /** * Get all monitors * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise>} * @throws {Error} */ const getAllMonitors = async (req, res) => { try { const monitors = await Monitor.find(); return monitors; } catch (error) { error.service = SERVICE_NAME; error.method = "getAllMonitors"; throw error; } }; /** * Get all monitors with uptime stats for 1,7,30, and 90 days * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise>} * @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. * @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 = checks.length - 1; i >= 0; 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 ?? 0; }; /** * Helper function to get average response time * @param {Array} checks Array of check objects. * @returns {number} Timestamp of the most recent check. */ const getAverageResponseTime = (checks) => { if (!checks || checks.length === 0) { return 0; } 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 / validChecks.length; }; /** * Helper function to get percentage 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); }; /** * Get date range parameters * @param {string} dateRange - 'day' | 'week' | 'month' | 'all' * @returns {Object} Start and end dates */ const getDateRange = (dateRange) => { const startDates = { recent: new Date(new Date().setHours(new Date().getHours() - 2)), day: new Date(new Date().setDate(new Date().getDate() - 1)), week: new Date(new Date().setDate(new Date().getDate() - 7)), month: new Date(new Date().setMonth(new Date().getMonth() - 1)), all: new Date(0), }; return { start: startDates[dateRange], end: new Date(), }; }; /** * 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} All checks and date-ranged checks */ const getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => { const indexSpec = { monitorId: 1, createdAt: sortOrder, // This will be 1 or -1 }; const [checksAll, checksForDateRange] = await Promise.all([ model.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), model .find({ monitorId, createdAt: { $gte: dateRange.start, $lte: dateRange.end }, }) .hint(indexSpec) .lean(), ]); 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 uptime details by monitor ID * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise} * @throws {Error} */ const getUptimeDetailsById = async (req) => { const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { const { monitorId } = req.params; const { dateRange, normalize } = req.query; const dates = getDateRange(dateRange); const formatLookup = { recent: "%Y-%m-%dT%H:%M:00Z", day: "%Y-%m-%dT%H:00:00Z", week: "%Y-%m-%dT%H:00:00Z", month: "%Y-%m-%dT00:00:00Z", }; const dateString = formatLookup[dateRange]; const results = await Check.aggregate( buildUptimeDetailsPipeline(monitorId, dates, dateString) ); const monitorData = results[0]; monitorData.groupedUpChecks = NormalizeDataUptimeDetails( monitorData.groupedUpChecks, 10, 100 ); monitorData.groupedDownChecks = NormalizeDataUptimeDetails( monitorData.groupedDownChecks, 10, 100 ); const normalizedGroupChecks = NormalizeDataUptimeDetails( monitorData.groupedChecks, 10, 100 ); monitorData.groupedChecks = normalizedGroupChecks; const monitorStats = await MonitorStats.findOne({ monitorId }); return { monitorData, monitorStats }; } catch (error) { error.service = SERVICE_NAME; error.method = "getUptimeDetailsById"; throw error; } }; const getDistributedUptimeDetailsById = async (req) => { try { const { monitorId } = req?.params ?? {}; if (typeof monitorId === "undefined") { throw new Error(); } const monitor = await Monitor.findById(monitorId); if (monitor === null || monitor === undefined) { throw new Error(this.stringService.dbFindMonitorById(monitorId)); } const { dateRange, normalize } = req.query; const dates = getDateRange(dateRange); const formatLookup = { recent: "%Y-%m-%dT%H:%M:00Z", day: { $concat: [ { $dateToString: { format: "%Y-%m-%dT%H:", date: "$updatedAt" } }, { $cond: [{ $lt: [{ $minute: "$updatedAt" }, 30] }, "00:00Z", "30:00Z"], }, ], }, week: "%Y-%m-%dT%H:00:00Z", month: "%Y-%m-%dT00:00:00Z", }; const dateString = formatLookup[dateRange]; const monitorStatsResult = await MonitorStats.aggregate( buildMonitorStatsPipeline(monitor) ); const monitorStats = monitorStatsResult[0]; const dePINDetailsByDateRange = await DistributedUptimeCheck.aggregate( buildDePINDetailsByDateRange(monitor, dates, dateString) ); const latestChecks = await DistributedUptimeCheck.aggregate( buildDePINLatestChecks(monitor) ); const checkData = dePINDetailsByDateRange[0]; const normalizedGroupChecks = NormalizeDataUptimeDetails( checkData.groupedChecks, 10, 100 ); const data = { ...monitor.toObject(), latestChecks, totalChecks: monitorStats?.totalChecks, avgResponseTime: monitorStats?.avgResponseTime, uptimePercentage: monitorStats?.uptimePercentage, timeSinceLastCheck: monitorStats?.timeSinceLastCheck, uptBurnt: monitorStats?.uptBurnt, groupedChecks: normalizedGroupChecks, groupedMapChecks: checkData.groupedMapChecks, }; return data; } catch (error) { error.service = SERVICE_NAME; error.method = "getDistributedUptimeDetailsById"; throw error; } }; /** * Get stats by monitor ID * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise} * @throws {Error} */ const getMonitorStatsById = async (req) => { const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { const { monitorId } = req.params; // 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(stringService.getDbFindMonitorById(monitorId)); } // 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 ), }; if ( monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" ) { // HTTP/PING Specific stats monitorStats.periodAvgResponseTime = getAverageResponseTime(checksForDateRange); monitorStats.periodUptime = getUptimePercentage(checksForDateRange); const groupedChecks = groupChecksByTime(checksForDateRange, dateRange); monitorStats.aggregateData = Object.values(groupedChecks).map(calculateGroupStats); } return monitorStats; } catch (error) { error.service = SERVICE_NAME; error.method = "getMonitorStatsById"; throw error; } }; const getHardwareDetailsById = async (req) => { try { const { monitorId } = req.params; const { dateRange } = req.query; const monitor = await Monitor.findById(monitorId); const dates = getDateRange(dateRange); const formatLookup = { recent: "%Y-%m-%dT%H:%M:00Z", day: "%Y-%m-%dT%H:00:00Z", week: "%Y-%m-%dT%H:00:00Z", month: "%Y-%m-%dT00:00:00Z", }; const dateString = formatLookup[dateRange]; const hardwareStats = await HardwareCheck.aggregate( buildHardwareDetailsPipeline(monitor, dates, dateString) ); const monitorStats = { ...monitor.toObject(), stats: hardwareStats[0], }; return monitorStats; } catch (error) { error.service = SERVICE_NAME; error.method = "getHardwareDetailsById"; throw error; } }; /** * Get a monitor by ID * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise} * @throws {Error} */ const getMonitorById = async (monitorId) => { const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); try { const monitor = await Monitor.findById(monitorId); if (monitor === null || monitor === undefined) { const error = new Error(stringService.getDbFindMonitorById(monitorId)); error.status = 404; throw error; } // Get notifications const notifications = await Notification.find({ monitorId: monitorId, }); // Update monitor with notifications and save monitor.notifications = notifications; await monitor.save(); return monitor; } catch (error) { error.service = SERVICE_NAME; error.method = "getMonitorById"; throw error; } }; const getMonitorsByTeamId = async (req) => { let { limit, type, page, rowsPerPage, filter, field, order } = req.query; limit = parseInt(limit); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); if (field === undefined) { field = "name"; order = "asc"; } // Build match stage const matchStage = { teamId: ObjectId.createFromHexString(req.params.teamId) }; if (type !== undefined) { matchStage.type = Array.isArray(type) ? { $in: type } : type; } const summaryResult = await Monitor.aggregate( buildMonitorSummaryByTeamIdPipeline({ matchStage }) ); const summary = summaryResult[0]; const monitors = await Monitor.aggregate( buildMonitorsByTeamIdPipeline({ matchStage, field, order }) ); const filteredMonitors = await Monitor.aggregate( buildFilteredMonitorsByTeamIdPipeline({ matchStage, filter, page, rowsPerPage, field, order, limit, type, }) ); const normalizedFilteredMonitors = filteredMonitors.map((monitor) => { if (!monitor.checks) { return monitor; } monitor.checks = NormalizeData(monitor.checks, 10, 100); return monitor; }); return { summary, monitors, filteredMonitors: normalizedFilteredMonitors }; }; const getMonitorsAndSummaryByTeamId = async (req) => { try { const { type } = req.query; const teamId = ObjectId.createFromHexString(req.params.teamId); const matchStage = { teamId }; if (type !== undefined) { matchStage.type = Array.isArray(type) ? { $in: type } : type; } if (req.explain === true) { return Monitor.aggregate( buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }) ).explain("executionStats"); } const queryResult = await Monitor.aggregate( buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }) ); const { monitors, summary } = queryResult?.[0] ?? {}; return { monitors, summary }; } catch (error) { error.service = SERVICE_NAME; error.method = "getMonitorsAndSummaryByTeamId"; throw error; } }; const getMonitorsWithChecksByTeamId = async (req) => { try { let { limit, type, page, rowsPerPage, filter, field, order } = req.query; limit = parseInt(limit); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); if (field === undefined) { field = "name"; order = "asc"; } const teamId = ObjectId.createFromHexString(req.params.teamId); // Build match stage const matchStage = { teamId }; if (type !== undefined) { matchStage.type = Array.isArray(type) ? { $in: type } : type; } if (req.explain === true) { return Monitor.aggregate( buildMonitorsWithChecksByTeamIdPipeline({ matchStage, filter, page, rowsPerPage, field, order, limit, type, }) ).explain("executionStats"); } const queryResult = await Monitor.aggregate( buildMonitorsWithChecksByTeamIdPipeline({ matchStage, filter, page, rowsPerPage, field, order, limit, type, }) ); const monitors = queryResult[0]?.monitors; const count = queryResult[0]?.count; const normalizedFilteredMonitors = monitors.map((monitor) => { if (!monitor.checks) { return monitor; } monitor.checks = NormalizeData(monitor.checks, 10, 100); return monitor; }); return { count, monitors: normalizedFilteredMonitors }; } catch (error) { error.service = SERVICE_NAME; error.method = "getMonitorsWithChecksByTeamId"; throw error; } }; /** * Create a monitor * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise} * @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) { error.service = SERVICE_NAME; error.method = "createMonitor"; throw error; } }; /** * Create bulk monitors * @async * @param {Express.Request} req * @returns {Promise} * @throws {Error} */ const createBulkMonitors = async (req) => { try { const monitors = req.body.map( (item) => new Monitor({ ...item, notifications: undefined }) ); await Monitor.bulkSave(monitors); return monitors; } catch (error) { error.service = SERVICE_NAME; error.method = "createBulkMonitors"; throw error; } }; /** * Delete a monitor by ID * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise} * @throws {Error} */ const deleteMonitor = async (req, res) => { const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); const monitorId = req.params.monitorId; try { const monitor = await Monitor.findByIdAndDelete(monitorId); if (!monitor) { throw new Error(stringService.getDbFindMonitorById(monitorId)); } return monitor; } catch (error) { error.service = SERVICE_NAME; error.method = "deleteMonitor"; throw error; } }; /** * DELETE ALL MONITORS (TEMP) */ const deleteAllMonitors = async (teamId) => { try { const monitors = await Monitor.find({ teamId }); const { deletedCount } = await Monitor.deleteMany({ teamId }); return { monitors, deletedCount }; } catch (error) { error.service = SERVICE_NAME; error.method = "deleteAllMonitors"; 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) { error.service = SERVICE_NAME; error.method = "deleteMonitorsByUserId"; throw error; } }; /** * Edit a monitor by ID * @async * @param {Express.Request} req * @param {Express.Response} res * @returns {Promise} * @throws {Error} */ const editMonitor = async (candidateId, candidateMonitor) => { candidateMonitor.notifications = undefined; try { const editedMonitor = await Monitor.findByIdAndUpdate(candidateId, candidateMonitor, { new: true, }); return editedMonitor; } catch (error) { error.service = SERVICE_NAME; error.method = "editMonitor"; throw error; } }; const addDemoMonitors = async (userId, teamId) => { try { const demoMonitorsToInsert = demoMonitors.map((monitor) => { return { userId, teamId, name: monitor.name, description: monitor.name, type: "http", url: monitor.url, interval: 60000, }; }); const insertedMonitors = await Monitor.insertMany(demoMonitorsToInsert); return insertedMonitors; } catch (error) { error.service = SERVICE_NAME; error.method = "addDemoMonitors"; throw error; } }; export { getAllMonitors, getAllMonitorsWithUptimeStats, getMonitorStatsById, getMonitorById, getMonitorsByTeamId, getMonitorsAndSummaryByTeamId, getMonitorsWithChecksByTeamId, getUptimeDetailsById, getDistributedUptimeDetailsById, createMonitor, createBulkMonitors, deleteMonitor, deleteAllMonitors, deleteMonitorsByUserId, editMonitor, addDemoMonitors, getHardwareDetailsById, }; // Helper functions export { calculateUptimeDuration, getLastChecked, getLatestResponseTime, getAverageResponseTime, getUptimePercentage, getIncidents, getDateRange, getMonitorChecks, processChecksForDisplay, groupChecksByTime, calculateGroupStats, }; // limit 25 // page 1 // rowsPerPage 25 // filter undefined // field name // order asc // skip 25 // sort { name: 1 } // filteredMonitors [] // limit 25 // page NaN // rowsPerPage 25 // filter undefined // field name // order asc // skip 0 // sort { name: 1 }