Files
Checkmate/server/src/db/v1/modules/monitorModule.js
Alexander Holliday 95ad3e71ca Merge pull request #2987 from bluewave-labs/feat/v2/components
feat: move components to v1 and v2 dirs
2025-10-02 17:22:20 +05:30

579 lines
17 KiB
JavaScript
Executable File

import {
buildUptimeDetailsPipeline,
buildHardwareDetailsPipeline,
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
} from "./monitorModuleQueries.js";
const SERVICE_NAME = "monitorModule";
class MonitorModule {
constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
this.Monitor = Monitor;
this.MonitorStats = MonitorStats;
this.Check = Check;
this.stringService = stringService;
this.fs = fs;
this.path = path;
this.fileURLToPath = fileURLToPath;
this.ObjectId = ObjectId;
this.NormalizeData = NormalizeData;
this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
this.demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json");
}
// Helper
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
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);
};
getLatestResponseTime = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
return checks[0]?.responseTime ?? 0;
};
// Helper
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
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
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
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(),
};
};
//Helper
getMonitorChecks = async (monitorId, dateRange, sortOrder) => {
const indexSpec = {
monitorId: 1,
updatedAt: sortOrder, // This will be 1 or -1
};
const [checksAll, checksForDateRange] = await Promise.all([
this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
this.Check.find({
monitorId,
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
})
.hint(indexSpec)
.lean(),
]);
return { checksAll, checksForDateRange };
};
// Helper
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;
};
// Helper
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;
}, {});
};
// Helper
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: this.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,
};
};
getAllMonitors = async () => {
try {
const monitors = await this.Monitor.find();
return monitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllMonitors";
throw error;
}
};
getMonitorById = async (monitorId) => {
try {
const monitor = await this.Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
const error = new Error(this.stringService.getDbFindMonitorById(monitorId));
error.status = 404;
throw error;
}
return monitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorById";
throw error;
}
};
getMonitorsByIds = async (monitorIds) => {
try {
const objectIds = monitorIds.map((id) => new this.ObjectId(id));
return await this.Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean();
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsByIds";
throw error;
}
};
getUptimeDetailsById = async ({ monitorId, dateRange }) => {
try {
const dates = this.getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
week: "%Y-%m-%dT00:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString));
const monitorData = results[0];
monitorData.groupedUpChecks = this.NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100);
monitorData.groupedDownChecks = this.NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100);
const normalizedGroupChecks = this.NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100);
monitorData.groupedChecks = normalizedGroupChecks;
const monitorStats = await this.MonitorStats.findOne({ monitorId });
return { monitorData, monitorStats };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUptimeDetailsById";
throw error;
}
};
getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => {
try {
// Get monitor, if we can't find it, abort with error
const monitor = await this.Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
throw new Error(this.stringService.getDbFindMonitorById(monitorId));
}
// Get query params
const sort = sortOrder === "asc" ? 1 : -1;
// Get Checks for monitor in date range requested
const dates = this.getDateRange(dateRange);
const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort);
// Build monitor stats
const monitorStats = {
...monitor.toObject(),
uptimeDuration: this.calculateUptimeDuration(checksAll),
lastChecked: this.getLastChecked(checksAll),
latestResponseTime: this.getLatestResponseTime(checksAll),
periodIncidents: this.getIncidents(checksForDateRange),
periodTotalChecks: checksForDateRange.length,
checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize),
};
if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") {
// HTTP/PING Specific stats
monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange);
monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange);
const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange);
monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats);
}
return monitorStats;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorStatsById";
throw error;
}
};
getHardwareDetailsById = async ({ monitorId, dateRange }) => {
try {
const monitor = await this.Monitor.findById(monitorId);
const dates = this.getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
week: "%Y-%m-%dT00:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
const stats = hardwareStats[0];
return {
...monitor.toObject(),
stats,
};
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getHardwareDetailsById";
throw error;
}
};
getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => {
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build match stage
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage }));
const summary = summaryResult[0];
const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order }));
const filteredMonitors = await this.Monitor.aggregate(
buildFilteredMonitorsByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
);
const normalizedFilteredMonitors = filteredMonitors.map((monitor) => {
if (!monitor.checks) {
return monitor;
}
monitor.checks = this.NormalizeData(monitor.checks, 10, 100);
return monitor;
});
return { summary, monitors, filteredMonitors: normalizedFilteredMonitors };
};
getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => {
try {
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
if (explain === true) {
return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats");
}
const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }));
const { monitors, summary } = queryResult?.[0] ?? {};
return { monitors, summary };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsAndSummaryByTeamId";
throw error;
}
};
getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => {
try {
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build match stage
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
if (explain === true) {
return this.Monitor.aggregate(
buildMonitorsWithChecksByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
).explain("executionStats");
}
const queryResult = await this.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 = this.NormalizeData(monitor.checks, 10, 100);
return monitor;
});
return { count, monitors: normalizedFilteredMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsWithChecksByTeamId";
throw error;
}
};
createMonitor = async ({ body, teamId, userId }) => {
try {
const monitor = new this.Monitor({ ...body, teamId, userId });
const saved = await monitor.save();
return saved;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createMonitor";
throw error;
}
};
createBulkMonitors = async (req) => {
try {
const monitors = req.map((item) => new this.Monitor({ ...item, notifications: undefined }));
await this.Monitor.bulkSave(monitors);
return monitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createBulkMonitors";
throw error;
}
};
deleteMonitor = async ({ teamId, monitorId }) => {
try {
const deletedMonitor = await this.Monitor.findOneAndDelete({ _id: monitorId, teamId });
if (!deletedMonitor) {
throw new Error(this.stringService.getDbFindMonitorById(monitorId));
}
return deletedMonitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMonitor";
throw error;
}
};
deleteAllMonitors = async (teamId) => {
try {
const monitors = await this.Monitor.find({ teamId });
const { deletedCount } = await this.Monitor.deleteMany({ teamId });
return { monitors, deletedCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteAllMonitors";
throw error;
}
};
deleteMonitorsByUserId = async (userId) => {
try {
const result = await this.Monitor.deleteMany({ userId: userId });
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMonitorsByUserId";
throw error;
}
};
editMonitor = async ({ monitorId, body }) => {
try {
const editedMonitor = await this.Monitor.findByIdAndUpdate(monitorId, body, {
new: true,
});
return editedMonitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editMonitor";
throw error;
}
};
addDemoMonitors = async (userId, teamId) => {
try {
const demoMonitors = JSON.parse(this.fs.readFileSync(this.demoMonitorsPath, "utf8"));
const demoMonitorsToInsert = demoMonitors.map((monitor) => {
return {
userId,
teamId,
name: monitor.name,
description: monitor.name,
type: "http",
url: monitor.url,
interval: 60000,
};
});
const insertedMonitors = await this.Monitor.insertMany(demoMonitorsToInsert);
return insertedMonitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "addDemoMonitors";
throw error;
}
};
pauseMonitor = async ({ monitorId }) => {
try {
const monitor = await this.Monitor.findOneAndUpdate(
{ _id: monitorId },
[
{
$set: {
isActive: { $not: "$isActive" },
status: "$$REMOVE",
},
},
],
{ new: true }
);
return monitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "pauseMonitor";
throw error;
}
};
}
export default MonitorModule;