Merge branch 'bluewave-labs:develop' into Network-tab-implementation-infra
This commit is contained in:
118 files changed
+10748
-6684
No files matched your search
@@ -23,7 +23,7 @@ class CheckService {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
|
||||
if (!monitor) {
|
||||
throw this.errorService.createNotFoundError("Monitor not found");
|
||||
@@ -34,7 +34,7 @@ class CheckService {
|
||||
}
|
||||
|
||||
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
|
||||
const result = await this.db.getChecksByMonitor({
|
||||
const result = await this.db.checkModule.getChecksByMonitor({
|
||||
monitorId,
|
||||
type,
|
||||
sortOrder,
|
||||
@@ -55,7 +55,7 @@ class CheckService {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const checkData = await this.db.getChecksByTeam({
|
||||
const checkData = await this.db.checkModule.getChecksByTeam({
|
||||
sortOrder,
|
||||
dateRange,
|
||||
filter,
|
||||
@@ -72,7 +72,7 @@ class CheckService {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
|
||||
const summary = await this.db.checkModule.getChecksSummaryByTeamId({ teamId });
|
||||
return summary;
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ class CheckService {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const updatedCheck = await this.db.ackCheck(checkId, teamId, ack);
|
||||
const updatedCheck = await this.db.checkModule.ackCheck(checkId, teamId, ack);
|
||||
return updatedCheck;
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ class CheckService {
|
||||
throw this.errorService.createBadRequestError("No monitor ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
if (!monitor) {
|
||||
throw this.errorService.createNotFoundError("Monitor not found");
|
||||
}
|
||||
@@ -105,7 +105,7 @@ class CheckService {
|
||||
}
|
||||
}
|
||||
|
||||
const updatedChecks = await this.db.ackAllChecks(monitorId, teamId, ack, path);
|
||||
const updatedChecks = await this.db.checkModule.ackAllChecks(monitorId, teamId, ack, path);
|
||||
return updatedChecks;
|
||||
};
|
||||
|
||||
@@ -118,7 +118,7 @@ class CheckService {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
|
||||
if (!monitor) {
|
||||
throw this.errorService.createNotFoundError("Monitor not found");
|
||||
@@ -128,7 +128,7 @@ class CheckService {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
|
||||
const deletedCount = await this.db.deleteChecks(monitorId);
|
||||
const deletedCount = await this.db.checkModule.deleteChecks(monitorId);
|
||||
return deletedCount;
|
||||
};
|
||||
deleteChecksByTeamId = async ({ teamId }) => {
|
||||
@@ -136,14 +136,14 @@ class CheckService {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const deletedCount = await this.db.deleteChecksByTeamId(teamId);
|
||||
const deletedCount = await this.db.checkModule.deleteChecksByTeamId(teamId);
|
||||
return deletedCount;
|
||||
};
|
||||
|
||||
updateChecksTTL = async ({ teamId, ttl }) => {
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
const newTTL = parseInt(ttl, 10) * SECONDS_PER_DAY;
|
||||
await this.db.updateChecksTTL(teamId, newTTL);
|
||||
await this.db.checkModule.updateChecksTTL(teamId, newTTL);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ class InviteService {
|
||||
|
||||
getInviteToken = async ({ invite, teamId }) => {
|
||||
invite.teamId = teamId;
|
||||
const inviteToken = await this.db.requestInviteToken(invite);
|
||||
const inviteToken = await this.db.inviteModule.requestInviteToken(invite);
|
||||
return inviteToken;
|
||||
};
|
||||
|
||||
sendInviteEmail = async ({ inviteRequest, firstName }) => {
|
||||
const inviteToken = await this.db.requestInviteToken({ ...inviteRequest });
|
||||
const inviteToken = await this.db.inviteModule.requestInviteToken({ ...inviteRequest });
|
||||
const { clientHost } = this.settingsService.getSettings();
|
||||
|
||||
const html = await this.emailService.buildEmail("employeeActivationTemplate", {
|
||||
@@ -36,7 +36,7 @@ class InviteService {
|
||||
};
|
||||
|
||||
verifyInviteToken = async ({ inviteToken }) => {
|
||||
const invite = await this.db.getInviteToken(inviteToken);
|
||||
const invite = await this.db.inviteModule.getInviteToken(inviteToken);
|
||||
return invite;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class MaintenanceWindowService {
|
||||
|
||||
createMaintenanceWindow = async ({ teamId, body }) => {
|
||||
const monitorIds = body.monitors;
|
||||
const monitors = await this.db.getMonitorsByIds(monitorIds);
|
||||
const monitors = await this.db.monitorModule.getMonitorsByIds(monitorIds);
|
||||
|
||||
const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId));
|
||||
|
||||
@@ -25,7 +25,7 @@ class MaintenanceWindowService {
|
||||
}
|
||||
|
||||
const dbTransactions = monitorIds.map((monitorId) => {
|
||||
return this.db.createMaintenanceWindow({
|
||||
return this.db.maintenanceWindowModule.createMaintenanceWindow({
|
||||
teamId,
|
||||
monitorId,
|
||||
name: body.name,
|
||||
@@ -39,26 +39,26 @@ class MaintenanceWindowService {
|
||||
};
|
||||
|
||||
getMaintenanceWindowById = async ({ id, teamId }) => {
|
||||
const maintenanceWindow = await this.db.getMaintenanceWindowById({ id, teamId });
|
||||
const maintenanceWindow = await this.db.maintenanceWindowModule.getMaintenanceWindowById({ id, teamId });
|
||||
return maintenanceWindow;
|
||||
};
|
||||
|
||||
getMaintenanceWindowsByTeamId = async ({ teamId, query }) => {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId(teamId, query);
|
||||
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId(teamId, query);
|
||||
return maintenanceWindows;
|
||||
};
|
||||
|
||||
getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId({ monitorId, teamId });
|
||||
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ monitorId, teamId });
|
||||
return maintenanceWindows;
|
||||
};
|
||||
|
||||
deleteMaintenanceWindow = async ({ id, teamId }) => {
|
||||
await this.db.deleteMaintenanceWindowById({ id, teamId });
|
||||
await this.db.maintenanceWindowModule.deleteMaintenanceWindowById({ id, teamId });
|
||||
};
|
||||
|
||||
editMaintenanceWindow = async ({ id, teamId, body }) => {
|
||||
const editedMaintenanceWindow = await this.db.editMaintenanceWindowById({ id, body, teamId });
|
||||
const editedMaintenanceWindow = await this.db.maintenanceWindowModule.editMaintenanceWindowById({ id, body, teamId });
|
||||
return editedMaintenanceWindow;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,20 +20,20 @@ class MonitorService {
|
||||
}
|
||||
|
||||
verifyTeamAccess = async ({ teamId, monitorId }) => {
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
if (!monitor?.teamId?.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
};
|
||||
|
||||
getAllMonitors = async () => {
|
||||
const monitors = await this.db.getAllMonitors();
|
||||
const monitors = await this.db.monitorModule.getAllMonitors();
|
||||
return monitors;
|
||||
};
|
||||
|
||||
getUptimeDetailsById = async ({ teamId, monitorId, dateRange, normalize }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const data = await this.db.getUptimeDetailsById({
|
||||
const data = await this.db.monitorModule.getUptimeDetailsById({
|
||||
monitorId,
|
||||
dateRange,
|
||||
normalize,
|
||||
@@ -44,7 +44,7 @@ class MonitorService {
|
||||
|
||||
getMonitorStatsById = async ({ teamId, monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitorStats = await this.db.getMonitorStatsById({
|
||||
const monitorStats = await this.db.monitorModule.getMonitorStatsById({
|
||||
monitorId,
|
||||
limit,
|
||||
sortOrder,
|
||||
@@ -58,20 +58,20 @@ class MonitorService {
|
||||
|
||||
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange });
|
||||
const monitor = await this.db.monitorModule.getHardwareDetailsById({ monitorId, dateRange });
|
||||
|
||||
return monitor;
|
||||
};
|
||||
|
||||
getMonitorById = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
|
||||
return monitor;
|
||||
};
|
||||
|
||||
createMonitor = async ({ teamId, userId, body }) => {
|
||||
const monitor = await this.db.createMonitor({
|
||||
const monitor = await this.db.monitorModule.createMonitor({
|
||||
body,
|
||||
teamId,
|
||||
userId,
|
||||
@@ -122,7 +122,7 @@ class MonitorService {
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
const monitors = await this.db.createBulkMonitors(enrichedData);
|
||||
const monitors = await this.db.monitorModule.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
@@ -141,21 +141,21 @@ class MonitorService {
|
||||
|
||||
deleteMonitor = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.deleteMonitor({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId });
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.deleteStatusPagesByMonitorId(monitor._id);
|
||||
await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
deleteAllMonitors = async ({ teamId }) => {
|
||||
const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId);
|
||||
const { monitors, deletedCount } = await this.db.monitorModule.deleteAllMonitors(teamId);
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
try {
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.deleteChecks(monitor._id);
|
||||
await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await this.db.deleteNotificationsByMonitorId(monitor._id);
|
||||
await this.db.checkModule.deleteChecks(monitor._id);
|
||||
await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor._id);
|
||||
await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor._id);
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
@@ -171,19 +171,19 @@ class MonitorService {
|
||||
|
||||
editMonitor = async ({ teamId, monitorId, body }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const editedMonitor = await this.db.editMonitor({ monitorId, body });
|
||||
const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body });
|
||||
await this.jobQueue.updateJob(editedMonitor);
|
||||
};
|
||||
|
||||
pauseMonitor = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.pauseMonitor({ monitorId });
|
||||
const monitor = await this.db.monitorModule.pauseMonitor({ monitorId });
|
||||
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
addDemoMonitors = async ({ userId, teamId }) => {
|
||||
const demoMonitors = await this.db.addDemoMonitors(userId, teamId);
|
||||
const demoMonitors = await this.db.monitorModuleaddDemoMonitors(userId, teamId);
|
||||
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor)));
|
||||
return demoMonitors;
|
||||
};
|
||||
@@ -203,7 +203,7 @@ class MonitorService {
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => {
|
||||
const monitors = await this.db.getMonitorsByTeamId({
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
@@ -217,7 +217,7 @@ class MonitorService {
|
||||
};
|
||||
|
||||
getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => {
|
||||
const result = await this.db.getMonitorsAndSummaryByTeamId({
|
||||
const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({
|
||||
type,
|
||||
explain,
|
||||
teamId,
|
||||
@@ -226,7 +226,7 @@ class MonitorService {
|
||||
};
|
||||
|
||||
getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => {
|
||||
const result = await this.db.getMonitorsWithChecksByTeamId({
|
||||
const result = await this.db.monitorModule.getMonitorsWithChecksByTeamId({
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
@@ -241,7 +241,7 @@ class MonitorService {
|
||||
};
|
||||
|
||||
exportMonitorsToCSV = async ({ teamId }) => {
|
||||
const monitors = await this.db.getMonitorsByTeamId({ teamId });
|
||||
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
|
||||
@@ -29,18 +29,18 @@ class UserService {
|
||||
registerUser = async (user, file) => {
|
||||
// Create a new user
|
||||
// If superAdmin exists, a token should be attached to all further register requests
|
||||
const superAdminExists = await this.db.checkSuperadmin();
|
||||
const superAdminExists = await this.db.userModule.checkSuperadmin();
|
||||
if (superAdminExists) {
|
||||
const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken);
|
||||
const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(user.inviteToken);
|
||||
user.role = invitedUser.role;
|
||||
user.teamId = invitedUser.teamId;
|
||||
} else {
|
||||
// This is the first account, create JWT secret to use if one is not supplied by env
|
||||
const jwtSecret = crypto.randomBytes(64).toString("hex");
|
||||
await this.db.updateAppSettings({ jwtSecret });
|
||||
const jwtSecret = this.crypto.randomBytes(64).toString("hex");
|
||||
await this.db.settingsModule.updateAppSettings({ jwtSecret });
|
||||
}
|
||||
|
||||
const newUser = await this.db.insertUser({ ...user }, file);
|
||||
const newUser = await this.db.userModule.insertUser({ ...user }, file);
|
||||
|
||||
this.logger.debug({
|
||||
message: "New user created",
|
||||
@@ -83,7 +83,7 @@ class UserService {
|
||||
|
||||
loginUser = async (email, password) => {
|
||||
// Check if user exists
|
||||
const user = await this.db.getUserByEmail(email);
|
||||
const user = await this.db.userModule.getUserByEmail(email);
|
||||
// Compare password
|
||||
const match = await user.comparePassword(password);
|
||||
if (match !== true) {
|
||||
@@ -110,7 +110,7 @@ class UserService {
|
||||
// Add user email to body for DB operation
|
||||
updates.email = currentUser.email;
|
||||
// Get user
|
||||
const user = await this.db.getUserByEmail(currentUser.email);
|
||||
const user = await this.db.userModule.getUserByEmail(currentUser.email);
|
||||
// Compare passwords
|
||||
const match = await user.comparePassword(updates?.password);
|
||||
// If not a match, throw a 403
|
||||
@@ -122,18 +122,18 @@ class UserService {
|
||||
updates.password = updates.newPassword;
|
||||
}
|
||||
|
||||
const updatedUser = await this.db.updateUser({ userId: currentUser?._id, user: updates, file: file });
|
||||
const updatedUser = await this.db.userModule.updateUser({ userId: currentUser?._id, user: updates, file: file });
|
||||
return updatedUser;
|
||||
};
|
||||
|
||||
checkSuperadminExists = async () => {
|
||||
const superAdminExists = await this.db.checkSuperadmin();
|
||||
const superAdminExists = await this.db.userModule.checkSuperadmin();
|
||||
return superAdminExists;
|
||||
};
|
||||
|
||||
requestRecovery = async (email) => {
|
||||
const user = await this.db.getUserByEmail(email);
|
||||
const recoveryToken = await this.db.requestRecoveryToken(email);
|
||||
const user = await this.db.userModule.getUserByEmail(email);
|
||||
const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email);
|
||||
const name = user.firstName;
|
||||
const { clientHost } = this.settingsService.getSettings();
|
||||
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
|
||||
@@ -148,11 +148,11 @@ class UserService {
|
||||
};
|
||||
|
||||
validateRecovery = async (recoveryToken) => {
|
||||
await this.db.validateRecoveryToken(recoveryToken);
|
||||
await this.db.recoveryModule.validateRecoveryToken(recoveryToken);
|
||||
};
|
||||
|
||||
resetPassword = async (password, recoveryToken) => {
|
||||
const user = await this.db.resetPassword(password, recoveryToken);
|
||||
const user = await this.db.recoveryModule.resetPassword(password, recoveryToken);
|
||||
const appSettings = await this.settingsService.getSettings();
|
||||
const token = this.issueToken(user._doc, appSettings);
|
||||
return { user, token };
|
||||
@@ -181,7 +181,7 @@ class UserService {
|
||||
}
|
||||
|
||||
// 1. Find all the monitors associated with the team ID if superadmin
|
||||
const result = await this.db.getMonitorsByTeamId({
|
||||
const result = await this.db.monitorModule.getMonitorsByTeamId({
|
||||
teamId: teamId,
|
||||
});
|
||||
|
||||
@@ -195,21 +195,21 @@ class UserService {
|
||||
));
|
||||
}
|
||||
// 6. Delete the user by id
|
||||
await this.db.deleteUser(userId);
|
||||
await this.db.userModule.deleteUser(userId);
|
||||
};
|
||||
|
||||
getAllUsers = async () => {
|
||||
const users = await this.db.getAllUsers();
|
||||
const users = await this.db.userModule.getAllUsers();
|
||||
return users;
|
||||
};
|
||||
|
||||
getUserById = async (roles, userId) => {
|
||||
const user = await this.db.getUserById(roles, userId);
|
||||
const user = await this.db.userModule.getUserById(roles, userId);
|
||||
return user;
|
||||
};
|
||||
|
||||
editUserById = async (userId, user) => {
|
||||
await this.db.editUserById(userId, user);
|
||||
await this.db.userModule.editUserById(userId, user);
|
||||
};
|
||||
}
|
||||
export default UserService;
|
||||
@@ -1,64 +0,0 @@
|
||||
const SERVICE_NAME = "RedisService";
|
||||
|
||||
class RedisService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ Redis, logger }) {
|
||||
this.Redis = Redis;
|
||||
this.connections = new Set();
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return RedisService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getNewConnection(options = {}) {
|
||||
const connection = new this.Redis(process.env.REDIS_URL, {
|
||||
retryStrategy: (times) => {
|
||||
return null;
|
||||
},
|
||||
...options,
|
||||
});
|
||||
this.connections.add(connection);
|
||||
return connection;
|
||||
}
|
||||
|
||||
async closeAllConnections() {
|
||||
const closePromises = Array.from(this.connections).map((conn) =>
|
||||
conn.quit().catch((err) => {
|
||||
this.logger.error({
|
||||
message: "Error closing Redis connection",
|
||||
service: SERVICE_NAME,
|
||||
method: "closeAllConnections",
|
||||
details: { error: err },
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all(closePromises);
|
||||
this.connections.clear();
|
||||
this.logger.info({
|
||||
message: "All Redis connections closed",
|
||||
service: SERVICE_NAME,
|
||||
method: "closeAllConnections",
|
||||
});
|
||||
}
|
||||
|
||||
async flushRedis() {
|
||||
this.logger.info({
|
||||
message: "Flushing Redis",
|
||||
service: SERVICE_NAME,
|
||||
method: "flushRedis",
|
||||
});
|
||||
const flushPromises = Array.from(this.connections).map((conn) => conn.flushall());
|
||||
await Promise.all(flushPromises);
|
||||
this.logger.info({
|
||||
message: "Redis flushed",
|
||||
service: SERVICE_NAME,
|
||||
method: "flushRedis",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default RedisService;
|
||||
@@ -1,321 +0,0 @@
|
||||
const QUEUE_NAMES = ["uptime", "pagespeed", "hardware"];
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
const HEALTH_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
|
||||
const QUEUE_LOOKUP = {
|
||||
hardware: "hardware",
|
||||
http: "uptime",
|
||||
ping: "uptime",
|
||||
port: "uptime",
|
||||
docker: "uptime",
|
||||
pagespeed: "pagespeed",
|
||||
};
|
||||
const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
|
||||
|
||||
class JobQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, jobQueueHelper, logger, stringService }) {
|
||||
this.db = db;
|
||||
this.jobQueueHelper = jobQueueHelper;
|
||||
this.stringService = stringService;
|
||||
this.logger = logger;
|
||||
this.queues = {};
|
||||
this.workers = [];
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return JobQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create({ db, jobQueueHelper, logger, stringService }) {
|
||||
const instance = new JobQueue({ db, jobQueueHelper, logger, stringService });
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.initQueues();
|
||||
await this.initWorkers();
|
||||
const monitors = await this.db.getAllMonitors();
|
||||
await Promise.all(
|
||||
monitors
|
||||
.filter((monitor) => monitor.isActive)
|
||||
.map(async (monitor) => {
|
||||
try {
|
||||
await this.addJob(monitor._id, monitor);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "initJobQueue",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
this.healthCheckInterval = setInterval(async () => {
|
||||
try {
|
||||
const queueHealthChecks = await this.checkQueueHealth();
|
||||
const queueIsStuck = queueHealthChecks.some((healthCheck) => healthCheck.stuck);
|
||||
if (queueIsStuck) {
|
||||
this.logger.warn({
|
||||
message: "Queue is stuck",
|
||||
service: SERVICE_NAME,
|
||||
method: "periodicHealthCheck",
|
||||
details: queueHealthChecks,
|
||||
});
|
||||
await this.flushQueues();
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "periodicHealthCheck",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}, HEALTH_CHECK_INTERVAL);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "initJobQueue",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async initQueues() {
|
||||
const readyPromises = [];
|
||||
|
||||
for (const queueName of QUEUE_NAMES) {
|
||||
const q = this.jobQueueHelper.createQueue(queueName);
|
||||
this.queues[queueName] = q;
|
||||
readyPromises.push(q.waitUntilReady());
|
||||
}
|
||||
await Promise.all(readyPromises);
|
||||
this.logger.info({
|
||||
message: "Queues ready",
|
||||
service: SERVICE_NAME,
|
||||
method: "initQueues",
|
||||
});
|
||||
}
|
||||
|
||||
async initWorkers() {
|
||||
const workerReadyPromises = [];
|
||||
|
||||
for (const queueName of QUEUE_NAMES) {
|
||||
const worker = this.jobQueueHelper.createWorker(queueName, this.queues[queueName]);
|
||||
this.workers.push(worker);
|
||||
workerReadyPromises.push(worker.waitUntilReady());
|
||||
}
|
||||
await Promise.all(workerReadyPromises);
|
||||
this.logger.info({
|
||||
message: "Workers ready",
|
||||
service: SERVICE_NAME,
|
||||
method: "initWorkers",
|
||||
});
|
||||
}
|
||||
|
||||
pauseJob = async (monitor) => {
|
||||
this.deleteJob(monitor);
|
||||
};
|
||||
|
||||
resumeJob = async (monitor) => {
|
||||
this.addJob(monitor._id, monitor);
|
||||
};
|
||||
|
||||
async addJob(jobName, monitor) {
|
||||
this.logger.info({
|
||||
message: `Adding job ${monitor?.url ?? "No URL"}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "addJob",
|
||||
});
|
||||
|
||||
const queueName = QUEUE_LOOKUP[monitor.type];
|
||||
const queue = this.queues[queueName];
|
||||
if (typeof queue === "undefined") {
|
||||
throw new Error(`Queue for ${monitor.type} not found`);
|
||||
}
|
||||
const jobTemplate = {
|
||||
name: jobName,
|
||||
data: monitor,
|
||||
opts: {
|
||||
attempts: 1,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 1000,
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: false,
|
||||
timeout: 1 * 60 * 1000,
|
||||
},
|
||||
};
|
||||
|
||||
const schedulerId = getSchedulerId(monitor);
|
||||
await queue.upsertJobScheduler(schedulerId, { every: monitor?.interval ?? 60000 }, jobTemplate);
|
||||
}
|
||||
|
||||
async deleteJob(monitor) {
|
||||
try {
|
||||
const queue = this.queues[QUEUE_LOOKUP[monitor.type]];
|
||||
const schedulerId = getSchedulerId(monitor);
|
||||
const wasDeleted = await queue.removeJobScheduler(schedulerId);
|
||||
|
||||
if (wasDeleted === true) {
|
||||
this.logger.info({
|
||||
message: this.stringService.jobQueueDeleteJob,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteJob",
|
||||
details: `Deleted job ${monitor._id}`,
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
this.logger.error({
|
||||
message: this.stringService.jobQueueDeleteJob,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteJob",
|
||||
details: `Failed to delete job ${monitor._id}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "deleteJob") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateJob(monitor) {
|
||||
await this.deleteJob(monitor);
|
||||
await this.addJob(monitor._id, monitor);
|
||||
}
|
||||
|
||||
async getJobs() {
|
||||
try {
|
||||
let stats = {};
|
||||
await Promise.all(
|
||||
QUEUE_NAMES.map(async (name) => {
|
||||
const queue = this.queues[name];
|
||||
const jobs = await queue.getJobs();
|
||||
const ret = await Promise.all(
|
||||
jobs.map(async (job) => {
|
||||
const state = await job.getState();
|
||||
return { url: job.data.url, state, progress: job.progress };
|
||||
})
|
||||
);
|
||||
stats[name] = { jobs: ret };
|
||||
})
|
||||
);
|
||||
return stats;
|
||||
} catch (error) {
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "getJobStats") : null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getMetrics() {
|
||||
try {
|
||||
let metrics = {};
|
||||
|
||||
await Promise.all(
|
||||
QUEUE_NAMES.map(async (name) => {
|
||||
const queue = this.queues[name];
|
||||
const [waiting, active, failed, delayed, repeatableJobs] = await Promise.all([
|
||||
queue.getWaitingCount(),
|
||||
queue.getActiveCount(),
|
||||
queue.getFailedCount(),
|
||||
queue.getDelayedCount(),
|
||||
queue.getRepeatableJobs(),
|
||||
]);
|
||||
|
||||
metrics[name] = {
|
||||
waiting,
|
||||
active,
|
||||
failed,
|
||||
delayed,
|
||||
repeatableJobs: repeatableJobs.length,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return metrics;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMetrics",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async checkQueueHealth() {
|
||||
const res = [];
|
||||
for (const queueName of QUEUE_NAMES) {
|
||||
const q = this.queues[queueName];
|
||||
await q.waitUntilReady();
|
||||
|
||||
const lastJobProcessedTime = q.lastJobProcessedTime;
|
||||
const currentTime = Date.now();
|
||||
const timeDiff = currentTime - lastJobProcessedTime;
|
||||
|
||||
// Check for jobs
|
||||
const jobCounts = await q.getJobCounts();
|
||||
const hasJobs = Object.values(jobCounts).some((count) => count > 0);
|
||||
|
||||
res.push({
|
||||
queueName,
|
||||
timeSinceLastJob: timeDiff,
|
||||
stuck: hasJobs && timeDiff > 10000,
|
||||
jobCounts,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async flushQueues() {
|
||||
try {
|
||||
this.logger.warn({
|
||||
message: "Flushing queues",
|
||||
method: "flushQueues",
|
||||
service: SERVICE_NAME,
|
||||
});
|
||||
for (const worker of this.workers) {
|
||||
await worker.close();
|
||||
}
|
||||
this.workers = [];
|
||||
|
||||
for (const queue of Object.values(this.queues)) {
|
||||
await queue.obliterate();
|
||||
}
|
||||
this.queue = {};
|
||||
await this.init();
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: `${error.message} - Flushing redis manually`,
|
||||
service: SERVICE_NAME,
|
||||
method: "flushQueues",
|
||||
});
|
||||
return await this.jobQueueHelper.flushRedis();
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
if (this.healthCheckInterval) {
|
||||
clearInterval(this.healthCheckInterval);
|
||||
this.healthCheckInterval = null;
|
||||
}
|
||||
for (const worker of this.workers) {
|
||||
await worker.close();
|
||||
}
|
||||
for (const queue of Object.values(this.queues)) {
|
||||
await queue.obliterate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default JobQueue;
|
||||
@@ -1,308 +0,0 @@
|
||||
const SERVICE_NAME = "JobQueueHelper";
|
||||
|
||||
class JobQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ redisService, Queue, Worker, logger, db, networkService, statusService, notificationService }) {
|
||||
this.db = db;
|
||||
this.redisService = redisService;
|
||||
this.Queue = Queue;
|
||||
this.Worker = Worker;
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return JobQueueHelper.SERVICE_NAME;
|
||||
}
|
||||
|
||||
createQueue(queueName) {
|
||||
const connection = this.redisService.getNewConnection();
|
||||
const q = new this.Queue(queueName, {
|
||||
connection,
|
||||
});
|
||||
q.lastJobProcessedTime = Date.now();
|
||||
q.on("cleaned", (jobs, type) => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is cleaned with jobs: ${jobs} and type: ${type}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:cleaned",
|
||||
});
|
||||
});
|
||||
q.on("error", (err) => {
|
||||
this.logger.error({
|
||||
message: `Queue ${queueName} is error with msg: ${err}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:error",
|
||||
});
|
||||
});
|
||||
q.on("ioredis:close", () => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is ioredis:close`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:ioredis:close",
|
||||
});
|
||||
});
|
||||
q.on("paused", () => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is paused`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:paused",
|
||||
});
|
||||
});
|
||||
q.on("progress", (job, progress) => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is progress with msg: ${progress}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:progress",
|
||||
});
|
||||
});
|
||||
q.on("removed", (job) => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is removed with msg: ${job}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:removed",
|
||||
});
|
||||
});
|
||||
q.on("resumed", () => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is resumed`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:resumed",
|
||||
});
|
||||
});
|
||||
q.on("waiting", () => {
|
||||
this.logger.debug({
|
||||
message: `Queue ${queueName} is waiting`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createQueue:waiting",
|
||||
});
|
||||
});
|
||||
return q;
|
||||
}
|
||||
|
||||
createWorker(queueName, queue) {
|
||||
const connection = this.redisService.getNewConnection({
|
||||
maxRetriesPerRequest: null,
|
||||
});
|
||||
const worker = new this.Worker(queueName, this.createJobHandler(queue), {
|
||||
connection,
|
||||
concurrency: 50,
|
||||
});
|
||||
worker.on("active", (job) => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is active`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:active",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("closed", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is closed`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:closed",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("closing", (msg) => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is closing with msg: ${msg}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:closing",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("completed", (job) => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is completed`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:completed",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("drained", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is drained`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:drained",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("error", (failedReason) => {
|
||||
this.logger.error({
|
||||
message: `Worker ${queueName} is error with msg: ${failedReason}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:error",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("failed", (job, error, prev) => {
|
||||
this.logger.error({
|
||||
message: `Worker ${queueName} is failed with msg: ${error.message}`,
|
||||
service: error?.service ?? SERVICE_NAME,
|
||||
method: error?.method ?? "createWorker:failed",
|
||||
stack: error?.stack,
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("ioredis:close", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is ioredis:close`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:ioredis:close",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("paused", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is paused`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:paused",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("progress", (job, progress) => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is progress with msg: ${progress}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:progress",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("ready", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is ready`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:ready",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("resumed", () => {
|
||||
this.logger.debug({
|
||||
message: `Worker ${queueName} is resumed`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:resumed",
|
||||
});
|
||||
});
|
||||
|
||||
worker.on("stalled", () => {
|
||||
this.logger.warn({
|
||||
message: `Worker ${queueName} is stalled`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker:stalled",
|
||||
});
|
||||
});
|
||||
return worker;
|
||||
}
|
||||
|
||||
async isInMaintenanceWindow(monitorId) {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
|
||||
// Check for active maintenance window:
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
|
||||
if (window.active) {
|
||||
const start = new Date(window.start);
|
||||
const end = new Date(window.end);
|
||||
const now = new Date();
|
||||
const repeatInterval = window.repeat || 0;
|
||||
|
||||
// If start is < now and end > now, we're in maintenance
|
||||
if (start <= now && end >= now) return true;
|
||||
|
||||
// If maintenance window was set in the past with a repeat,
|
||||
// we need to advance start and end to see if we are in range
|
||||
|
||||
while (start < now && repeatInterval !== 0) {
|
||||
start.setTime(start.getTime() + repeatInterval);
|
||||
end.setTime(end.getTime() + repeatInterval);
|
||||
if (start <= now && end >= now) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return acc;
|
||||
}, false);
|
||||
return maintenanceWindowIsActive;
|
||||
}
|
||||
|
||||
createJobHandler(q) {
|
||||
return async (job) => {
|
||||
try {
|
||||
// Update the last job processed time for this queue
|
||||
q.lastJobProcessedTime = Date.now();
|
||||
// Get all maintenance windows for this monitor
|
||||
await job.updateProgress(0);
|
||||
const monitorId = job.data._id;
|
||||
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
|
||||
// If a maintenance window is active, we're done
|
||||
|
||||
if (maintenanceWindowActive) {
|
||||
await job.updateProgress(100);
|
||||
this.logger.info({
|
||||
message: `Monitor ${monitorId} is in maintenance window`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createWorker",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the current status
|
||||
await job.updateProgress(30);
|
||||
const monitor = job.data;
|
||||
const networkResponse = await this.networkService.getStatus(monitor);
|
||||
|
||||
// If the network response is not found, we're done
|
||||
if (!networkResponse) {
|
||||
await job.updateProgress(100);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle status change
|
||||
await job.updateProgress(60);
|
||||
const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse);
|
||||
// Handle notifications
|
||||
await job.updateProgress(80);
|
||||
this.notificationService
|
||||
.handleNotifications({
|
||||
...networkResponse,
|
||||
monitor: updatedMonitor,
|
||||
prevStatus,
|
||||
statusChanged,
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "createJobHandler",
|
||||
details: `Error sending notifications for job ${job.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
await job.updateProgress(100);
|
||||
return true;
|
||||
} catch (error) {
|
||||
await job.updateProgress(100);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
async flushRedis() {
|
||||
try {
|
||||
const connection = this.redisService.getNewConnection();
|
||||
const flushResult = await connection.flushall();
|
||||
return flushResult;
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "flushRedis",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default JobQueueHelper;
|
||||
@@ -1,213 +0,0 @@
|
||||
import { Pulse } from "@pulsecron/pulse";
|
||||
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
class PulseQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ appSettings, db, pulseQueueHelper, logger }) {
|
||||
this.db = db;
|
||||
this.appSettings = appSettings;
|
||||
this.pulseQueueHelper = pulseQueueHelper;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return PulseQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create({ appSettings, db, pulseQueueHelper, logger }) {
|
||||
const instance = new PulseQueue({ appSettings, db, pulseQueueHelper, logger });
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Core methods
|
||||
// ****************************************
|
||||
init = async () => {
|
||||
try {
|
||||
const mongoConnectionString = this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
|
||||
this.pulse = new Pulse({ db: { address: mongoConnectionString } });
|
||||
await this.pulse.start();
|
||||
this.pulse.define("monitor-job", this.pulseQueueHelper.getMonitorJob(), {});
|
||||
|
||||
const monitors = await this.db.getAllMonitors();
|
||||
for (const monitor of monitors) {
|
||||
await this.addJob(monitor._id, monitor);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: "Failed to initialize PulseQueue",
|
||||
service: SERVICE_NAME,
|
||||
method: "init",
|
||||
details: error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
addJob = async (monitorId, monitor) => {
|
||||
this.logger.debug({
|
||||
message: `Adding job ${monitor?.url ?? "No URL"}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "addJob",
|
||||
});
|
||||
const intervalInSeconds = monitor.interval / 1000;
|
||||
const job = this.pulse.create("monitor-job", {
|
||||
monitor,
|
||||
});
|
||||
job.unique({ "data.monitor._id": monitor._id });
|
||||
job.attrs.jobId = monitorId.toString();
|
||||
job.repeatEvery(`${intervalInSeconds} seconds`);
|
||||
if (monitor.isActive === false) {
|
||||
job.disable();
|
||||
}
|
||||
await job.save();
|
||||
};
|
||||
|
||||
deleteJob = async (monitor) => {
|
||||
this.logger.debug({
|
||||
message: `Deleting job ${monitor?.url ?? "No URL"}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteJob",
|
||||
});
|
||||
await this.pulse.cancel({
|
||||
"data.monitor._id": monitor._id,
|
||||
});
|
||||
};
|
||||
|
||||
pauseJob = async (monitor) => {
|
||||
const result = await this.pulse.disable({
|
||||
"data.monitor._id": monitor._id,
|
||||
});
|
||||
|
||||
if (result.length < 1) {
|
||||
throw new Error("Failed to pause monitor");
|
||||
}
|
||||
|
||||
this.logger.debug({
|
||||
message: `Paused monitor ${monitor._id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "pauseJob",
|
||||
});
|
||||
};
|
||||
|
||||
resumeJob = async (monitor) => {
|
||||
const result = await this.pulse.enable({
|
||||
"data.monitor._id": monitor._id,
|
||||
});
|
||||
|
||||
if (result.length < 1) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
|
||||
this.logger.debug({
|
||||
message: `Resumed monitor ${monitor._id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "resumeJob",
|
||||
});
|
||||
};
|
||||
|
||||
updateJob = async (monitor) => {
|
||||
const jobs = await this.pulse.jobs({
|
||||
"data.monitor._id": monitor._id,
|
||||
});
|
||||
|
||||
const job = jobs[0];
|
||||
if (!job) {
|
||||
throw new Error("Job not found");
|
||||
}
|
||||
|
||||
const intervalInSeconds = monitor.interval / 1000;
|
||||
job.repeatEvery(`${intervalInSeconds} seconds`);
|
||||
job.attrs.data.monitor = monitor;
|
||||
await job.save();
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
this.logger.info({
|
||||
message: "Shutting down JobQueue",
|
||||
service: SERVICE_NAME,
|
||||
method: "shutdown",
|
||||
});
|
||||
await this.pulse.stop();
|
||||
};
|
||||
|
||||
// ****************************************
|
||||
// Diagnostic methods
|
||||
// ****************************************
|
||||
|
||||
getMetrics = async () => {
|
||||
const jobs = await this.pulse.jobs();
|
||||
const metrics = jobs.reduce(
|
||||
(acc, job) => {
|
||||
acc.totalRuns += job.attrs.runCount || 0;
|
||||
acc.totalFailures += job.attrs.failCount || 0;
|
||||
acc.jobs++;
|
||||
if (job.attrs.failCount > 0 && job.attrs.failedAt >= job.attrs.lastFinishedAt) {
|
||||
acc.failingJobs++;
|
||||
}
|
||||
if (job.attrs.lockedAt) {
|
||||
acc.activeJobs++;
|
||||
}
|
||||
if (job.attrs.failCount > 0) {
|
||||
acc.jobsWithFailures.push({
|
||||
monitorId: job.attrs.data.monitor._id,
|
||||
monitorUrl: job.attrs.data.monitor.url,
|
||||
monitorType: job.attrs.data.monitor.type,
|
||||
failedAt: job.attrs.failedAt,
|
||||
failCount: job.attrs.failCount,
|
||||
failReason: job.attrs.failReason,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
jobs: 0,
|
||||
activeJobs: 0,
|
||||
failingJobs: 0,
|
||||
jobsWithFailures: [],
|
||||
totalRuns: 0,
|
||||
totalFailures: 0,
|
||||
}
|
||||
);
|
||||
return metrics;
|
||||
};
|
||||
|
||||
getJobs = async () => {
|
||||
const jobs = await this.pulse.jobs();
|
||||
return jobs.map((job) => {
|
||||
return {
|
||||
monitorId: job.attrs.data.monitor._id,
|
||||
monitorUrl: job.attrs.data.monitor.url,
|
||||
monitorType: job.attrs.data.monitor.type,
|
||||
active: !job.attrs.disabled,
|
||||
lockedAt: job.attrs.lockedAt,
|
||||
runCount: job.attrs.runCount || 0,
|
||||
failCount: job.attrs.failCount || 0,
|
||||
failReason: job.attrs.failReason,
|
||||
lastRunAt: job.attrs.lastRunAt,
|
||||
lastFinishedAt: job.attrs.lastFinishedAt,
|
||||
lastRunTook: job.attrs.lockedAt ? null : job.attrs.lastFinishedAt - job.attrs.lastRunAt,
|
||||
lastFailedAt: job.attrs.failedAt,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
flushQueues = async () => {
|
||||
const cancelRes = await this.pulse.cancel();
|
||||
await this.pulse.stop();
|
||||
const initRes = await this.init();
|
||||
return {
|
||||
flushedJobs: cancelRes,
|
||||
initSuccess: initRes,
|
||||
};
|
||||
};
|
||||
|
||||
obliterate = async () => {
|
||||
await this.flushQueues();
|
||||
};
|
||||
}
|
||||
|
||||
export default PulseQueue;
|
||||
@@ -1,104 +0,0 @@
|
||||
const SERVICE_NAME = "PulseQueueHelper";
|
||||
|
||||
class PulseQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, logger, networkService, statusService, notificationService }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return PulseQueueHelper.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getMonitorJob = () => {
|
||||
return async (job) => {
|
||||
try {
|
||||
const monitor = job.attrs.data.monitor;
|
||||
const monitorId = job.attrs.data.monitor._id;
|
||||
if (!monitorId) {
|
||||
throw new Error("No monitor id");
|
||||
}
|
||||
|
||||
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
|
||||
if (maintenanceWindowActive) {
|
||||
this.logger.info({
|
||||
message: `Monitor ${monitorId} is in maintenance window`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const networkResponse = await this.networkService.getStatus(monitor);
|
||||
|
||||
if (!networkResponse) {
|
||||
throw new Error("No network response");
|
||||
}
|
||||
|
||||
const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse);
|
||||
|
||||
this.notificationService
|
||||
.handleNotifications({
|
||||
...networkResponse,
|
||||
monitor: updatedMonitor,
|
||||
prevStatus,
|
||||
statusChanged,
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
details: `Error sending notifications for job ${job.id}: ${error.message}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: error.service || SERVICE_NAME,
|
||||
method: error.method || "getMonitorJob",
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
async isInMaintenanceWindow(monitorId) {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
|
||||
// Check for active maintenance window:
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
|
||||
if (window.active) {
|
||||
const start = new Date(window.start);
|
||||
const end = new Date(window.end);
|
||||
const now = new Date();
|
||||
const repeatInterval = window.repeat || 0;
|
||||
|
||||
// If start is < now and end > now, we're in maintenance
|
||||
if (start <= now && end >= now) return true;
|
||||
|
||||
// If maintenance window was set in the past with a repeat,
|
||||
// we need to advance start and end to see if we are in range
|
||||
|
||||
while (start < now && repeatInterval !== 0) {
|
||||
start.setTime(start.getTime() + repeatInterval);
|
||||
end.setTime(end.getTime() + repeatInterval);
|
||||
if (start <= now && end >= now) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return acc;
|
||||
}, false);
|
||||
return maintenanceWindowIsActive;
|
||||
}
|
||||
}
|
||||
|
||||
export default PulseQueueHelper;
|
||||
@@ -4,8 +4,8 @@ const SERVICE_NAME = "JobQueue";
|
||||
class SuperSimpleQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ appSettings, db, logger, helper }) {
|
||||
this.appSettings = appSettings;
|
||||
constructor({ envSettings, db, logger, helper }) {
|
||||
this.envSettings = envSettings;
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.helper = helper;
|
||||
@@ -15,8 +15,8 @@ class SuperSimpleQueue {
|
||||
return SuperSimpleQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create({ appSettings, db, logger, helper }) {
|
||||
const instance = new SuperSimpleQueue({ appSettings, db, logger, helper });
|
||||
static async create({ envSettings, db, logger, helper }) {
|
||||
const instance = new SuperSimpleQueue({ envSettings, db, logger, helper });
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
@@ -28,12 +28,12 @@ class SuperSimpleQueue {
|
||||
// storeType: "redis",
|
||||
logLevel: "debug",
|
||||
debug: true,
|
||||
dbUri: this.appSettings.dbConnectionString,
|
||||
// dbUri: this.envSettings.dbConnectionString,
|
||||
});
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||
const monitors = await this.db.getAllMonitors();
|
||||
const monitors = await this.db.monitorModule.getAllMonitors();
|
||||
for (const monitor of monitors) {
|
||||
await this.addJob(monitor._id, monitor);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class SuperSimpleQueueHelper {
|
||||
};
|
||||
|
||||
async isInMaintenanceWindow(monitorId) {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
|
||||
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId(monitorId);
|
||||
// Check for active maintenance window:
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
|
||||
if (window.active) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const SERVICE_NAME = "BufferService";
|
||||
const BUFFER_TIMEOUT = process.env.NODE_ENV === "development" ? 5000 : 1000 * 60 * 1; // 1 minute
|
||||
const TYPE_MAP = {
|
||||
http: "checks",
|
||||
ping: "checks",
|
||||
@@ -11,7 +10,8 @@ const TYPE_MAP = {
|
||||
|
||||
class BufferService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor({ db, logger }) {
|
||||
constructor({ db, logger, envSettings }) {
|
||||
this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 5000 : 1000 * 60 * 1; // 1 minute
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
@@ -21,14 +21,14 @@ class BufferService {
|
||||
hardwareChecks: [],
|
||||
};
|
||||
this.OPERATION_MAP = {
|
||||
checks: this.db.createChecks,
|
||||
pagespeedChecks: this.db.createPageSpeedChecks,
|
||||
hardwareChecks: this.db.createHardwareChecks,
|
||||
checks: this.db.checkModule.createChecks,
|
||||
pagespeedChecks: this.db.pageSpeedCheckModule.createPageSpeedChecks,
|
||||
hardwareChecks: this.db.hardwareCheckModule.createHardwareChecks,
|
||||
};
|
||||
|
||||
this.scheduleNextFlush();
|
||||
this.logger.info({
|
||||
message: `Buffer service initialized, flushing every ${BUFFER_TIMEOUT / 1000}s`,
|
||||
message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "constructor",
|
||||
});
|
||||
@@ -66,7 +66,7 @@ class BufferService {
|
||||
// Schedule the next flush only after the current one completes
|
||||
this.scheduleNextFlush();
|
||||
}
|
||||
}, BUFFER_TIMEOUT);
|
||||
}, this.BUFFER_TIMEOUT);
|
||||
}
|
||||
async flushBuffers() {
|
||||
let items = 0;
|
||||
|
||||
@@ -81,7 +81,7 @@ class NotificationService {
|
||||
}
|
||||
|
||||
async notifyAll({ notificationIDs, subject, html, content }) {
|
||||
const notifications = await this.db.getNotificationsByIds(notificationIDs);
|
||||
const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs);
|
||||
|
||||
// Map each notification to a test promise
|
||||
const promises = notifications.map(async (notification) => {
|
||||
|
||||
@@ -122,7 +122,7 @@ class StatusService {
|
||||
this.insertCheck(networkResponse);
|
||||
try {
|
||||
const { monitorId, status, code } = networkResponse;
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
|
||||
// Update running stats
|
||||
this.updateRunningStats({ monitor, networkResponse });
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
const SERVICE_NAME = "ServiceRegistry";
|
||||
import logger from "../../utils/logger.js";
|
||||
|
||||
class ServiceRegistry {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor() {
|
||||
|
||||
constructor({ logger }) {
|
||||
this.services = {};
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return ServiceRegistry.SERVICE_NAME;
|
||||
}
|
||||
|
||||
// Instance methods
|
||||
register(name, service) {
|
||||
logger.info({
|
||||
this.logger.info({
|
||||
message: `Registering service ${name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "register",
|
||||
@@ -21,7 +24,7 @@ class ServiceRegistry {
|
||||
|
||||
get(name) {
|
||||
if (!this.services[name]) {
|
||||
logger.error({
|
||||
this.logger.error({
|
||||
message: `Service ${name} is not registered`,
|
||||
service: SERVICE_NAME,
|
||||
method: "get",
|
||||
@@ -34,6 +37,27 @@ class ServiceRegistry {
|
||||
listServices() {
|
||||
return Object.keys(this.services);
|
||||
}
|
||||
|
||||
static get(name) {
|
||||
if (!ServiceRegistry.instance) {
|
||||
throw new Error("ServiceRegistry not initialized");
|
||||
}
|
||||
return ServiceRegistry.instance.get(name);
|
||||
}
|
||||
|
||||
static register(name, service) {
|
||||
if (!ServiceRegistry.instance) {
|
||||
throw new Error("ServiceRegistry not initialized");
|
||||
}
|
||||
return ServiceRegistry.instance.register(name, service);
|
||||
}
|
||||
|
||||
static listServices() {
|
||||
if (!ServiceRegistry.instance) {
|
||||
throw new Error("ServiceRegistry not initialized");
|
||||
}
|
||||
return ServiceRegistry.instance.listServices();
|
||||
}
|
||||
}
|
||||
|
||||
export default new ServiceRegistry();
|
||||
export default ServiceRegistry;
|
||||
@@ -1,22 +1,14 @@
|
||||
const SERVICE_NAME = "SettingsService";
|
||||
|
||||
const envConfig = {
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
logLevel: process.env.LOG_LEVEL,
|
||||
systemEmailHost: process.env.SYSTEM_EMAIL_HOST,
|
||||
systemEmailPort: process.env.SYSTEM_EMAIL_PORT,
|
||||
systemEmailUser: process.env.SYSTEM_EMAIL_USER,
|
||||
systemEmailAddress: process.env.SYSTEM_EMAIL_ADDRESS,
|
||||
systemEmailPassword: process.env.SYSTEM_EMAIL_PASSWORD,
|
||||
jwtSecret: process.env.JWT_SECRET,
|
||||
jwtTTL: process.env.TOKEN_TTL,
|
||||
systemEmailHost: process.env.SYSTEM_EMAIL_HOST,
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
logLevel: process.env.LOG_LEVEL,
|
||||
clientHost: process.env.CLIENT_HOST,
|
||||
dbConnectionString: process.env.DB_CONNECTION_STRING,
|
||||
redisUrl: process.env.REDIS_URL,
|
||||
callbackUrl: process.env.CALLBACK_URL,
|
||||
port: process.env.PORT,
|
||||
pagespeedApiKey: process.env.PAGESPEED_API_KEY,
|
||||
uprockApiKey: process.env.UPROCK_API_KEY,
|
||||
};
|
||||
/**
|
||||
* SettingsService
|
||||
@@ -46,13 +38,7 @@ class SettingsService {
|
||||
loadSettings() {
|
||||
return this.settings;
|
||||
}
|
||||
/**
|
||||
* Reload settings by calling loadSettings.
|
||||
* @returns {Promise<Object>} The reloaded settings.
|
||||
*/
|
||||
reloadSettings() {
|
||||
return this.loadSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current settings.
|
||||
* @returns {Object} The current settings.
|
||||
|
||||
Reference in new issue
Block a user