Resolved merge conflict in Server/index.js

This commit is contained in:
Skorpios committed 2025-02-08 16:39:01 -08:00
commit e9bcf66b29
99 files changed
+5354 -358

No files matched your search

@@ -1,16 +1,219 @@
import { handleError } from "./controllerUtils.js";
import { successMessages } from "../utils/messages.js";
import Monitor from "../db/models/Monitor.js";
import DistributedUptimeCheck from "../db/models/DistributedUptimeCheck.js";
const SERVICE_NAME = "DistributedUptimeQueueController";
class DistributedUptimeController {
constructor() {}
constructor(db, http, statusService) {
this.db = db;
this.http = http;
this.statusService = statusService;
this.resultsCallback = this.resultsCallback.bind(this);
this.getDistributedUptimeMonitors = this.getDistributedUptimeMonitors.bind(this);
this.getDistributedUptimeMonitorDetails =
this.getDistributedUptimeMonitorDetails.bind(this);
}
async resultsCallback(req, res, next) {
try {
const { id, result } = req.body;
// Calculate response time
const {
first_byte_took,
body_read_took,
dns_took,
conn_took,
connect_took,
tls_took,
status_code,
error,
} = result;
// Calculate response time
const responseTime =
(first_byte_took +
body_read_took +
dns_took +
conn_took +
connect_took +
tls_took) /
1_000_000;
// Calculate if server is up or down
const isErrorStatus = status_code >= 400;
const hasError = error !== "";
const status = isErrorStatus || hasError ? false : true;
// Build response
const distributedUptimeResponse = {
monitorId: id,
type: "distributed_http",
payload: result,
status,
code: status_code,
responseTime,
first_byte_took,
body_read_took,
dns_took,
conn_took,
connect_took,
tls_took,
};
if (error) {
const code = status_code || this.NETWORK_ERROR;
distributedUptimeResponse.code = code;
distributedUptimeResponse.message =
this.http.STATUS_CODES[code] || "Network Error";
} else {
distributedUptimeResponse.message = this.http.STATUS_CODES[status_code];
}
await this.statusService.updateStatus(distributedUptimeResponse);
res.status(200).json({ message: "OK" });
} catch (error) {
throw handleError(error, SERVICE_NAME, "resultsCallback");
next(handleError(error, SERVICE_NAME, "resultsCallback"));
}
}
async getDistributedUptimeMonitors(req, res, next) {
try {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("Access-Control-Allow-Origin", "*");
const BATCH_DELAY = 1000;
let batchTimeout = null;
let opInProgress = false;
// Do things here
const notifyChange = async () => {
if (opInProgress) {
// Get data
const monitors = await this.db.getMonitorsByTeamId(req);
res.write(`data: ${JSON.stringify({ monitors })}\n\n`);
opInProgress = false;
}
batchTimeout = null;
};
const handleChange = () => {
opInProgress = true;
if (batchTimeout) clearTimeout(batchTimeout);
batchTimeout = setTimeout(notifyChange, BATCH_DELAY);
};
const monitorStream = Monitor.watch(
[{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
{ fullDocument: "updateLookup" }
);
const checksStream = DistributedUptimeCheck.watch(
[{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
{ fullDocument: "updateLookup" }
);
monitorStream.on("change", handleChange);
checksStream.on("change", handleChange);
// Send initial data
const monitors = await this.db.getMonitorsByTeamId(req);
res.write(`data: ${JSON.stringify({ monitors })}\n\n`);
// Handle client disconnect
req.on("close", () => {
if (batchTimeout) {
clearTimeout(batchTimeout);
}
monitorStream.close();
checksStream.close();
clearInterval(keepAlive);
});
// Keep connection alive
const keepAlive = setInterval(() => {
res.write(": keepalive\n\n");
}, 30000);
// Clean up on close
req.on("close", () => {
clearInterval(keepAlive);
});
} catch (error) {
console.log(error);
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitors"));
}
}
async getDistributedUptimeMonitorDetails(req, res, next) {
try {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("Access-Control-Allow-Origin", "*");
const BATCH_DELAY = 1000;
let batchTimeout = null;
let opInProgress = false;
// Do things here
const notifyChange = async () => {
try {
if (opInProgress) {
// Get data
const monitor = await this.db.getDistributedUptimeDetailsById(req);
res.write(`data: ${JSON.stringify({ monitor })}\n\n`);
opInProgress = false;
}
batchTimeout = null;
} catch (error) {
console.error("Error in notifyChange:", error);
opInProgress = false;
batchTimeout = null;
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
}
};
const handleChange = () => {
opInProgress = true;
if (batchTimeout) clearTimeout(batchTimeout);
batchTimeout = setTimeout(notifyChange, BATCH_DELAY);
};
const checksStream = DistributedUptimeCheck.watch(
[{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
{ fullDocument: "updateLookup" }
);
checksStream.on("change", handleChange);
// Send initial data
const monitor = await this.db.getDistributedUptimeDetailsById(req);
res.write(`data: ${JSON.stringify({ monitor })}\n\n`);
// Handle client disconnect
req.on("close", () => {
if (batchTimeout) {
clearTimeout(batchTimeout);
}
checksStream.close();
clearInterval(keepAlive);
});
// Keep connection alive
const keepAlive = setInterval(() => {
res.write(": keepalive\n\n");
}, 30000);
// Clean up on close
req.on("close", () => {
clearInterval(keepAlive);
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
}
}
}
+37 -6
View File
@@ -2,8 +2,9 @@ import { handleError, handleValidationError } from "./controllerUtils.js";
import {
createStatusPageBodyValidation,
getStatusPageParamValidation,
imageValidation,
} from "../validation/joi.js";
import { successMessages } from "../utils/messages.js";
import { successMessages, errorMessages } from "../utils/messages.js";
const SERVICE_NAME = "statusPageController";
@@ -15,13 +16,14 @@ class StatusPageController {
createStatusPage = async (req, res, next) => {
try {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const statusPage = await this.db.createStatusPage(req.body);
const statusPage = await this.db.createStatusPage(req.body, req.file);
return res.success({
msg: successMessages.STATUS_PAGE_CREATE,
data: statusPage,
@@ -30,17 +32,35 @@ class StatusPageController {
next(handleError(error, SERVICE_NAME, "createStatusPage"));
}
};
getStatusPageByUrl = async (req, res, next) => {
updateStatusPage = async (req, res, next) => {
try {
await getStatusPageParamValidation.validateAsync(req.params);
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { url } = req.params;
const statusPage = await this.db.getStatusPageByUrl(url);
const statusPage = await this.db.updateStatusPage(req.body, req.file);
if (statusPage === null) {
const error = new Error(errorMessages.STATUS_PAGE_NOT_FOUND);
error.status = 404;
throw error;
}
return res.success({
msg: successMessages.STATUS_PAGE_UPDATE,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateStatusPage"));
}
};
getStatusPage = async (req, res, next) => {
try {
const statusPage = await this.db.getStatusPage();
return res.success({
msg: successMessages.STATUS_PAGE_BY_URL,
data: statusPage,
@@ -49,6 +69,17 @@ class StatusPageController {
next(handleError(error, SERVICE_NAME, "getStatusPage"));
}
};
deleteStatusPage = async (req, res, next) => {
try {
await this.db.deleteStatusPage();
return res.success({
msg: successMessages.STATUS_PAGE_DELETE,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteStatusPage"));
}
};
}
export default StatusPageController;
+108
View File
@@ -1,13 +1,121 @@
import mongoose from "mongoose";
import { BaseCheckSchema } from "./Check.js";
// {
// "id": "12123",
// "result": {
// "task_arrived": "2025-01-13T19:21:37.463466602Z",
// "dns_start": "2025-01-14T00:21:33.1801319+05:00",
// "dns_end": "2025-01-14T00:21:33.4582552+05:00",
// "conn_start": "2025-01-14T00:21:33.1801319+05:00",
// "conn_end": "2025-01-14T00:21:33.7076318+05:00",
// "connect_start": "2025-01-14T00:21:33.4582552+05:00",
// "connect_end": "2025-01-14T00:21:33.541899+05:00",
// "tls_hand_shake_start": "2025-01-14T00:21:33.541899+05:00",
// "tls_hand_shake_end": "2025-01-14T00:21:33.7076318+05:00",
// "body_read_start": "2025-01-14T00:21:34.1894707+05:00",
// "body_read_end": "2025-01-14T00:21:34.1894707+05:00",
// "wrote_request": "2025-01-14T00:21:33.7076318+05:00",
// "got_first_response_byte": "2025-01-14T00:21:34.1327652+05:00",
// "first_byte_took": 425133400,
// "body_read_took": 56030000,
// "dns_took": 278123300,
// "conn_took": 527499900,
// "connect_took": 83643800,
// "tls_took": 165732800,
// "sni_name": "uprock.com",
// "status_code": 200,
// "body_size": 19320,
// "request_header_size": 95,
// "response_header_size": 246,
// "response_headers": "X-Vercel-Id: bom1::iad1::sm87v-1736796096856-aec270c01f23\nDate: Mon, 13 Jan 2025 19:21:37 GMT\nServer: Vercel\nStrict-Transport-Security: max-age=63072000\nVary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url\nX-Matched-Path: /\nX-Powered-By: Next.js\nX-Vercel-Cache: MISS\nAge: 0\nCache-Control: private, no-cache, no-store, max-age=0, must-revalidate\nContent-Type: text/html; charset=utf-8",
// "error": "",
// "device_id": "d5f578e143a2cd603dd6bf5f846a86a538bde4a8fbe2ad1fca284ad9f033daf8",
// "ip_address": "223.123.19.0",
// "proof": "",
// "created_at": "2025-01-13T19:21:37.463466912Z",
// "continent": "AS",
// "country_code": "PK",
// "city": "Muzaffargarh",
// "upt_burnt" : "0.01",
// "location": {
// "lat": 71.0968,
// "lng": 30.0208
// },
// "payload": {
// "callback": "https://webhook.site/2a15b0af-545a-4ac2-b913-153b97592d7a",
// "x": "y"
// }
// }
// }
const LocationSchema = new mongoose.Schema(
{
lat: { type: Number, required: true },
lng: { type: Number, required: true },
},
{ _id: false }
);
const DistributedUptimeCheckSchema = mongoose.Schema(
{
...BaseCheckSchema.obj,
first_byte_took: {
type: Number,
required: false,
},
body_read_took: {
type: Number,
required: false,
},
dns_took: {
type: Number,
required: false,
},
conn_took: {
type: Number,
required: false,
},
connect_took: {
type: Number,
required: false,
},
tls_took: {
type: Number,
required: false,
},
location: {
type: LocationSchema,
required: false,
},
continent: {
type: String,
required: false,
},
countryCode: {
type: String,
required: false,
},
city: {
type: String,
required: false,
},
uptBurnt: {
type: mongoose.Schema.Types.Decimal128,
required: false,
},
},
{ timestamps: true }
);
DistributedUptimeCheckSchema.pre("save", function (next) {
if (this.isModified("uptBurnt") && typeof this.uptBurnt === "string") {
this.uptBurnt = mongoose.Types.Decimal128.fromString(this.uptBurnt);
}
next();
});
DistributedUptimeCheckSchema.index({ createdAt: 1 });
DistributedUptimeCheckSchema.index({ monitorId: 1, createdAt: 1 });
DistributedUptimeCheckSchema.index({ monitorId: 1, createdAt: -1 });
+16 -5
View File
@@ -23,11 +23,6 @@ const StatusPageSchema = mongoose.Schema(
required: true,
default: "#4169E1",
},
theme: {
type: String,
required: true,
default: "light",
},
monitors: [
{
type: mongoose.Schema.Types.ObjectId,
@@ -35,6 +30,22 @@ const StatusPageSchema = mongoose.Schema(
required: true,
},
],
logo: {
data: Buffer,
contentType: String,
},
isPublished: {
type: Boolean,
default: false,
},
showCharts: {
type: Boolean,
default: true,
},
showUptimePercentage: {
type: Boolean,
default: true,
},
},
{ timestamps: true }
);
+6
View File
@@ -43,6 +43,11 @@ import * as hardwareCheckModule from "./modules/hardwareCheckModule.js";
import * as checkModule from "./modules/checkModule.js";
//****************************************
// Distributed Checks
//****************************************
import * as distributedCheckModule from "./modules/distributedCheckModule.js";
//****************************************
// Maintenance Window
//****************************************
@@ -74,6 +79,7 @@ class MongoDB {
Object.assign(this, pageSpeedCheckModule);
Object.assign(this, hardwareCheckModule);
Object.assign(this, checkModule);
Object.assign(this, distributedCheckModule);
Object.assign(this, maintenanceWindowModule);
Object.assign(this, notificationModule);
Object.assign(this, settingsModule);
@@ -0,0 +1,15 @@
import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
const SERVICE_NAME = "distributedCheckModule";
const createDistributedCheck = async (checkData) => {
try {
const check = await new DistributedUptimeCheck({ ...checkData }).save();
return check;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createCheck";
throw error;
}
};
export { createDistributedCheck };
+69
View File
@@ -2,6 +2,7 @@ import Monitor from "../../models/Monitor.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 { errorMessages } from "../../../utils/messages.js";
import Notification from "../../models/Notification.js";
import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js";
@@ -11,6 +12,7 @@ import { fileURLToPath } from "url";
import {
buildUptimeDetailsPipeline,
buildHardwareDetailsPipeline,
buildDistributedUptimeDetailsPipeline,
} from "./monitorModuleQueries.js";
import { ObjectId } from "mongodb";
const __filename = fileURLToPath(import.meta.url);
@@ -366,6 +368,48 @@ const getUptimeDetailsById = async (req) => {
}
};
const getDistributedUptimeDetailsById = async (req) => {
try {
const { monitorId } = req.params;
const monitor = await Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
throw new Error(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId));
}
const { dateRange, normalize } = req.query;
const dates = getDateRange(dateRange);
const formatLookup = {
day: "%Y-%m-%dT%H:%M:00Z",
week: "%Y-%m-%dT%H:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const results = await DistributedUptimeCheck.aggregate(
buildDistributedUptimeDetailsPipeline(monitor, dates, dateString)
);
const monitorData = results[0];
const normalizedGroupChecks = NormalizeDataUptimeDetails(
monitorData.groupedChecks,
10,
100
);
const monitorStats = {
...monitor.toObject(),
...monitorData,
groupedChecks: normalizedGroupChecks,
};
return monitorStats;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getDistributedUptimeDetailsById";
throw error;
}
};
/**
* Get stats by monitor ID
* @async
@@ -630,6 +674,26 @@ const getMonitorsByTeamId = async (req) => {
},
]
: []),
...(limit
? [
{
$lookup: {
from: "distributeduptimechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "distributeduptimechecks",
},
},
]
: []),
{
$addFields: {
@@ -648,6 +712,10 @@ const getMonitorsByTeamId = async (req) => {
case: { $eq: ["$type", "hardware"] },
then: "$hardwarechecks",
},
{
case: { $eq: ["$type", "distributed_http"] },
then: "$distributeduptimechecks",
},
],
default: [],
},
@@ -815,6 +883,7 @@ export {
getMonitorById,
getMonitorsByTeamId,
getUptimeDetailsById,
getDistributedUptimeDetailsById,
createMonitor,
deleteMonitor,
deleteAllMonitors,
+185 -1
View File
@@ -534,4 +534,188 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
];
};
export { buildUptimeDetailsPipeline, buildHardwareDetailsPipeline };
const buildDistributedUptimeDetailsPipeline = (monitor, dates, dateString) => {
return [
{
$match: {
monitorId: monitor._id,
},
},
{
$sort: {
createdAt: 1,
},
},
{
$facet: {
aggregateData: [
{
$group: {
_id: null,
avgResponseTime: {
$avg: "$responseTime",
},
lastCheck: {
$last: "$$ROOT",
},
totalChecks: {
$sum: 1,
},
uptBurnt: {
$sum: "$uptBurnt",
},
},
},
],
// For the response time chart, should return checks for date window
// Grouped by: {day: hour}, {week: day}, {month: day}
groupedMapChecks: [
{
$match: {
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{
$group: {
_id: {
date: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
city: "$city",
lat: "$location.lat",
lng: "$location.lng",
},
city: { $first: "$city" }, // Add this line to include city in output
lat: { $first: "$location.lat" },
lng: { $first: "$location.lng" },
avgResponseTime: {
$avg: "$responseTime",
},
totalChecks: {
$sum: 1,
},
},
},
{
$sort: {
"_id.date": 1,
},
},
],
groupedChecks: [
{
$match: {
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
avgResponseTime: {
$avg: "$responseTime",
},
totalChecks: {
$sum: 1,
},
},
},
{
$sort: {
_id: 1,
},
},
],
// Average response time for the date window
groupAvgResponseTime: [
{
$match: {
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{
$group: {
_id: null,
avgResponseTime: {
$avg: "$responseTime",
},
},
},
],
latestChecks: [
{
$sort: { createdAt: -1 }, // Sort by newest first
},
{
$limit: 5, // Get only the first 5 documents
},
{
$project: {
responseTime: 1,
city: 1,
countryCode: 1,
uptBurnt: { $toString: "$uptBurnt" },
},
},
],
},
},
{
$project: {
totalUptBurnt: {
$toString: {
$arrayElemAt: ["$aggregateData.uptBurnt", 0],
},
},
avgResponseTime: {
$arrayElemAt: ["$aggregateData.avgResponseTime", 0],
},
totalChecks: {
$arrayElemAt: ["$aggregateData.totalChecks", 0],
},
latestResponseTime: {
$arrayElemAt: ["$aggregateData.lastCheck.responseTime", 0],
},
timeSinceLastCheck: {
$let: {
vars: {
lastCheck: {
$arrayElemAt: ["$aggregateData.lastCheck", 0],
},
},
in: {
$cond: [
{
$ifNull: ["$$lastCheck", false],
},
{
$subtract: [new Date(), "$$lastCheck.createdAt"],
},
0,
],
},
},
},
groupedMapChecks: "$groupedMapChecks",
groupedChecks: "$groupedChecks",
groupedAvgResponseTime: {
$arrayElemAt: ["$groupAvgResponseTime", 0],
},
latestChecks: "$latestChecks",
},
},
];
};
export {
buildUptimeDetailsPipeline,
buildHardwareDetailsPipeline,
buildDistributedUptimeDetailsPipeline,
};
+123 -7
View File
@@ -1,11 +1,18 @@
import StatusPage from "../../models/StatusPage.js";
import { errorMessages } from "../../../utils/messages.js";
import { NormalizeData } from "../../../utils/dataUtils.js";
const SERVICE_NAME = "statusPageModule";
const createStatusPage = async (statusPageData) => {
const createStatusPage = async (statusPageData, image) => {
try {
const statusPage = new StatusPage({ ...statusPageData });
if (image) {
statusPage.logo = {
data: image.buffer,
contentType: image.mimetype,
};
}
await statusPage.save();
return statusPage;
} catch (error) {
@@ -20,16 +27,115 @@ const createStatusPage = async (statusPageData) => {
}
};
const getStatusPageByUrl = async (url) => {
const updateStatusPage = async (statusPageData, image) => {
try {
const statusPage = await StatusPage.findOne({ url });
if (statusPage === null || statusPage === undefined) {
if (image) {
statusPageData.logo = {
data: image.buffer,
contentType: image.mimetype,
};
}
const statusPage = await StatusPage.findOneAndUpdate({}, statusPageData, {
new: true,
});
return statusPage;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateStatusPage";
throw error;
}
};
const getStatusPage = async () => {
try {
const statusPageQuery = await StatusPage.aggregate([
{ $limit: 1 },
{
$set: {
originalMonitors: "$monitors",
},
},
{
$lookup: {
from: "monitors",
localField: "monitors",
foreignField: "_id",
as: "monitors",
},
},
{
$unwind: "$monitors",
},
{
$lookup: {
from: "checks",
let: { monitorId: "$monitors._id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
{ $limit: 25 },
],
as: "monitors.checks",
},
},
{
$addFields: {
"monitors.orderIndex": {
$indexOfArray: ["$originalMonitors", "$monitors._id"],
},
},
},
{
$group: {
_id: "$_id",
statusPage: { $first: "$$ROOT" },
monitors: { $push: "$monitors" },
},
},
{
$project: {
statusPage: {
_id: 1,
color: 1,
companyName: 1,
isPublished: 1,
logo: 1,
originalMonitors: 1,
showCharts: 1,
showUptimePercentage: 1,
timezone: 1,
url: 1,
},
monitors: {
$sortArray: {
input: "$monitors",
sortBy: { orderIndex: 1 },
},
},
},
},
]);
if (!statusPageQuery.length) {
const error = new Error(errorMessages.STATUS_PAGE_NOT_FOUND);
error.status = 404;
throw error;
}
return statusPage;
const { statusPage, monitors } = statusPageQuery[0];
const normalizedMonitors = monitors.map((monitor) => {
return {
...monitor,
checks: NormalizeData(monitor.checks, 10, 100),
};
});
return { statusPage, monitors: normalizedMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
@@ -37,4 +143,14 @@ const getStatusPageByUrl = async (url) => {
}
};
export { createStatusPage, getStatusPageByUrl };
const deleteStatusPage = async () => {
try {
await StatusPage.deleteOne({});
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createStatusPage";
throw error;
}
};
export { createStatusPage, updateStatusPage, getStatusPage, deleteStatusPage };
+5 -1
View File
@@ -252,7 +252,11 @@ const startApp = async () => {
ServiceRegistry.get(NotificationService.SERVICE_NAME)
);
const distributedUptimeController = new DistributedUptimeController();
const distributedUptimeController = new DistributedUptimeController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
http,
ServiceRegistry.get(StatusService.SERVICE_NAME)
);
//Create routes
const authRoutes = new AuthRoutes(authController);
+15 -14
View File
@@ -11,7 +11,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.40.0",
"bullmq": "5.40.2",
"cors": "^2.8.5",
"dockerode": "4.0.4",
"dotenv": "^16.4.5",
@@ -1748,9 +1748,9 @@
}
},
"node_modules/bullmq": {
"version": "5.40.0",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.0.tgz",
"integrity": "sha512-tmrk32EmcbtUOGPSdwlDUcc0w+nAMqCisk8vEFFmG8aOzIehz0BxTNSj6Grh0qoMugRF3VglWk8HGUBnWqU2Fw==",
"version": "5.40.2",
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.40.2.tgz",
"integrity": "sha512-Cn4NUpwGAF4WnuXR2kTZCTAUEUHajSCn/IqiDG9ry1kVvAwwwg1Ati3J5HN2uZjqD5PBfNDXYnsc2+0PzakDwg==",
"license": "MIT",
"dependencies": {
"cron-parser": "^4.9.0",
@@ -4206,9 +4206,10 @@
"license": "ISC"
},
"node_modules/ioredis": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.2.tgz",
"integrity": "sha512-0SZXGNGZ+WzISQ67QDyZ2x0+wVxjjUndtD8oSeik/4ajifeiRufed8fCb8QW8VMyi4MXcS+UO1k/0NGhvq1PAg==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz",
"integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==",
"license": "MIT",
"dependencies": {
"@ioredis/commands": "^1.1.1",
"cluster-key-slot": "^1.1.0",
@@ -5515,9 +5516,9 @@
}
},
"node_modules/mongodb": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz",
"integrity": "sha512-KeESYR5TEaFxOuwRqkOm3XOsMqCSkdeDMjaW5u2nuKfX7rqaofp7JQGoi7sVqQcNJTKuveNbzZtWMstb8ABP6Q==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.9",
@@ -5571,14 +5572,14 @@
}
},
"node_modules/mongoose": {
"version": "8.9.7",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.7.tgz",
"integrity": "sha512-mvNXmU0V8qZzMR/qoK2mjT4Ti2ALdtfS0teK+twxhlGkwzOD76V02/zWajTu2MJ7QyEmZe9OWvnJsIY0iAuX3Q==",
"version": "8.10.0",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.10.0.tgz",
"integrity": "sha512-nLhk3Qrv6q/HpD2k1O7kbBqsq+/kmKpdv5KJ+LLhQlII3e1p/SSLoLP6jMuSiU6+iLK7zFw4T1niAk3mA3QVug==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.1",
"kareem": "2.6.3",
"mongodb": "~6.12.0",
"mongodb": "~6.13.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
+1 -1
View File
@@ -18,7 +18,7 @@
"dependencies": {
"axios": "^1.7.2",
"bcrypt": "5.1.1",
"bullmq": "5.40.0",
"bullmq": "5.40.2",
"cors": "^2.8.5",
"dockerode": "4.0.4",
"dotenv": "^16.4.5",
+8
View File
@@ -8,6 +8,14 @@ class DistributedUptimeRoutes {
}
initRoutes() {
this.router.post("/callback", this.distributedUptimeController.resultsCallback);
this.router.get(
"/monitors/:teamId",
this.distributedUptimeController.getDistributedUptimeMonitors
);
this.router.get(
"/monitors/details/:monitorId",
this.distributedUptimeController.getDistributedUptimeMonitorDetails
);
}
getRouter() {
+16 -3
View File
@@ -1,6 +1,7 @@
import { Router } from "express";
import { verifyJWT } from "../middleware/verifyJWT.js";
import multer from "multer";
const upload = multer();
class StatusPageRoutes {
constructor(statusPageController) {
@@ -10,8 +11,20 @@ class StatusPageRoutes {
}
initRoutes() {
this.router.get("/:url", this.statusPageController.getStatusPageByUrl);
this.router.post("/:url", verifyJWT, this.statusPageController.createStatusPage);
this.router.get("/", this.statusPageController.getStatusPage);
this.router.post(
"/",
upload.single("logo"),
verifyJWT,
this.statusPageController.createStatusPage
);
this.router.put(
"/",
upload.single("logo"),
verifyJWT,
this.statusPageController.updateStatusPage
);
this.router.delete("/", verifyJWT, this.statusPageController.deleteStatusPage);
}
getRouter() {
+5 -47
View File
@@ -1,4 +1,4 @@
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware"];
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
const SERVICE_NAME = "JobQueue";
const JOBS_PER_WORKER = 5;
const QUEUE_LOOKUP = {
@@ -8,6 +8,7 @@ const QUEUE_LOOKUP = {
port: "uptime",
docker: "uptime",
pagespeed: "pagespeed",
distributed_http: "distributed",
};
const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
@@ -163,6 +164,9 @@ class NewJobQueue {
// Get the current status
await job.updateProgress(30);
const networkResponse = await this.networkService.getStatus(job);
if (job.data.type === "distributed_http") {
return;
}
// Handle status change
await job.updateProgress(60);
const { monitor, statusChanged, prevStatus } =
@@ -219,51 +223,6 @@ class NewJobQueue {
connection: this.connection,
concurrency: 5,
});
// worker.on("active", (job) => {
// this.logger.info({
// message: `Worker started processing job: ${job.id}`,
// service: SERVICE_NAME,
// method: "createWorker",
// });
// });
// worker.on("completed", (job) => {
// this.logger.info({
// message: `Worker completed job: ${job.id}`,
// service: SERVICE_NAME,
// method: "createWorker",
// });
// });
// // Log job progress updates
// worker.on("progress", (job, progress) => {
// this.logger.info({
// message: `Job progress: ${job.id}`,
// service: SERVICE_NAME,
// method: "createWorker",
// details: `Progress: ${progress}%`,
// });
// });
// // Log when a job fails
// worker.on("failed", (job, err) => {
// this.logger.error({
// message: `Worker failed job: ${job.id}`,
// service: SERVICE_NAME,
// method: "createWorker",
// details: `Error: ${err.message}`,
// stack: err.stack,
// });
// });
// worker.on("stalled", (jobId) => {
// this.logger.warn({
// message: `Worker stalled job: ${jobId}`,
// service: SERVICE_NAME,
// method: "createWorker",
// });
// });
return worker;
}
@@ -392,7 +351,6 @@ class NewJobQueue {
const jobs = await queue.getJobs();
const ret = await Promise.all(
jobs.map(async (job) => {
console.log(job);
const state = await job.getState();
return { url: job.data.url, state, progress: job.progress };
})
+33
View File
@@ -1,5 +1,6 @@
import { errorMessages, successMessages } from "../utils/messages.js";
const SERVICE_NAME = "NetworkService";
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
/**
* Constructs a new NetworkService instance.
@@ -19,6 +20,7 @@ class NetworkService {
this.TYPE_HARDWARE = "hardware";
this.TYPE_DOCKER = "docker";
this.TYPE_PORT = "port";
this.TYPE_DISTRIBUTED_HTTP = "distributed_http";
this.SERVICE_NAME = SERVICE_NAME;
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
@@ -323,6 +325,35 @@ class NetworkService {
}
}
async requestDistributedHttp(job) {
try {
const monitor = job.data;
const CALLBACK_URL = process.env.NGROK_URL
? process.env.NGROK_URL
: process.env.CALLBACK_URL;
const response = await this.axios.post(
UPROCK_ENDPOINT,
{
id: monitor._id,
url: monitor.url,
callback: `${CALLBACK_URL}/api/v1/distributed-uptime/callback`,
},
{
headers: {
"Content-Type": "application/json",
"x-checkmate-key": process.env.UPROCK_API_KEY,
},
}
);
} catch (error) {
console.log(error.message);
error.service = this.SERVICE_NAME;
error.method = "requestDistributedHttp";
throw error;
}
}
/**
* Handles unsupported job types by throwing an error with details.
*
@@ -406,6 +437,8 @@ class NetworkService {
return await this.requestDocker(job);
case this.TYPE_PORT:
return await this.requestPort(job);
case this.TYPE_DISTRIBUTED_HTTP:
return await this.requestDistributedHttp(job);
default:
return this.handleUnsupportedType(type);
+39 -3
View File
@@ -25,7 +25,7 @@ class StatusService {
* @param {Object} networkResponse - The network response containing monitorId and status.
* @param {string} networkResponse.monitorId - The ID of the monitor.
* @param {string} networkResponse.status - The new status of the monitor.
* @returns {Promise<Object>} - A promise that resolves to an object containing the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred.
* @returns {Promise<Object>} - A promise that resolves to an object containinfg the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred.
* @returns {Promise<Object>} returnObject - The object returned by the function.
* @returns {Object} returnObject.monitor - The monitor object.
* @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed.
@@ -83,8 +83,23 @@ class StatusService {
* @returns {Object} The check object.
*/
buildCheck = (networkResponse) => {
const { monitorId, teamId, type, status, responseTime, code, message, payload } =
networkResponse;
const {
monitorId,
teamId,
type,
status,
responseTime,
code,
message,
payload,
first_byte_took,
body_read_took,
dns_took,
conn_took,
connect_took,
tls_took,
} = networkResponse;
const check = {
monitorId,
teamId,
@@ -92,8 +107,28 @@ class StatusService {
statusCode: code,
responseTime,
message,
first_byte_took,
body_read_took,
dns_took,
conn_took,
connect_took,
tls_took,
};
if (type === "distributed_http") {
check.continent = payload.continent;
check.countryCode = payload.country_code;
check.city = payload.city;
check.location = payload.location;
check.uptBurnt = payload.upt_burnt;
check.first_byte_took = payload.first_byte_took;
check.body_read_took = payload.body_read_took;
check.dns_took = payload.dns_took;
check.conn_took = payload.conn_took;
check.connect_took = payload.connect_took;
check.tls_took = payload.tls_took;
}
if (type === "pagespeed") {
const categories = payload.lighthouseResult?.categories;
const audits = payload.lighthouseResult?.audits;
@@ -145,6 +180,7 @@ class StatusService {
hardware: this.db.createHardwareCheck,
docker: this.db.createCheck,
port: this.db.createCheck,
distributed_http: this.db.createDistributedCheck,
};
const operation = operationMap[networkResponse.type];
+2 -1
View File
@@ -136,7 +136,8 @@ const successMessages = {
// Status Page
STATUS_PAGE_BY_URL: "Got status page by url successfully",
STATUS_PAGE_CREATE: "Status page created successfully",
STATUS_PAGE_DELETE: "Status page deleted successfully",
STATUS_PAGE_UPDATE: "Status page updated successfully",
// Docker
DOCKER_SUCCESS: "Docker container status fetched successfully",
+28 -1
View File
@@ -423,7 +423,6 @@ const createStatusPageBodyValidation = joi.object({
url: joi.string().required(),
timezone: joi.string().required(),
color: joi.string().required(),
theme: joi.string().required(),
monitors: joi
.array()
.items(joi.string().pattern(/^[0-9a-fA-F]{24}$/))
@@ -434,8 +433,35 @@ const createStatusPageBodyValidation = joi.object({
"array.empty": "At least one monitor is required",
"any.required": "Monitors are required",
}),
isPublished: joi.boolean(),
showCharts: joi.boolean(),
showUptimePercentage: joi.boolean(),
});
const imageValidation = joi
.object({
fieldname: joi.string().required(),
originalname: joi.string().required(),
encoding: joi.string().required(),
mimetype: joi
.string()
.valid("image/jpeg", "image/png", "image/jpg")
.required()
.messages({
"string.valid": "File must be a valid image (jpeg, jpg, or png)",
}),
size: joi.number().max(3145728).required().messages({
"number.max": "File size must be less than 3MB",
}),
buffer: joi.binary().required(),
destination: joi.string(),
filename: joi.string(),
path: joi.string(),
})
.messages({
"any.required": "Image file is required",
});
export {
roleValidatior,
loginValidation,
@@ -493,4 +519,5 @@ export {
updateAppSettingsBodyValidation,
createStatusPageBodyValidation,
getStatusPageParamValidation,
imageValidation,
};