move to src
This commit is contained in:
1 parent
acc910cbc0
commit
1c47c8ce2c
127 files changed
+71
-19
No files matched your search
@@ -0,0 +1,150 @@
|
||||
const SERVICE_NAME = "checkService";
|
||||
|
||||
class CheckService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, settingsService, stringService, errorService }) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.stringService = stringService;
|
||||
this.errorService = errorService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return CheckService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getChecksByMonitor = async ({ monitorId, query, teamId }) => {
|
||||
if (!monitorId) {
|
||||
throw this.errorService.createBadRequestError("No monitor ID in request");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
|
||||
if (!monitor) {
|
||||
throw this.errorService.createNotFoundError("Monitor not found");
|
||||
}
|
||||
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
|
||||
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
|
||||
const result = await this.db.getChecksByMonitor({
|
||||
monitorId,
|
||||
type,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
filter,
|
||||
ack,
|
||||
page,
|
||||
rowsPerPage,
|
||||
status,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
getChecksByTeam = async ({ teamId, query }) => {
|
||||
let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query;
|
||||
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const checkData = await this.db.getChecksByTeam({
|
||||
sortOrder,
|
||||
dateRange,
|
||||
filter,
|
||||
ack,
|
||||
page,
|
||||
rowsPerPage,
|
||||
teamId,
|
||||
});
|
||||
return checkData;
|
||||
};
|
||||
|
||||
getChecksSummaryByTeamId = async ({ teamId }) => {
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
|
||||
return summary;
|
||||
};
|
||||
|
||||
ackCheck = async ({ checkId, teamId, ack }) => {
|
||||
if (!checkId) {
|
||||
throw this.errorService.createBadRequestError("No check ID in request");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const updatedCheck = await this.db.ackCheck(checkId, teamId, ack);
|
||||
return updatedCheck;
|
||||
};
|
||||
|
||||
ackAllChecks = async ({ monitorId, path, teamId, ack }) => {
|
||||
if (path === "monitor") {
|
||||
if (!monitorId) {
|
||||
throw this.errorService.createBadRequestError("No monitor ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
if (!monitor) {
|
||||
throw this.errorService.createNotFoundError("Monitor not found");
|
||||
}
|
||||
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
}
|
||||
|
||||
const updatedChecks = await this.db.ackAllChecks(monitorId, teamId, ack, path);
|
||||
return updatedChecks;
|
||||
};
|
||||
|
||||
deleteChecks = async ({ monitorId, teamId }) => {
|
||||
if (!monitorId) {
|
||||
throw this.errorService.createBadRequestError("No monitor ID in request");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
|
||||
if (!monitor) {
|
||||
throw this.errorService.createNotFoundError("Monitor not found");
|
||||
}
|
||||
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
|
||||
const deletedCount = await this.db.deleteChecks(monitorId);
|
||||
return deletedCount;
|
||||
};
|
||||
deleteChecksByTeamId = async ({ teamId }) => {
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
const deletedCount = await this.db.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);
|
||||
};
|
||||
}
|
||||
|
||||
export default CheckService;
|
||||
@@ -0,0 +1,94 @@
|
||||
import v8 from "v8";
|
||||
import os from "os";
|
||||
|
||||
const SERVICE_NAME = "diagnosticService";
|
||||
|
||||
class DiagnosticService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* Performance Observer for monitoring system performance metrics.
|
||||
* Clears performance marks after each measurement to prevent memory leaks.
|
||||
*/
|
||||
const obs = new PerformanceObserver((items) => {
|
||||
// Get the first entry but we don't need to store it
|
||||
items.getEntries()[0];
|
||||
performance.clearMarks();
|
||||
});
|
||||
obs.observe({ entryTypes: ["measure"] });
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return DiagnosticService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getCPUUsage = async () => {
|
||||
const startUsage = process.cpuUsage();
|
||||
const timingPeriod = 1000; // measured in ms
|
||||
await new Promise((resolve) => setTimeout(resolve, timingPeriod));
|
||||
const endUsage = process.cpuUsage(startUsage);
|
||||
const cpuUsage = {
|
||||
userUsageMs: endUsage.user / 1000,
|
||||
systemUsageMs: endUsage.system / 1000,
|
||||
usagePercentage: ((endUsage.user + endUsage.system) / 1000 / timingPeriod) * 100,
|
||||
};
|
||||
return cpuUsage;
|
||||
};
|
||||
|
||||
getSystemStats = async () => {
|
||||
// Memory Usage
|
||||
const totalMemory = os.totalmem();
|
||||
const freeMemory = os.freemem();
|
||||
|
||||
const osStats = {
|
||||
freeMemoryBytes: freeMemory, // bytes
|
||||
totalMemoryBytes: totalMemory, // bytes
|
||||
};
|
||||
|
||||
const used = process.memoryUsage();
|
||||
const memoryUsage = {};
|
||||
for (let key in used) {
|
||||
memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB
|
||||
}
|
||||
|
||||
// CPU Usage
|
||||
const cpuMetrics = await this.getCPUUsage();
|
||||
|
||||
// V8 Heap Statistics
|
||||
const heapStats = v8.getHeapStatistics();
|
||||
const v8Metrics = {
|
||||
totalHeapSizeBytes: heapStats.total_heap_size, // bytes
|
||||
usedHeapSizeBytes: heapStats.used_heap_size, // bytes
|
||||
heapSizeLimitBytes: heapStats.heap_size_limit, // bytes
|
||||
};
|
||||
|
||||
// Event Loop Delay
|
||||
let eventLoopDelay = 0;
|
||||
performance.mark("start");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
performance.mark("end");
|
||||
performance.measure("eventLoopDelay", "start", "end");
|
||||
const entries = performance.getEntriesByName("eventLoopDelay");
|
||||
if (entries.length > 0) {
|
||||
eventLoopDelay = entries[0].duration;
|
||||
}
|
||||
|
||||
// Uptime
|
||||
const uptimeMs = process.uptime() * 1000; // ms
|
||||
|
||||
// Combine Metrics
|
||||
const diagnostics = {
|
||||
osStats,
|
||||
memoryUsage,
|
||||
cpuUsage: cpuMetrics,
|
||||
v8HeapStats: v8Metrics,
|
||||
eventLoopDelayMs: eventLoopDelay,
|
||||
uptimeMs,
|
||||
};
|
||||
|
||||
return diagnostics;
|
||||
};
|
||||
}
|
||||
|
||||
export default DiagnosticService;
|
||||
@@ -0,0 +1,44 @@
|
||||
const SERVICE_NAME = "inviteService";
|
||||
|
||||
class InviteService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, settingsService, emailService, stringService, errorService }) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.emailService = emailService;
|
||||
this.stringService = stringService;
|
||||
this.errorService = errorService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return InviteService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getInviteToken = async ({ invite, teamId }) => {
|
||||
invite.teamId = teamId;
|
||||
const inviteToken = await this.db.requestInviteToken(invite);
|
||||
return inviteToken;
|
||||
};
|
||||
|
||||
sendInviteEmail = async ({ inviteRequest, firstName }) => {
|
||||
const inviteToken = await this.db.requestInviteToken({ ...inviteRequest });
|
||||
const { clientHost } = this.settingsService.getSettings();
|
||||
|
||||
const html = await this.emailService.buildEmail("employeeActivationTemplate", {
|
||||
name: firstName,
|
||||
link: `${clientHost}/register/${inviteToken.token}`,
|
||||
});
|
||||
const result = await this.emailService.sendEmail(inviteRequest.email, "Welcome to Uptime Monitor", html);
|
||||
if (!result) {
|
||||
throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings.");
|
||||
}
|
||||
};
|
||||
|
||||
verifyInviteToken = async ({ inviteToken }) => {
|
||||
const invite = await this.db.getInviteToken(inviteToken);
|
||||
return invite;
|
||||
};
|
||||
}
|
||||
|
||||
export default InviteService;
|
||||
@@ -0,0 +1,66 @@
|
||||
const SERVICE_NAME = "maintenanceWindowService";
|
||||
|
||||
class MaintenanceWindowService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, settingsService, stringService, errorService }) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.stringService = stringService;
|
||||
this.errorService = errorService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return MaintenanceWindowService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
createMaintenanceWindow = async ({ teamId, body }) => {
|
||||
const monitorIds = body.monitors;
|
||||
const monitors = await this.db.getMonitorsByIds(monitorIds);
|
||||
|
||||
const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId));
|
||||
|
||||
if (unauthorizedMonitors.length > 0) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
|
||||
const dbTransactions = monitorIds.map((monitorId) => {
|
||||
return this.db.createMaintenanceWindow({
|
||||
teamId,
|
||||
monitorId,
|
||||
name: body.name,
|
||||
active: body.active ? body.active : true,
|
||||
repeat: body.repeat,
|
||||
start: body.start,
|
||||
end: body.end,
|
||||
});
|
||||
});
|
||||
await Promise.all(dbTransactions);
|
||||
};
|
||||
|
||||
getMaintenanceWindowById = async ({ id, teamId }) => {
|
||||
const maintenanceWindow = await this.db.getMaintenanceWindowById({ id, teamId });
|
||||
return maintenanceWindow;
|
||||
};
|
||||
|
||||
getMaintenanceWindowsByTeamId = async ({ teamId, query }) => {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId(teamId, query);
|
||||
return maintenanceWindows;
|
||||
};
|
||||
|
||||
getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => {
|
||||
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId({ monitorId, teamId });
|
||||
return maintenanceWindows;
|
||||
};
|
||||
|
||||
deleteMaintenanceWindow = async ({ id, teamId }) => {
|
||||
await this.db.deleteMaintenanceWindowById({ id, teamId });
|
||||
};
|
||||
|
||||
editMaintenanceWindow = async ({ id, teamId, body }) => {
|
||||
const editedMaintenanceWindow = await this.db.editMaintenanceWindowById({ id, body, teamId });
|
||||
return editedMaintenanceWindow;
|
||||
};
|
||||
}
|
||||
|
||||
export default MaintenanceWindowService;
|
||||
@@ -0,0 +1,266 @@
|
||||
import { createMonitorsBodyValidation } from "../../validation/joi.js";
|
||||
|
||||
const SERVICE_NAME = "MonitorService";
|
||||
class MonitorService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService }) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.jobQueue = jobQueue;
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
this.papaparse = papaparse;
|
||||
this.logger = logger;
|
||||
this.errorService = errorService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return MonitorService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
verifyTeamAccess = async ({ teamId, monitorId }) => {
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
if (!monitor?.teamId?.equals(teamId)) {
|
||||
throw this.errorService.createAuthorizationError();
|
||||
}
|
||||
};
|
||||
|
||||
getAllMonitors = async () => {
|
||||
const monitors = await this.db.getAllMonitors();
|
||||
return monitors;
|
||||
};
|
||||
|
||||
getUptimeDetailsById = async ({ teamId, monitorId, dateRange, normalize }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const data = await this.db.getUptimeDetailsById({
|
||||
monitorId,
|
||||
dateRange,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
getMonitorStatsById = async ({ teamId, monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitorStats = await this.db.getMonitorStatsById({
|
||||
monitorId,
|
||||
limit,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return monitorStats;
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange });
|
||||
|
||||
return monitor;
|
||||
};
|
||||
|
||||
getMonitorById = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
|
||||
return monitor;
|
||||
};
|
||||
|
||||
createMonitor = async ({ teamId, userId, body }) => {
|
||||
const monitor = await this.db.createMonitor({
|
||||
body,
|
||||
teamId,
|
||||
userId,
|
||||
});
|
||||
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
};
|
||||
|
||||
createBulkMonitors = async ({ fileData, userId, teamId }) => {
|
||||
const { parse } = this.papaparse;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
parse(fileData, {
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
transform: (value, header) => {
|
||||
if (value === "") return undefined; // Empty fields become undefined
|
||||
|
||||
// Handle 'port' and 'interval' fields, check if they're valid numbers
|
||||
if (["port", "interval"].includes(header)) {
|
||||
const num = parseInt(value, 10);
|
||||
if (isNaN(num)) {
|
||||
throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
complete: async ({ data, errors }) => {
|
||||
try {
|
||||
if (errors.length > 0) {
|
||||
throw this.errorService.createServerError("Error parsing CSV");
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
throw this.errorService.createServerError("CSV file contains no data rows");
|
||||
}
|
||||
|
||||
const enrichedData = data.map((monitor) => ({
|
||||
userId,
|
||||
teamId,
|
||||
...monitor,
|
||||
description: monitor.description || monitor.name || monitor.url,
|
||||
name: monitor.name || monitor.url,
|
||||
type: monitor.type || "http",
|
||||
}));
|
||||
|
||||
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
||||
|
||||
const monitors = await this.db.createBulkMonitors(enrichedData);
|
||||
|
||||
await Promise.all(
|
||||
monitors.map(async (monitor) => {
|
||||
this.jobQueue.addJob(monitor._id, monitor);
|
||||
})
|
||||
);
|
||||
|
||||
resolve(monitors);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
deleteMonitor = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.deleteMonitor({ teamId, monitorId });
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.db.deleteStatusPagesByMonitorId(monitor._id);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
deleteAllMonitors = async ({ teamId }) => {
|
||||
const { monitors, deletedCount } = await this.db.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);
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteAllMonitors",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
return deletedCount;
|
||||
};
|
||||
|
||||
editMonitor = async ({ teamId, monitorId, body }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const editedMonitor = await this.db.editMonitor({ monitorId, body });
|
||||
await this.jobQueue.updateJob(editedMonitor);
|
||||
};
|
||||
|
||||
pauseMonitor = async ({ teamId, monitorId }) => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.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);
|
||||
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor)));
|
||||
return demoMonitors;
|
||||
};
|
||||
|
||||
sendTestEmail = async ({ to }) => {
|
||||
const subject = this.stringService.testEmailSubject;
|
||||
const context = { testName: "Monitoring System" };
|
||||
|
||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
||||
const messageId = await this.emailService.sendEmail(to, subject, html);
|
||||
|
||||
if (!messageId) {
|
||||
throw this.errorService.createServerError("Failed to send test email.");
|
||||
}
|
||||
|
||||
return messageId;
|
||||
};
|
||||
|
||||
getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => {
|
||||
const monitors = await this.db.getMonitorsByTeamId({
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
teamId,
|
||||
});
|
||||
return monitors;
|
||||
};
|
||||
|
||||
getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => {
|
||||
const result = await this.db.getMonitorsAndSummaryByTeamId({
|
||||
type,
|
||||
explain,
|
||||
teamId,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => {
|
||||
const result = await this.db.getMonitorsWithChecksByTeamId({
|
||||
limit,
|
||||
type,
|
||||
page,
|
||||
rowsPerPage,
|
||||
filter,
|
||||
field,
|
||||
order,
|
||||
teamId,
|
||||
explain,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
exportMonitorsToCSV = async ({ teamId }) => {
|
||||
const monitors = await this.db.getMonitorsByTeamId({ teamId });
|
||||
|
||||
if (!monitors || monitors.length === 0) {
|
||||
throw this.errorService.createNotFoundError("No monitors to export");
|
||||
}
|
||||
|
||||
const csvData = monitors?.filteredMonitors?.map((monitor) => ({
|
||||
name: monitor.name,
|
||||
description: monitor.description,
|
||||
type: monitor.type,
|
||||
url: monitor.url,
|
||||
interval: monitor.interval,
|
||||
port: monitor.port,
|
||||
ignoreTlsErrors: monitor.ignoreTlsErrors,
|
||||
isActive: monitor.isActive,
|
||||
}));
|
||||
|
||||
const csv = this.papaparse.unparse(csvData);
|
||||
return csv;
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorService;
|
||||
@@ -0,0 +1,213 @@
|
||||
const SERVICE_NAME = "userService";
|
||||
|
||||
class UserService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ db, emailService, settingsService, logger, stringService, jwt, errorService }) {
|
||||
this.db = db;
|
||||
this.emailService = emailService;
|
||||
this.settingsService = settingsService;
|
||||
this.logger = logger;
|
||||
this.stringService = stringService;
|
||||
this.jwt = jwt;
|
||||
this.errorService = errorService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return UserService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
issueToken = (payload, appSettings) => {
|
||||
const tokenTTL = appSettings?.jwtTTL ?? "2h";
|
||||
const tokenSecret = appSettings?.jwtSecret;
|
||||
const payloadData = payload;
|
||||
return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
|
||||
};
|
||||
|
||||
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();
|
||||
if (superAdminExists) {
|
||||
const invitedUser = await this.db.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 newUser = await this.db.insertUser({ ...user }, file);
|
||||
|
||||
this.logger.debug({
|
||||
message: "New user created",
|
||||
service: SERVICE_NAME,
|
||||
method: "registerUser",
|
||||
details: newUser._id,
|
||||
});
|
||||
|
||||
const userForToken = { ...newUser._doc };
|
||||
delete userForToken.profileImage;
|
||||
delete userForToken.avatarImage;
|
||||
|
||||
const appSettings = await this.settingsService.getSettings();
|
||||
|
||||
const token = this.issueToken(userForToken, appSettings);
|
||||
|
||||
try {
|
||||
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
|
||||
name: newUser.firstName,
|
||||
});
|
||||
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "registerUser",
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "registerUser",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
|
||||
return { user: newUser, token };
|
||||
};
|
||||
|
||||
loginUser = async (email, password) => {
|
||||
// Check if user exists
|
||||
const user = await this.db.getUserByEmail(email);
|
||||
// Compare password
|
||||
const match = await user.comparePassword(password);
|
||||
if (match !== true) {
|
||||
throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword);
|
||||
}
|
||||
|
||||
// Remove password from user object. Should this be abstracted to DB layer?
|
||||
const userWithoutPassword = { ...user._doc };
|
||||
delete userWithoutPassword.password;
|
||||
delete userWithoutPassword.avatarImage;
|
||||
|
||||
// Happy path, return token
|
||||
const appSettings = await this.settingsService.getSettings();
|
||||
const token = this.issueToken(userWithoutPassword, appSettings);
|
||||
// reset avatar image
|
||||
userWithoutPassword.avatarImage = user.avatarImage;
|
||||
return { user: userWithoutPassword, token };
|
||||
};
|
||||
|
||||
editUser = async (updates, file, currentUser) => {
|
||||
// Change Password check
|
||||
if (updates?.password && updates?.newPassword) {
|
||||
// Get user's email
|
||||
// Add user email to body for DB operation
|
||||
updates.email = currentUser.email;
|
||||
// Get user
|
||||
const user = await this.db.getUserByEmail(currentUser.email);
|
||||
// Compare passwords
|
||||
const match = await user.comparePassword(updates?.password);
|
||||
// If not a match, throw a 403
|
||||
// 403 instead of 401 to avoid triggering axios interceptor
|
||||
if (!match) {
|
||||
throw this.errorService.createAuthorizationError(this.stringService.authIncorrectPassword);
|
||||
}
|
||||
// If a match, update the password
|
||||
updates.password = updates.newPassword;
|
||||
}
|
||||
|
||||
const updatedUser = await this.db.updateUser({ userId: currentUser?._id, user: updates, file: file });
|
||||
return updatedUser;
|
||||
};
|
||||
|
||||
checkSuperadminExists = async () => {
|
||||
const superAdminExists = await this.db.checkSuperadmin();
|
||||
return superAdminExists;
|
||||
};
|
||||
|
||||
requestRecovery = async (email) => {
|
||||
const user = await this.db.getUserByEmail(email);
|
||||
const recoveryToken = await this.db.requestRecoveryToken(email);
|
||||
const name = user.firstName;
|
||||
const { clientHost } = this.settingsService.getSettings();
|
||||
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
|
||||
|
||||
const html = await this.emailService.buildEmail("passwordResetTemplate", {
|
||||
name,
|
||||
email,
|
||||
url,
|
||||
});
|
||||
const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html);
|
||||
return msgId;
|
||||
};
|
||||
|
||||
validateRecovery = async (recoveryToken) => {
|
||||
await this.db.validateRecoveryToken(recoveryToken);
|
||||
};
|
||||
|
||||
resetPassword = async (password, recoveryToken) => {
|
||||
const user = await this.db.resetPassword(password, recoveryToken);
|
||||
const appSettings = await this.settingsService.getSettings();
|
||||
const token = this.issueToken(user._doc, appSettings);
|
||||
return { user, token };
|
||||
};
|
||||
|
||||
deleteUser = async (user) => {
|
||||
const email = user?.email;
|
||||
if (!email) {
|
||||
throw this.errorService.createBadRequestError("No email in request");
|
||||
}
|
||||
|
||||
const teamId = user?.teamId;
|
||||
const userId = user?._id;
|
||||
|
||||
if (!teamId) {
|
||||
throw this.errorService.createBadRequestError("No team ID in request");
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw this.errorService.createBadRequestError("No user ID in request");
|
||||
}
|
||||
|
||||
const roles = user?.role;
|
||||
if (roles.includes("demo")) {
|
||||
throw this.errorService.createBadRequestError("Demo user cannot be deleted");
|
||||
}
|
||||
|
||||
// 1. Find all the monitors associated with the team ID if superadmin
|
||||
const result = await this.db.getMonitorsByTeamId({
|
||||
teamId: teamId,
|
||||
});
|
||||
|
||||
if (roles.includes("superadmin")) {
|
||||
// 2. Remove all jobs, delete checks and alerts
|
||||
result?.monitors.length > 0 &&
|
||||
(await Promise.all(
|
||||
result.monitors.map(async (monitor) => {
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
})
|
||||
));
|
||||
}
|
||||
// 6. Delete the user by id
|
||||
await this.db.deleteUser(userId);
|
||||
};
|
||||
|
||||
getAllUsers = async () => {
|
||||
const users = await this.db.getAllUsers();
|
||||
return users;
|
||||
};
|
||||
|
||||
getUserById = async (roles, userId) => {
|
||||
const user = await this.db.getUserById(roles, userId);
|
||||
return user;
|
||||
};
|
||||
|
||||
editUserById = async (userId, user) => {
|
||||
await this.db.editUserById(userId, user);
|
||||
};
|
||||
}
|
||||
export default UserService;
|
||||
@@ -0,0 +1,64 @@
|
||||
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;
|
||||
@@ -0,0 +1,321 @@
|
||||
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;
|
||||
@@ -0,0 +1,308 @@
|
||||
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;
|
||||
@@ -0,0 +1,213 @@
|
||||
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;
|
||||
@@ -0,0 +1,104 @@
|
||||
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;
|
||||
@@ -0,0 +1,170 @@
|
||||
import Scheduler from "super-simple-scheduler";
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
|
||||
class SuperSimpleQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ appSettings, db, logger, helper }) {
|
||||
this.appSettings = appSettings;
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.helper = helper;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return SuperSimpleQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create({ appSettings, db, logger, helper }) {
|
||||
const instance = new SuperSimpleQueue({ appSettings, db, logger, helper });
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
try {
|
||||
this.scheduler = new Scheduler({
|
||||
// storeType: "mongo",
|
||||
// storeType: "redis",
|
||||
logLevel: "debug",
|
||||
debug: true,
|
||||
dbUri: this.appSettings.dbConnectionString,
|
||||
});
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.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 SuperSimpleQueue",
|
||||
service: SERVICE_NAME,
|
||||
method: "init",
|
||||
details: error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
addJob = async (monitorId, monitor) => {
|
||||
this.scheduler.addJob({
|
||||
id: monitorId.toString(),
|
||||
template: "monitor-job",
|
||||
repeat: monitor.interval,
|
||||
data: monitor.toObject(),
|
||||
});
|
||||
};
|
||||
|
||||
deleteJob = async (monitor) => {
|
||||
this.scheduler.removeJob(monitor._id.toString());
|
||||
};
|
||||
|
||||
pauseJob = async (monitor) => {
|
||||
const result = this.scheduler.pauseJob(monitor._id.toString());
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Paused monitor ${monitor._id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "pauseJob",
|
||||
});
|
||||
};
|
||||
|
||||
resumeJob = async (monitor) => {
|
||||
const result = this.scheduler.resumeJob(monitor._id.toString());
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Resumed monitor ${monitor._id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "resumeJob",
|
||||
});
|
||||
};
|
||||
|
||||
updateJob = async (monitor) => {
|
||||
this.scheduler.updateJob(monitor._id.toString(), monitor.interval);
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
this.scheduler.stop();
|
||||
};
|
||||
|
||||
getMetrics = async () => {
|
||||
const jobs = await this.scheduler.getJobs();
|
||||
const metrics = jobs.reduce(
|
||||
(acc, job) => {
|
||||
acc.totalRuns += job.runCount || 0;
|
||||
acc.totalFailures += job.failCount || 0;
|
||||
acc.jobs++;
|
||||
if (job.failCount > 0 && job.lastFailedAt >= job.lsatRunAt) {
|
||||
acc.failingJobs++;
|
||||
}
|
||||
|
||||
if (job.lockedAt) {
|
||||
acc.activeJobs++;
|
||||
}
|
||||
|
||||
if (job.failCount > 0) {
|
||||
acc.jobsWithFailures.push({
|
||||
monitorId: job.id,
|
||||
monitorUrl: job?.data?.url || null,
|
||||
monitorType: job?.data?.type || null,
|
||||
failedAt: job.lastFailedAt,
|
||||
failCount: job.failCount,
|
||||
failReason: job.lastFailReason,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
jobs: 0,
|
||||
activeJobs: 0,
|
||||
failingJobs: 0,
|
||||
jobsWithFailures: [],
|
||||
totalRuns: 0,
|
||||
totalFailures: 0,
|
||||
}
|
||||
);
|
||||
return metrics;
|
||||
};
|
||||
|
||||
getJobs = async () => {
|
||||
const jobs = await this.scheduler.getJobs();
|
||||
return jobs.map((job) => {
|
||||
return {
|
||||
monitorId: job.id,
|
||||
monitorUrl: job?.data?.url || null,
|
||||
monitorType: job?.data?.type || null,
|
||||
active: job.active,
|
||||
lockedAt: job.lockedAt,
|
||||
runCount: job.runCount || 0,
|
||||
failCount: job.failCount || 0,
|
||||
failReason: job.lastFailReason,
|
||||
lastRunAt: job.lastRunAt,
|
||||
lastFinishedAt: job.lastFinishedAt,
|
||||
lastRunTook: job.lockedAt ? null : job.lastFinishedAt - job.lastRunAt,
|
||||
lastFailedAt: job.lastFailedAt,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
flushQueues = async () => {
|
||||
const stopRes = this.scheduler.stop();
|
||||
const flushRes = this.scheduler.flushJobs();
|
||||
const initRes = await this.init();
|
||||
return {
|
||||
success: stopRes && flushRes && initRes,
|
||||
};
|
||||
};
|
||||
|
||||
obliterate = async () => {
|
||||
console.log("obliterate not implemented");
|
||||
};
|
||||
}
|
||||
|
||||
export default SuperSimpleQueue;
|
||||
@@ -0,0 +1,100 @@
|
||||
const SERVICE_NAME = "JobQueueHelper";
|
||||
|
||||
class SuperSimpleQueueHelper {
|
||||
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 SuperSimpleQueueHelper.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getMonitorJob = () => {
|
||||
return async (monitor) => {
|
||||
try {
|
||||
const monitorId = 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 ${monitor._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 SuperSimpleQueueHelper;
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
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",
|
||||
port: "checks",
|
||||
docker: "checks",
|
||||
pagespeed: "pagespeedChecks",
|
||||
hardware: "hardwareChecks",
|
||||
};
|
||||
|
||||
class BufferService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor({ db, logger }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.buffers = {
|
||||
checks: [],
|
||||
pagespeedChecks: [],
|
||||
hardwareChecks: [],
|
||||
};
|
||||
this.OPERATION_MAP = {
|
||||
checks: this.db.createChecks,
|
||||
pagespeedChecks: this.db.createPageSpeedChecks,
|
||||
hardwareChecks: this.db.createHardwareChecks,
|
||||
};
|
||||
|
||||
this.scheduleNextFlush();
|
||||
this.logger.info({
|
||||
message: `Buffer service initialized, flushing every ${BUFFER_TIMEOUT / 1000}s`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "constructor",
|
||||
});
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return BufferService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
addToBuffer({ check, type }) {
|
||||
try {
|
||||
this.buffers[TYPE_MAP[type]].push(check);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "addToBuffer",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scheduleNextFlush() {
|
||||
this.bufferTimer = setTimeout(async () => {
|
||||
try {
|
||||
await this.flushBuffers();
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: `Error in flush cycle: ${error.message}`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "scheduleNextFlush",
|
||||
stack: error.stack,
|
||||
});
|
||||
} finally {
|
||||
// Schedule the next flush only after the current one completes
|
||||
this.scheduleNextFlush();
|
||||
}
|
||||
}, BUFFER_TIMEOUT);
|
||||
}
|
||||
async flushBuffers() {
|
||||
let items = 0;
|
||||
for (const [bufferName, buffer] of Object.entries(this.buffers)) {
|
||||
items += buffer.length;
|
||||
const operation = this.OPERATION_MAP[bufferName];
|
||||
if (!operation) {
|
||||
this.logger.error({
|
||||
message: `No operation found for ${bufferName}`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffers",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await operation(buffer);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffers",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
this.buffers[bufferName] = [];
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Flushed ${items} items`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffers",
|
||||
});
|
||||
items = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default BufferService;
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const SERVICE_NAME = "EmailService";
|
||||
|
||||
/**
|
||||
* Represents an email service that can load templates, build, and send emails.
|
||||
*/
|
||||
class EmailService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the EmailService, initializing template loaders and the email transporter.
|
||||
* @param {Object} settingsService - The settings service to get email configuration.
|
||||
* @param {Object} fs - The file system module.
|
||||
* @param {Object} path - The path module.
|
||||
* @param {Function} compile - The Handlebars compile function.
|
||||
* @param {Function} mjml2html - The MJML to HTML conversion function.
|
||||
* @param {Object} nodemailer - The nodemailer module.
|
||||
* @param {Object} logger - The logger module.
|
||||
*/
|
||||
constructor(settingsService, fs, path, compile, mjml2html, nodemailer, logger) {
|
||||
this.settingsService = settingsService;
|
||||
this.fs = fs;
|
||||
this.path = path;
|
||||
this.compile = compile;
|
||||
this.mjml2html = mjml2html;
|
||||
this.nodemailer = nodemailer;
|
||||
this.logger = logger;
|
||||
this.init();
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return EmailService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
/**
|
||||
* Loads an email template from the filesystem.
|
||||
*
|
||||
* @param {string} templateName - The name of the template to load.
|
||||
* @returns {Function} A compiled template function that can be used to generate HTML email content.
|
||||
*/
|
||||
this.loadTemplate = (templateName) => {
|
||||
try {
|
||||
const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`);
|
||||
const templateContent = this.fs.readFileSync(templatePath, "utf8");
|
||||
return this.compile(templateContent);
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "loadTemplate",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A lookup object to access preloaded email templates.
|
||||
* @type {Object.<string, Function>}
|
||||
* TODO Load less used templates in their respective functions
|
||||
*/
|
||||
this.templateLookup = {
|
||||
welcomeEmailTemplate: this.loadTemplate("welcomeEmail"),
|
||||
employeeActivationTemplate: this.loadTemplate("employeeActivation"),
|
||||
noIncidentsThisWeekTemplate: this.loadTemplate("noIncidentsThisWeek"),
|
||||
serverIsDownTemplate: this.loadTemplate("serverIsDown"),
|
||||
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
|
||||
passwordResetTemplate: this.loadTemplate("passwordReset"),
|
||||
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
|
||||
testEmailTemplate: this.loadTemplate("testEmailTemplate"),
|
||||
};
|
||||
|
||||
/**
|
||||
* The email transporter used to send emails.
|
||||
* @type {Object}
|
||||
*/
|
||||
};
|
||||
|
||||
buildEmail = async (template, context) => {
|
||||
try {
|
||||
const mjml = this.templateLookup[template](context);
|
||||
const html = await this.mjml2html(mjml);
|
||||
return html.html;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "buildEmail",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
sendEmail = async (to, subject, html, transportConfig) => {
|
||||
let config;
|
||||
if (typeof transportConfig !== "undefined") {
|
||||
config = transportConfig;
|
||||
} else {
|
||||
config = await this.settingsService.getDBSettings();
|
||||
}
|
||||
const {
|
||||
systemEmailHost,
|
||||
systemEmailPort,
|
||||
systemEmailSecure,
|
||||
systemEmailPool,
|
||||
systemEmailUser,
|
||||
systemEmailAddress,
|
||||
systemEmailPassword,
|
||||
systemEmailConnectionHost,
|
||||
systemEmailTLSServername,
|
||||
systemEmailIgnoreTLS,
|
||||
systemEmailRequireTLS,
|
||||
systemEmailRejectUnauthorized,
|
||||
} = config;
|
||||
|
||||
const emailConfig = {
|
||||
host: systemEmailHost,
|
||||
port: Number(systemEmailPort),
|
||||
secure: systemEmailSecure,
|
||||
auth: {
|
||||
user: systemEmailUser || systemEmailAddress,
|
||||
pass: systemEmailPassword,
|
||||
},
|
||||
name: systemEmailConnectionHost || "localhost",
|
||||
connectionTimeout: 5000,
|
||||
pool: systemEmailPool,
|
||||
tls: {
|
||||
rejectUnauthorized: systemEmailRejectUnauthorized,
|
||||
ignoreTLS: systemEmailIgnoreTLS,
|
||||
requireTLS: systemEmailRequireTLS,
|
||||
servername: systemEmailTLSServername,
|
||||
},
|
||||
};
|
||||
this.transporter = this.nodemailer.createTransport(emailConfig);
|
||||
|
||||
try {
|
||||
await this.transporter.verify();
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: "Email transporter verification failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "verifyTransporter",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await this.transporter.sendMail({
|
||||
to: to,
|
||||
from: systemEmailAddress,
|
||||
subject: subject,
|
||||
html: html,
|
||||
});
|
||||
return info?.messageId;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "sendEmail",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default EmailService;
|
||||
@@ -0,0 +1,101 @@
|
||||
export class AppError extends Error {
|
||||
constructor(message, status = 500, service = null, method = null, details = null) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.service = service;
|
||||
this.method = method;
|
||||
this.details = details;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends AppError {
|
||||
constructor(message, details = null, service = null, method = null) {
|
||||
super(message, 422, service, method, details);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthenticationError extends AppError {
|
||||
constructor(message, details = null, service = null, method = null) {
|
||||
super(message, 401, service, method, details);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorizationError extends AppError {
|
||||
constructor(message, details = null, service = null, method = null) {
|
||||
super(message, 403, service, method, details);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends AppError {
|
||||
constructor(message, details = null, service = null, method = null) {
|
||||
super(message, 404, service, method, details);
|
||||
}
|
||||
}
|
||||
|
||||
class ConflictError extends AppError {
|
||||
constructor(message, details = null, service = null, method = null) {
|
||||
super(message, 409, service, method, details);
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseError extends AppError {
|
||||
constructor(message, details = null, service = null, method = null) {
|
||||
super(message, 500, service, method, details);
|
||||
}
|
||||
}
|
||||
|
||||
class BadRequestError extends AppError {
|
||||
constructor(message, details = null, service = null, method = null) {
|
||||
super(message, 400, service, method, details);
|
||||
}
|
||||
}
|
||||
|
||||
const SERVICE_NAME = "ErrorService";
|
||||
class ErrorService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor() {}
|
||||
|
||||
get serviceName() {
|
||||
return ErrorService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
createError = (message, status = 500, service = null, method = null, details = null) => {
|
||||
return new AppError(message, status, service, method, details);
|
||||
};
|
||||
|
||||
createValidationError = (message, details = null, service = null, method = null) => {
|
||||
return new ValidationError(message, details, service, method);
|
||||
};
|
||||
|
||||
createAuthenticationError = (message = "Unauthorized", details = null, service = null, method = null) => {
|
||||
return new AuthenticationError(message, details, service, method);
|
||||
};
|
||||
|
||||
createAuthorizationError = (message, details = null, service = null, method = null) => {
|
||||
return new AuthorizationError(message, details, service, method);
|
||||
};
|
||||
|
||||
createNotFoundError = (message, details = null, service = null, method = null) => {
|
||||
return new NotFoundError(message, details, service, method);
|
||||
};
|
||||
|
||||
createConflictError = (message, details = null, service = null, method = null) => {
|
||||
return new ConflictError(message, details, service, method);
|
||||
};
|
||||
|
||||
createDatabaseError = (message, details = null, service = null, method = null) => {
|
||||
return new DatabaseError(message, details, service, method);
|
||||
};
|
||||
|
||||
createServerError = (message, details = null, service = null, method = null) => {
|
||||
return this.createError(message, 500, service, method, details);
|
||||
};
|
||||
|
||||
createBadRequestError = (message = "BadRequest", details = null, service = null, method = null) => {
|
||||
return new BadRequestError(message, details, service, method);
|
||||
};
|
||||
}
|
||||
|
||||
export default ErrorService;
|
||||
+503
@@ -0,0 +1,503 @@
|
||||
import jmespath from "jmespath";
|
||||
import https from "https";
|
||||
|
||||
const SERVICE_NAME = "NetworkService";
|
||||
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
|
||||
|
||||
/**
|
||||
* Constructs a new NetworkService instance.
|
||||
*
|
||||
* @param {Object} axios - The axios instance for HTTP requests.
|
||||
* @param {Object} ping - The ping utility for network checks.
|
||||
* @param {Object} logger - The logger instance for logging.
|
||||
* @param {Object} http - The HTTP utility for network operations.
|
||||
* @param {Object} net - The net utility for network operations.
|
||||
*/
|
||||
class NetworkService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor(axios, ping, logger, http, Docker, net, stringService, settingsService) {
|
||||
this.TYPE_PING = "ping";
|
||||
this.TYPE_HTTP = "http";
|
||||
this.TYPE_PAGESPEED = "pagespeed";
|
||||
this.TYPE_HARDWARE = "hardware";
|
||||
this.TYPE_DOCKER = "docker";
|
||||
this.TYPE_PORT = "port";
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.NETWORK_ERROR = 5000;
|
||||
this.PING_ERROR = 5001;
|
||||
this.axios = axios;
|
||||
this.ping = ping;
|
||||
this.logger = logger;
|
||||
this.http = http;
|
||||
this.Docker = Docker;
|
||||
this.net = net;
|
||||
this.stringService = stringService;
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return NetworkService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Times the execution of an asynchronous operation.
|
||||
*
|
||||
* @param {Function} operation - The asynchronous operation to be timed.
|
||||
* @returns {Promise<Object>} An object containing the response, response time, and optionally an error.
|
||||
* @property {Object|null} response - The response from the operation, or null if an error occurred.
|
||||
* @property {number} responseTime - The time taken for the operation to complete, in milliseconds.
|
||||
* @property {Error} [error] - The error object if an error occurred during the operation.
|
||||
*/
|
||||
async timeRequest(operation) {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const response = await operation();
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
return { response, responseTime };
|
||||
} catch (error) {
|
||||
const endTime = Date.now();
|
||||
const responseTime = endTime - startTime;
|
||||
return { response: null, responseTime, error };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a ping check to a specified host to verify its availability.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The host URL to ping
|
||||
* @param {string} monitor._id - The unique identifier of the monitor
|
||||
* @returns {Promise<Object>} A promise that resolves to a ping response object
|
||||
* @returns {string} pingResponse.monitorId - The ID of the monitor
|
||||
* @returns {string} pingResponse.type - The type of monitor (always "ping")
|
||||
* @returns {number} pingResponse.responseTime - The time taken for the ping
|
||||
* @returns {Object} pingResponse.payload - The raw ping response data
|
||||
* @returns {boolean} pingResponse.status - Whether the host is alive (true) or not (false)
|
||||
* @returns {number} pingResponse.code - Status code (200 for success, PING_ERROR for failure)
|
||||
* @returns {string} pingResponse.message - Success or failure message
|
||||
* @throws {Error} If there's an error during the ping operation
|
||||
*/
|
||||
async requestPing(monitor) {
|
||||
try {
|
||||
const url = monitor.url;
|
||||
const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(url));
|
||||
|
||||
const pingResponse = {
|
||||
monitorId: monitor._id,
|
||||
type: "ping",
|
||||
responseTime,
|
||||
payload: response,
|
||||
};
|
||||
if (error) {
|
||||
pingResponse.status = false;
|
||||
pingResponse.code = this.PING_ERROR;
|
||||
pingResponse.message = "No response";
|
||||
return pingResponse;
|
||||
}
|
||||
|
||||
pingResponse.code = 200;
|
||||
pingResponse.status = response.alive;
|
||||
pingResponse.message = "Success";
|
||||
return pingResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestPing";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an HTTP GET request to a specified URL with optional validation of response data.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The URL to make the HTTP request to
|
||||
* @param {string} [monitor.secret] - Optional Bearer token for authentication
|
||||
* @param {string} monitor._id - The unique identifier of the monitor
|
||||
* @param {string} monitor.name - The name of the monitor
|
||||
* @param {string} monitor.teamId - The team ID associated with the monitor
|
||||
* @param {string} monitor.type - The type of monitor
|
||||
* @param {boolean} [monitor.ignoreTlsErrors] - Whether to ignore TLS certificate errors
|
||||
* @param {string} [monitor.jsonPath] - Optional JMESPath expression to extract data from JSON response
|
||||
* @param {string} [monitor.matchMethod] - Method to match response data ('include', 'regex', or exact match)
|
||||
* @param {string} [monitor.expectedValue] - Expected value to match against response data
|
||||
* @returns {Promise<Object>} A promise that resolves to an HTTP response object
|
||||
* @returns {string} httpResponse.monitorId - The ID of the monitor
|
||||
* @returns {string} httpResponse.teamId - The team ID
|
||||
* @returns {string} httpResponse.type - The type of monitor
|
||||
* @returns {number} httpResponse.responseTime - The time taken for the request
|
||||
* @returns {Object} httpResponse.payload - The response data
|
||||
* @returns {boolean} httpResponse.status - Whether the request was successful and matched expected value (if specified)
|
||||
* @returns {number} httpResponse.code - HTTP status code or NETWORK_ERROR
|
||||
* @returns {string} httpResponse.message - Success or failure message
|
||||
* @throws {Error} If there's an error during the HTTP request or data validation
|
||||
*/
|
||||
async requestHttp(monitor) {
|
||||
try {
|
||||
const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
|
||||
const config = {};
|
||||
|
||||
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
|
||||
|
||||
if (ignoreTlsErrors === true) {
|
||||
config.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
}
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(() => this.axios.get(url, config));
|
||||
|
||||
const httpResponse = {
|
||||
monitorId: _id,
|
||||
teamId,
|
||||
type,
|
||||
responseTime,
|
||||
payload: response?.data,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
const code = error.response?.status || this.NETWORK_ERROR;
|
||||
httpResponse.code = code;
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.http.STATUS_CODES[code] || this.stringService.httpNetworkError;
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
httpResponse.code = response.status;
|
||||
|
||||
if (!expectedValue) {
|
||||
// not configure expected value, return
|
||||
httpResponse.status = true;
|
||||
httpResponse.message = this.http.STATUS_CODES[response.status];
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
// validate if response data match expected value
|
||||
let result = response?.data;
|
||||
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "requestHttp",
|
||||
message: `Job: [${name}](${_id}) match result with expected value`,
|
||||
details: { expectedValue, result, jsonPath, matchMethod },
|
||||
});
|
||||
|
||||
if (jsonPath) {
|
||||
const contentType = response.headers["content-type"];
|
||||
|
||||
const isJson = contentType?.includes("application/json");
|
||||
if (!isJson) {
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.stringService.httpNotJson;
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
try {
|
||||
result = jmespath.search(result, jsonPath);
|
||||
} catch (error) {
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.stringService.httpJsonPathError;
|
||||
return httpResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (result === null || result === undefined) {
|
||||
httpResponse.status = false;
|
||||
httpResponse.message = this.stringService.httpEmptyResult;
|
||||
return httpResponse;
|
||||
}
|
||||
|
||||
let match;
|
||||
result = typeof result === "object" ? JSON.stringify(result) : result.toString();
|
||||
if (matchMethod === "include") match = result.includes(expectedValue);
|
||||
else if (matchMethod === "regex") match = new RegExp(expectedValue).test(result);
|
||||
else match = result === expectedValue;
|
||||
|
||||
httpResponse.status = match;
|
||||
httpResponse.message = match ? this.stringService.httpMatchSuccess : this.stringService.httpMatchFail;
|
||||
return httpResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestHttp";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the performance of a webpage using Google's PageSpeed Insights API.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The URL of the webpage to analyze
|
||||
* @returns {Promise<Object|undefined>} A promise that resolves to a pagespeed response object or undefined if API key is missing
|
||||
* @returns {string} response.monitorId - The ID of the monitor
|
||||
* @returns {string} response.type - The type of monitor
|
||||
* @returns {number} response.responseTime - The time taken for the analysis
|
||||
* @returns {boolean} response.status - Whether the analysis was successful
|
||||
* @returns {number} response.code - HTTP status code from the PageSpeed API
|
||||
* @returns {string} response.message - Success or failure message
|
||||
* @returns {Object} response.payload - The PageSpeed analysis results
|
||||
* @throws {Error} If there's an error during the PageSpeed analysis
|
||||
*/
|
||||
async requestPagespeed(monitor) {
|
||||
try {
|
||||
const url = monitor.url;
|
||||
const updatedMonitor = { ...monitor };
|
||||
let pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
|
||||
|
||||
const dbSettings = await this.settingsService.getDBSettings();
|
||||
if (dbSettings?.pagespeedApiKey) {
|
||||
pagespeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
|
||||
} else {
|
||||
this.logger.warn({
|
||||
message: "Pagespeed API key not found, job not executed",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "requestPagespeed",
|
||||
details: { url },
|
||||
});
|
||||
return;
|
||||
}
|
||||
updatedMonitor.url = pagespeedUrl;
|
||||
return await this.requestHttp(updatedMonitor);
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestPagespeed";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async requestHardware(monitor) {
|
||||
try {
|
||||
return await this.requestHttp(monitor);
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestHardware";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the status of a Docker container by its ID.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The Docker container ID to check
|
||||
* @param {string} monitor._id - The unique identifier of the monitor
|
||||
* @param {string} monitor.type - The type of monitor
|
||||
* @returns {Promise<Object>} A promise that resolves to a docker response object
|
||||
* @returns {string} dockerResponse.monitorId - The ID of the monitor
|
||||
* @returns {string} dockerResponse.type - The type of monitor
|
||||
* @returns {number} dockerResponse.responseTime - The time taken for the container inspection
|
||||
* @returns {boolean} dockerResponse.status - Whether the container is running (true) or not (false)
|
||||
* @returns {number} dockerResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure)
|
||||
* @returns {string} dockerResponse.message - Success or failure message
|
||||
* @throws {Error} If the container is not found or if there's an error inspecting the container
|
||||
*/
|
||||
async requestDocker(monitor) {
|
||||
try {
|
||||
const docker = new this.Docker({
|
||||
socketPath: "/var/run/docker.sock",
|
||||
handleError: true, // Enable error handling
|
||||
});
|
||||
|
||||
const containers = await docker.listContainers({ all: true });
|
||||
const containerExists = containers.some((c) => c.Id.startsWith(monitor.url));
|
||||
if (!containerExists) {
|
||||
throw new Error(this.stringService.dockerNotFound);
|
||||
}
|
||||
const container = docker.getContainer(monitor.url);
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(() => container.inspect());
|
||||
|
||||
const dockerResponse = {
|
||||
monitorId: monitor._id,
|
||||
type: monitor.type,
|
||||
responseTime,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
dockerResponse.status = false;
|
||||
dockerResponse.code = error.statusCode || this.NETWORK_ERROR;
|
||||
dockerResponse.message = error.reason || "Failed to fetch Docker container information";
|
||||
return dockerResponse;
|
||||
}
|
||||
dockerResponse.status = response?.State?.Status === "running" ? true : false;
|
||||
dockerResponse.code = 200;
|
||||
dockerResponse.message = "Docker container status fetched successfully";
|
||||
return dockerResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestDocker";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to establish a TCP connection to a specified host and port.
|
||||
* @async
|
||||
* @param {Object} monitor - The monitor configuration object
|
||||
* @param {string} monitor.url - The host URL to connect to
|
||||
* @param {number} monitor.port - The port number to connect to
|
||||
* @param {string} monitor._id - The unique identifier of the monitor
|
||||
* @param {string} monitor.type - The type of monitor
|
||||
* @returns {Promise<Object>} A promise that resolves to a port response object
|
||||
* @returns {string} portResponse.monitorId - The ID of the monitor
|
||||
* @returns {string} portResponse.type - The type of monitor
|
||||
* @returns {number} portResponse.responseTime - The time taken for the connection attempt
|
||||
* @returns {boolean} portResponse.status - Whether the connection was successful
|
||||
* @returns {number} portResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure)
|
||||
* @returns {string} portResponse.message - Success or failure message
|
||||
* @throws {Error} If the connection times out or encounters an error
|
||||
*/
|
||||
async requestPort(monitor) {
|
||||
try {
|
||||
const { url, port } = monitor;
|
||||
const { response, responseTime, error } = await this.timeRequest(async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = this.net.createConnection(
|
||||
{
|
||||
host: url,
|
||||
port,
|
||||
},
|
||||
() => {
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
resolve({ success: true });
|
||||
}
|
||||
);
|
||||
|
||||
socket.setTimeout(5000);
|
||||
socket.on("timeout", () => {
|
||||
socket.destroy();
|
||||
reject(new Error("Connection timeout"));
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
socket.destroy();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const portResponse = {
|
||||
monitorId: monitor._id,
|
||||
type: monitor.type,
|
||||
responseTime,
|
||||
};
|
||||
|
||||
if (error) {
|
||||
portResponse.status = false;
|
||||
portResponse.code = this.NETWORK_ERROR;
|
||||
portResponse.message = this.stringService.portFail;
|
||||
return portResponse;
|
||||
}
|
||||
|
||||
portResponse.status = response.success;
|
||||
portResponse.code = 200;
|
||||
portResponse.message = this.stringService.portSuccess;
|
||||
return portResponse;
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestTCP";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles unsupported job types by throwing an error with details.
|
||||
*
|
||||
* @param {string} type - The unsupported job type that was provided
|
||||
* @throws {Error} An error with service name, method name and unsupported type message
|
||||
*/
|
||||
handleUnsupportedType(type) {
|
||||
const err = new Error(`Unsupported type: ${type}`);
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "getStatus";
|
||||
throw err;
|
||||
}
|
||||
|
||||
async requestWebhook(type, url, body) {
|
||||
try {
|
||||
const response = await this.axios.post(url, body, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
type: "webhook",
|
||||
status: true,
|
||||
code: response.status,
|
||||
message: `Successfully sent ${type} notification`,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "requestWebhook",
|
||||
});
|
||||
|
||||
return {
|
||||
type: "webhook",
|
||||
status: false,
|
||||
code: error.response?.status || this.NETWORK_ERROR,
|
||||
message: `Failed to send ${type} notification`,
|
||||
payload: error.response?.data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async requestPagerDuty({ message, routingKey, monitorUrl }) {
|
||||
try {
|
||||
const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, {
|
||||
routing_key: routingKey,
|
||||
event_action: "trigger",
|
||||
payload: {
|
||||
summary: message,
|
||||
severity: "critical",
|
||||
source: monitorUrl,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (response?.data?.status !== "success") return false;
|
||||
return true;
|
||||
} catch (error) {
|
||||
error.details = error.response?.data;
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestPagerDuty";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of a job based on its type and returns the appropriate response.
|
||||
*
|
||||
* @param {Object} job - The job object containing the data for the status request.
|
||||
* @param {Object} job.data - The data object within the job.
|
||||
* @param {string} job.data.type - The type of the job (e.g., "ping", "http", "pagespeed", "hardware").
|
||||
* @returns {Promise<Object>} The response object from the appropriate request method.
|
||||
* @throws {Error} Throws an error if the job type is unsupported.
|
||||
*/
|
||||
async getStatus(monitor) {
|
||||
const type = monitor.type ?? "unknown";
|
||||
switch (type) {
|
||||
case this.TYPE_PING:
|
||||
return await this.requestPing(monitor);
|
||||
case this.TYPE_HTTP:
|
||||
return await this.requestHttp(monitor);
|
||||
case this.TYPE_PAGESPEED:
|
||||
return await this.requestPagespeed(monitor);
|
||||
case this.TYPE_HARDWARE:
|
||||
return await this.requestHardware(monitor);
|
||||
case this.TYPE_DOCKER:
|
||||
return await this.requestDocker(monitor);
|
||||
case this.TYPE_PORT:
|
||||
return await this.requestPort(monitor);
|
||||
default:
|
||||
return this.handleUnsupportedType(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default NetworkService;
|
||||
@@ -0,0 +1,119 @@
|
||||
const SERVICE_NAME = "NotificationService";
|
||||
|
||||
class NotificationService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ emailService, db, logger, networkService, stringService, notificationUtils }) {
|
||||
this.emailService = emailService;
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
this.stringService = stringService;
|
||||
this.notificationUtils = notificationUtils;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return NotificationService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
sendNotification = async ({ notification, subject, content, html }) => {
|
||||
const { type, address } = notification;
|
||||
|
||||
if (type === "email") {
|
||||
const messageId = await this.emailService.sendEmail(address, subject, html);
|
||||
if (!messageId) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a body for webhooks
|
||||
let body = { text: content };
|
||||
if (type === "discord") {
|
||||
body = { content };
|
||||
}
|
||||
|
||||
if (type === "slack" || type === "discord" || type === "webhook") {
|
||||
const response = await this.networkService.requestWebhook(type, address, body);
|
||||
return response.status;
|
||||
}
|
||||
if (type === "pager_duty") {
|
||||
const response = await this.networkService.requestPagerDuty({
|
||||
message: content,
|
||||
monitorUrl: subject,
|
||||
routingKey: address,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
async handleNotifications(networkResponse) {
|
||||
const { monitor, statusChanged, prevStatus } = networkResponse;
|
||||
const { type } = monitor;
|
||||
if (type !== "hardware" && statusChanged === false) return false;
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
if (type !== "hardware" && prevStatus === undefined) return false;
|
||||
|
||||
const notificationIDs = networkResponse.monitor?.notifications ?? [];
|
||||
if (notificationIDs.length === 0) return false;
|
||||
|
||||
if (networkResponse.monitor.type === "hardware") {
|
||||
const thresholds = networkResponse?.monitor?.thresholds;
|
||||
|
||||
if (thresholds === undefined) return false; // No thresholds set, we're done
|
||||
const metrics = networkResponse?.payload?.data ?? null;
|
||||
if (metrics === null) return false; // No metrics, we're done
|
||||
|
||||
const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse);
|
||||
if (alerts.length === 0) return false;
|
||||
|
||||
const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts);
|
||||
const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts);
|
||||
|
||||
const success = await this.notifyAll({ notificationIDs, subject, html, content });
|
||||
return success;
|
||||
}
|
||||
|
||||
// Status monitors
|
||||
const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse);
|
||||
const content = await this.notificationUtils.buildWebhookMessage(networkResponse);
|
||||
const success = this.notifyAll({ notificationIDs, subject, html, content });
|
||||
return success;
|
||||
}
|
||||
|
||||
async notifyAll({ notificationIDs, subject, html, content }) {
|
||||
const notifications = await this.db.getNotificationsByIds(notificationIDs);
|
||||
|
||||
// Map each notification to a test promise
|
||||
const promises = notifications.map(async (notification) => {
|
||||
try {
|
||||
await this.sendNotification({ notification, subject, content, html });
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
return results.every((r) => r === true);
|
||||
}
|
||||
|
||||
async getTestNotification() {
|
||||
const html = await this.notificationUtils.buildTestEmail();
|
||||
const content = "This is a test notification";
|
||||
const subject = "Test Notification";
|
||||
return { subject, html, content };
|
||||
}
|
||||
|
||||
async testAllNotifications(notificationIDs) {
|
||||
const { subject, html, content } = await this.getTestNotification();
|
||||
return this.notifyAll({ notificationIDs, subject, html, content });
|
||||
}
|
||||
|
||||
async sendTestNotification(notification) {
|
||||
const { subject, html, content } = await this.getTestNotification();
|
||||
const success = await this.sendNotification({ notification, subject, content, html });
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationService;
|
||||
@@ -0,0 +1,132 @@
|
||||
const SERVICE_NAME = "NotificationUtils";
|
||||
|
||||
class NotificationUtils {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ stringService, emailService }) {
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return NotificationUtils.SERVICE_NAME;
|
||||
}
|
||||
|
||||
buildTestEmail = async () => {
|
||||
const context = { testName: "Monitoring System" };
|
||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
||||
return html;
|
||||
};
|
||||
|
||||
buildStatusEmail = async (networkResponse) => {
|
||||
const { monitor, status, prevStatus } = networkResponse;
|
||||
const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url };
|
||||
const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`;
|
||||
const html = await this.emailService.buildEmail(template, context);
|
||||
return { subject, html };
|
||||
};
|
||||
|
||||
buildWebhookMessage = (networkResponse) => {
|
||||
const { monitor, status, code, timestamp } = networkResponse;
|
||||
// Format timestamp using the local system timezone
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
// Get timezone abbreviation and format the date
|
||||
const timeZoneAbbr = date.toLocaleTimeString("en-US", { timeZoneName: "short" }).split(" ").pop();
|
||||
|
||||
// Format the date with readable format
|
||||
return (
|
||||
date
|
||||
.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
.replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") +
|
||||
" " +
|
||||
timeZoneAbbr
|
||||
);
|
||||
};
|
||||
|
||||
// Get formatted time
|
||||
const formattedTime = timestamp ? formatTime(timestamp) : formatTime(new Date().getTime());
|
||||
|
||||
// Create different messages based on status with extra spacing
|
||||
let messageText;
|
||||
if (status === true) {
|
||||
messageText = this.stringService.monitorUpAlert
|
||||
.replace("{monitorName}", monitor.name)
|
||||
.replace("{time}", formattedTime)
|
||||
.replace("{code}", code || "Unknown");
|
||||
} else {
|
||||
messageText = this.stringService.monitorDownAlert
|
||||
.replace("{monitorName}", monitor.name)
|
||||
.replace("{time}", formattedTime)
|
||||
.replace("{code}", code || "Unknown");
|
||||
}
|
||||
return messageText;
|
||||
};
|
||||
|
||||
buildHardwareAlerts = async (networkResponse) => {
|
||||
const monitor = networkResponse?.monitor;
|
||||
const thresholds = networkResponse?.monitor?.thresholds;
|
||||
const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds;
|
||||
|
||||
const metrics = networkResponse?.payload?.data;
|
||||
const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics;
|
||||
|
||||
const alerts = {
|
||||
cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false,
|
||||
memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false,
|
||||
disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false,
|
||||
};
|
||||
|
||||
const alertsToSend = [];
|
||||
const alertTypes = ["cpu", "memory", "disk"];
|
||||
for (const type of alertTypes) {
|
||||
// Iterate over each alert type to see if any need to be decremented
|
||||
if (alerts[type] === true) {
|
||||
monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered
|
||||
|
||||
if (monitor[`${type}AlertThreshold`] <= 0) {
|
||||
// If threshold drops below 0, reset and send notification
|
||||
monitor[`${type}AlertThreshold`] = monitor.alertThreshold;
|
||||
|
||||
const formatAlert = {
|
||||
cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
|
||||
memory: () =>
|
||||
`Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
|
||||
disk: () =>
|
||||
`Your current disk usage: ${disk
|
||||
.map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`)
|
||||
.join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
|
||||
};
|
||||
alertsToSend.push(formatAlert[type]());
|
||||
}
|
||||
}
|
||||
}
|
||||
await monitor.save();
|
||||
return alertsToSend;
|
||||
};
|
||||
|
||||
buildHardwareEmail = async (networkResponse, alerts) => {
|
||||
const { monitor } = networkResponse;
|
||||
const template = "hardwareIncidentTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url, alerts };
|
||||
const subject = `Monitor ${monitor.name} infrastructure alerts`;
|
||||
const html = await this.emailService.buildEmail(template, context);
|
||||
return { subject, html };
|
||||
};
|
||||
|
||||
buildHardwareNotificationMessage = (alerts) => {
|
||||
return alerts.map((alert) => alert).join("\n");
|
||||
};
|
||||
}
|
||||
|
||||
export default NotificationUtils;
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
import MonitorStats from "../../db/models/MonitorStats.js";
|
||||
import { safelyParseFloat } from "../../utils/dataUtils.js";
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
class StatusService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
/**
|
||||
* Creates an instance of StatusService.
|
||||
*
|
||||
* @param {Object} db - The database instance.
|
||||
* @param {Object} logger - The logger instance.
|
||||
*/
|
||||
constructor({ db, logger, buffer }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return StatusService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async updateRunningStats({ monitor, networkResponse }) {
|
||||
try {
|
||||
const monitorId = monitor._id;
|
||||
const { responseTime, status, upt_burnt } = networkResponse;
|
||||
// Get stats
|
||||
let stats = await MonitorStats.findOne({ monitorId });
|
||||
if (!stats) {
|
||||
stats = new MonitorStats({
|
||||
monitorId,
|
||||
avgResponseTime: 0,
|
||||
totalChecks: 0,
|
||||
totalUpChecks: 0,
|
||||
totalDownChecks: 0,
|
||||
uptimePercentage: 0,
|
||||
lastCheck: null,
|
||||
timeSInceLastCheck: 0,
|
||||
uptBurnt: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Update stats
|
||||
|
||||
// Last response time
|
||||
stats.lastResponseTime = responseTime;
|
||||
|
||||
// Avg response time:
|
||||
let avgResponseTime = stats.avgResponseTime;
|
||||
if (typeof responseTime !== "undefined" && responseTime !== null) {
|
||||
if (avgResponseTime === 0) {
|
||||
avgResponseTime = responseTime;
|
||||
} else {
|
||||
avgResponseTime = (avgResponseTime * (stats.totalChecks - 1) + responseTime) / stats.totalChecks;
|
||||
}
|
||||
}
|
||||
stats.avgResponseTime = avgResponseTime;
|
||||
|
||||
// Total checks
|
||||
stats.totalChecks++;
|
||||
if (status === true) {
|
||||
stats.totalUpChecks++;
|
||||
// Update the timeSinceLastFailure if needed
|
||||
if (stats.timeOfLastFailure === 0) {
|
||||
stats.timeOfLastFailure = new Date().getTime();
|
||||
}
|
||||
} else {
|
||||
stats.totalDownChecks++;
|
||||
stats.timeOfLastFailure = 0;
|
||||
}
|
||||
|
||||
// Calculate uptime percentage
|
||||
let uptimePercentage;
|
||||
if (stats.totalChecks > 0) {
|
||||
uptimePercentage = stats.totalUpChecks / stats.totalChecks;
|
||||
} else {
|
||||
uptimePercentage = status === true ? 100 : 0;
|
||||
}
|
||||
stats.uptimePercentage = uptimePercentage;
|
||||
|
||||
// latest check
|
||||
stats.lastCheckTimestamp = new Date().getTime();
|
||||
|
||||
// UPT burned
|
||||
if (typeof upt_burnt !== "undefined" && upt_burnt !== null) {
|
||||
const currentUptBurnt = safelyParseFloat(stats.uptBurnt);
|
||||
const newUptBurnt = safelyParseFloat(upt_burnt);
|
||||
stats.uptBurnt = currentUptBurnt + newUptBurnt;
|
||||
}
|
||||
await stats.save();
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
service: this.SERVICE_NAME,
|
||||
message: error.message,
|
||||
method: "updateRunningStats",
|
||||
stack: error.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getStatusString = (status) => {
|
||||
if (status === true) return "up";
|
||||
if (status === false) return "down";
|
||||
return "unknown";
|
||||
};
|
||||
/**
|
||||
* Updates the status of a monitor based on the network response.
|
||||
*
|
||||
* @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 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.
|
||||
* @returns {boolean} returnObject.prevStatus - The previous status of the monitor
|
||||
*/
|
||||
updateStatus = async (networkResponse) => {
|
||||
this.insertCheck(networkResponse);
|
||||
try {
|
||||
const { monitorId, status, code } = networkResponse;
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
|
||||
// Update running stats
|
||||
this.updateRunningStats({ monitor, networkResponse });
|
||||
|
||||
// No change in monitor status, return early
|
||||
if (monitor.status === status)
|
||||
return {
|
||||
monitor,
|
||||
statusChanged: false,
|
||||
prevStatus: monitor.status,
|
||||
code,
|
||||
timestamp: new Date().getTime(),
|
||||
};
|
||||
|
||||
// Monitor status changed, save prev status and update monitor
|
||||
this.logger.info({
|
||||
service: this.SERVICE_NAME,
|
||||
message: `${monitor.name} went from ${this.getStatusString(monitor.status)} to ${this.getStatusString(status)}`,
|
||||
prevStatus: monitor.status,
|
||||
newStatus: status,
|
||||
});
|
||||
|
||||
const prevStatus = monitor.status;
|
||||
monitor.status = status;
|
||||
await monitor.save();
|
||||
|
||||
return {
|
||||
monitor,
|
||||
statusChanged: true,
|
||||
prevStatus,
|
||||
code,
|
||||
timestamp: new Date().getTime(),
|
||||
};
|
||||
} catch (error) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "updateStatus";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a check object from the network response.
|
||||
*
|
||||
* @param {Object} networkResponse - The network response object.
|
||||
* @param {string} networkResponse.monitorId - The monitor ID.
|
||||
* @param {string} networkResponse.type - The type of the response.
|
||||
* @param {string} networkResponse.status - The status of the response.
|
||||
* @param {number} networkResponse.responseTime - The response time.
|
||||
* @param {number} networkResponse.code - The status code.
|
||||
* @param {string} networkResponse.message - The message.
|
||||
* @param {Object} networkResponse.payload - The payload of the response.
|
||||
* @returns {Object} The check object.
|
||||
*/
|
||||
buildCheck = (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,
|
||||
status,
|
||||
statusCode: code,
|
||||
responseTime,
|
||||
message,
|
||||
first_byte_took,
|
||||
body_read_took,
|
||||
dns_took,
|
||||
conn_took,
|
||||
connect_took,
|
||||
tls_took,
|
||||
};
|
||||
|
||||
if (type === "pagespeed") {
|
||||
if (typeof payload === "undefined") {
|
||||
this.logger.warn({
|
||||
message: "Failed to build check",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "buildCheck",
|
||||
details: "empty payload",
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
const categories = payload?.lighthouseResult?.categories ?? {};
|
||||
const audits = payload?.lighthouseResult?.audits ?? {};
|
||||
const {
|
||||
"cumulative-layout-shift": cls = 0,
|
||||
"speed-index": si = 0,
|
||||
"first-contentful-paint": fcp = 0,
|
||||
"largest-contentful-paint": lcp = 0,
|
||||
"total-blocking-time": tbt = 0,
|
||||
} = audits;
|
||||
check.accessibility = (categories?.accessibility?.score || 0) * 100;
|
||||
check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100;
|
||||
check.seo = (categories?.seo?.score || 0) * 100;
|
||||
check.performance = (categories?.performance?.score || 0) * 100;
|
||||
check.audits = { cls, si, fcp, lcp, tbt };
|
||||
}
|
||||
|
||||
if (type === "hardware") {
|
||||
const { cpu, memory, disk, host } = payload?.data ?? {};
|
||||
const { errors } = payload?.errors ?? [];
|
||||
check.cpu = cpu ?? {};
|
||||
check.memory = memory ?? {};
|
||||
check.disk = disk ?? {};
|
||||
check.host = host ?? {};
|
||||
check.errors = errors ?? [];
|
||||
check.capture = payload?.capture ?? {};
|
||||
}
|
||||
return check;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a check into the database based on the network response.
|
||||
*
|
||||
* @param {Object} networkResponse - The network response object.
|
||||
* @param {string} networkResponse.monitorId - The monitor ID.
|
||||
* @param {string} networkResponse.type - The type of the response.
|
||||
* @param {string} networkResponse.status - The status of the response.
|
||||
* @param {number} networkResponse.responseTime - The response time.
|
||||
* @param {number} networkResponse.code - The status code.
|
||||
* @param {string} networkResponse.message - The message.
|
||||
* @param {Object} networkResponse.payload - The payload of the response.
|
||||
* @returns {Promise<void>} A promise that resolves when the check is inserted.
|
||||
*/
|
||||
insertCheck = async (networkResponse) => {
|
||||
try {
|
||||
const check = this.buildCheck(networkResponse);
|
||||
if (typeof check === "undefined") {
|
||||
this.logger.warn({
|
||||
message: "Failed to build check",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "insertCheck",
|
||||
details: networkResponse,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.buffer.addToBuffer({ check, type: networkResponse.type });
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: error.service || this.SERVICE_NAME,
|
||||
method: error.method || "insertCheck",
|
||||
details: error.details || `Error inserting check for monitor: ${networkResponse?.monitorId}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
export default StatusService;
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
const SERVICE_NAME = "ServiceRegistry";
|
||||
import logger from "../../utils/logger.js";
|
||||
class ServiceRegistry {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor() {
|
||||
this.services = {};
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return ServiceRegistry.SERVICE_NAME;
|
||||
}
|
||||
|
||||
register(name, service) {
|
||||
logger.info({
|
||||
message: `Registering service ${name}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "register",
|
||||
});
|
||||
this.services[name] = service;
|
||||
}
|
||||
|
||||
get(name) {
|
||||
if (!this.services[name]) {
|
||||
logger.error({
|
||||
message: `Service ${name} is not registered`,
|
||||
service: SERVICE_NAME,
|
||||
method: "get",
|
||||
});
|
||||
throw new Error(`Service ${name} is not registered`);
|
||||
}
|
||||
return this.services[name];
|
||||
}
|
||||
|
||||
listServices() {
|
||||
return Object.keys(this.services);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ServiceRegistry();
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
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,
|
||||
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
|
||||
*
|
||||
* This service is responsible for loading and managing the application settings.
|
||||
*/
|
||||
class SettingsService {
|
||||
static SERVICE_NAME = "SettingsService";
|
||||
|
||||
/**
|
||||
* Constructs a new SettingsService
|
||||
* @constructor
|
||||
* @throws {Error}
|
||||
*/ constructor(AppSettings) {
|
||||
this.AppSettings = AppSettings;
|
||||
this.settings = { ...envConfig };
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return SettingsService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings from env settings
|
||||
* @returns {Object>} The settings.
|
||||
*/
|
||||
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.
|
||||
* @throws Will throw an error if settings have not been loaded.
|
||||
*/
|
||||
getSettings() {
|
||||
if (!this.settings) {
|
||||
throw new Error("Settings have not been loaded");
|
||||
}
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
async getDBSettings() {
|
||||
// Remove any old settings
|
||||
await this.AppSettings.deleteMany({ version: { $exists: false } });
|
||||
|
||||
let settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean();
|
||||
if (settings === null) {
|
||||
await this.AppSettings.create({});
|
||||
settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean();
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsService;
|
||||
Executable
+448
@@ -0,0 +1,448 @@
|
||||
const SERVICE_NAME = "StringService";
|
||||
|
||||
class StringService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor(translationService) {
|
||||
if (StringService.instance) {
|
||||
return StringService.instance;
|
||||
}
|
||||
|
||||
this.translationService = translationService;
|
||||
this._language = "en"; // default language
|
||||
StringService.instance = this;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return StringService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
setLanguage(language) {
|
||||
this._language = language;
|
||||
}
|
||||
|
||||
get language() {
|
||||
return this._language;
|
||||
}
|
||||
|
||||
// Auth Messages
|
||||
get dontHaveAccount() {
|
||||
return this.translationService.getTranslation("dontHaveAccount");
|
||||
}
|
||||
|
||||
get email() {
|
||||
return this.translationService.getTranslation("email");
|
||||
}
|
||||
|
||||
get forgotPassword() {
|
||||
return this.translationService.getTranslation("forgotPassword");
|
||||
}
|
||||
|
||||
get password() {
|
||||
return this.translationService.getTranslation("password");
|
||||
}
|
||||
|
||||
get signUp() {
|
||||
return this.translationService.getTranslation("signUp");
|
||||
}
|
||||
|
||||
get submit() {
|
||||
return this.translationService.getTranslation("submit");
|
||||
}
|
||||
|
||||
get title() {
|
||||
return this.translationService.getTranslation("title");
|
||||
}
|
||||
|
||||
get continue() {
|
||||
return this.translationService.getTranslation("continue");
|
||||
}
|
||||
|
||||
get enterEmail() {
|
||||
return this.translationService.getTranslation("enterEmail");
|
||||
}
|
||||
|
||||
get authLoginTitle() {
|
||||
return this.translationService.getTranslation("authLoginTitle");
|
||||
}
|
||||
|
||||
get authLoginEnterPassword() {
|
||||
return this.translationService.getTranslation("authLoginEnterPassword");
|
||||
}
|
||||
|
||||
get commonPassword() {
|
||||
return this.translationService.getTranslation("commonPassword");
|
||||
}
|
||||
|
||||
get commonBack() {
|
||||
return this.translationService.getTranslation("commonBack");
|
||||
}
|
||||
|
||||
get authForgotPasswordTitle() {
|
||||
return this.translationService.getTranslation("authForgotPasswordTitle");
|
||||
}
|
||||
|
||||
get authForgotPasswordResetPassword() {
|
||||
return this.translationService.getTranslation("authForgotPasswordResetPassword");
|
||||
}
|
||||
|
||||
get createPassword() {
|
||||
return this.translationService.getTranslation("createPassword");
|
||||
}
|
||||
|
||||
get createAPassword() {
|
||||
return this.translationService.getTranslation("createAPassword");
|
||||
}
|
||||
|
||||
get authRegisterAlreadyHaveAccount() {
|
||||
return this.translationService.getTranslation("authRegisterAlreadyHaveAccount");
|
||||
}
|
||||
|
||||
get commonAppName() {
|
||||
return this.translationService.getTranslation("commonAppName");
|
||||
}
|
||||
|
||||
get authLoginEnterEmail() {
|
||||
return this.translationService.getTranslation("authLoginEnterEmail");
|
||||
}
|
||||
|
||||
get authRegisterTitle() {
|
||||
return this.translationService.getTranslation("authRegisterTitle");
|
||||
}
|
||||
|
||||
get monitorGetAll() {
|
||||
return this.translationService.getTranslation("monitorGetAll");
|
||||
}
|
||||
|
||||
get monitorGetById() {
|
||||
return this.translationService.getTranslation("monitorGetById");
|
||||
}
|
||||
|
||||
get monitorGetByIdSuccess() {
|
||||
return this.translationService.getTranslation("monitorGetByIdSuccess");
|
||||
}
|
||||
|
||||
get monitorCreate() {
|
||||
return this.translationService.getTranslation("monitorCreate");
|
||||
}
|
||||
|
||||
get bulkMonitorsCreate() {
|
||||
return this.translationService.getTranslation("bulkMonitorsCreate");
|
||||
}
|
||||
|
||||
get monitorEdit() {
|
||||
return this.translationService.getTranslation("monitorEdit");
|
||||
}
|
||||
|
||||
get monitorDelete() {
|
||||
return this.translationService.getTranslation("monitorDelete");
|
||||
}
|
||||
|
||||
get monitorPause() {
|
||||
return this.translationService.getTranslation("monitorPause");
|
||||
}
|
||||
|
||||
get monitorResume() {
|
||||
return this.translationService.getTranslation("monitorResume");
|
||||
}
|
||||
|
||||
get monitorDemoAdded() {
|
||||
return this.translationService.getTranslation("monitorDemoAdded");
|
||||
}
|
||||
|
||||
get monitorStatsById() {
|
||||
return this.translationService.getTranslation("monitorStatsById");
|
||||
}
|
||||
|
||||
get monitorCertificate() {
|
||||
return this.translationService.getTranslation("monitorCertificate");
|
||||
}
|
||||
|
||||
// Maintenance Window Messages
|
||||
get maintenanceWindowCreate() {
|
||||
return this.translationService.getTranslation("maintenanceWindowCreate");
|
||||
}
|
||||
|
||||
get maintenanceWindowGetById() {
|
||||
return this.translationService.getTranslation("maintenanceWindowGetById");
|
||||
}
|
||||
|
||||
get maintenanceWindowGetByTeam() {
|
||||
return this.translationService.getTranslation("maintenanceWindowGetByTeam");
|
||||
}
|
||||
|
||||
get maintenanceWindowDelete() {
|
||||
return this.translationService.getTranslation("maintenanceWindowDelete");
|
||||
}
|
||||
|
||||
get maintenanceWindowEdit() {
|
||||
return this.translationService.getTranslation("maintenanceWindowEdit");
|
||||
}
|
||||
|
||||
// Webhook Messages
|
||||
get webhookUnsupportedPlatform() {
|
||||
return this.translationService.getTranslation("webhookUnsupportedPlatform");
|
||||
}
|
||||
|
||||
get webhookSendError() {
|
||||
return this.translationService.getTranslation("webhookSendError");
|
||||
}
|
||||
|
||||
get webhookSendSuccess() {
|
||||
return this.translationService.getTranslation("webhookSendSuccess");
|
||||
}
|
||||
|
||||
get telegramRequiresBotTokenAndChatId() {
|
||||
return this.translationService.getTranslation("telegramRequiresBotTokenAndChatId");
|
||||
}
|
||||
|
||||
get webhookUrlRequired() {
|
||||
return this.translationService.getTranslation("webhookUrlRequired");
|
||||
}
|
||||
|
||||
get platformRequired() {
|
||||
return this.translationService.getTranslation("platformRequired");
|
||||
}
|
||||
|
||||
get testNotificationFailed() {
|
||||
return this.translationService.getTranslation("testNotificationFailed");
|
||||
}
|
||||
|
||||
get monitorUpAlert() {
|
||||
return this.translationService.getTranslation("monitorUpAlert");
|
||||
}
|
||||
|
||||
get monitorDownAlert() {
|
||||
return this.translationService.getTranslation("monitorDownAlert");
|
||||
}
|
||||
|
||||
getWebhookUnsupportedPlatform(platform) {
|
||||
return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform);
|
||||
}
|
||||
|
||||
getWebhookSendError(platform) {
|
||||
return this.translationService.getTranslation("webhookSendError").replace("{platform}", platform);
|
||||
}
|
||||
|
||||
getMonitorStatus(name, status, url) {
|
||||
const translationKey = status === true ? "monitorStatusUp" : "monitorStatusDown";
|
||||
return this.translationService.getTranslation(translationKey).replace("{name}", name).replace("{url}", url);
|
||||
}
|
||||
|
||||
// Error Messages
|
||||
get unknownError() {
|
||||
return this.translationService.getTranslation("unknownError");
|
||||
}
|
||||
|
||||
get friendlyError() {
|
||||
return this.translationService.getTranslation("friendlyError");
|
||||
}
|
||||
|
||||
get authIncorrectPassword() {
|
||||
return this.translationService.getTranslation("authIncorrectPassword");
|
||||
}
|
||||
|
||||
get unauthorized() {
|
||||
return this.translationService.getTranslation("unauthorized");
|
||||
}
|
||||
|
||||
get authAdminExists() {
|
||||
return this.translationService.getTranslation("authAdminExists");
|
||||
}
|
||||
|
||||
get authInviteNotFound() {
|
||||
return this.translationService.getTranslation("authInviteNotFound");
|
||||
}
|
||||
|
||||
get unknownService() {
|
||||
return this.translationService.getTranslation("unknownService");
|
||||
}
|
||||
|
||||
get noAuthToken() {
|
||||
return this.translationService.getTranslation("noAuthToken");
|
||||
}
|
||||
|
||||
get invalidAuthToken() {
|
||||
return this.translationService.getTranslation("invalidAuthToken");
|
||||
}
|
||||
|
||||
get expiredAuthToken() {
|
||||
return this.translationService.getTranslation("expiredAuthToken");
|
||||
}
|
||||
|
||||
// Queue Messages
|
||||
get queueGetMetrics() {
|
||||
return this.translationService.getTranslation("queueGetMetrics");
|
||||
}
|
||||
|
||||
get queueGetJobs() {
|
||||
return this.translationService.getTranslation("queueGetJobs");
|
||||
}
|
||||
|
||||
get queueAddJob() {
|
||||
return this.translationService.getTranslation("queueAddJob");
|
||||
}
|
||||
|
||||
get queueObliterate() {
|
||||
return this.translationService.getTranslation("queueObliterate");
|
||||
}
|
||||
|
||||
// Job Queue Messages
|
||||
get jobQueueDeleteJobSuccess() {
|
||||
return this.translationService.getTranslation("jobQueueDeleteJobSuccess");
|
||||
}
|
||||
|
||||
get jobQueuePauseJob() {
|
||||
return this.translationService.getTranslation("jobQueuePauseJob");
|
||||
}
|
||||
|
||||
get jobQueueResumeJob() {
|
||||
return this.translationService.getTranslation("jobQueueResumeJob");
|
||||
}
|
||||
|
||||
// Status Page Messages
|
||||
get statusPageByUrl() {
|
||||
return this.translationService.getTranslation("statusPageByUrl");
|
||||
}
|
||||
|
||||
get statusPageCreate() {
|
||||
return this.translationService.getTranslation("statusPageCreate");
|
||||
}
|
||||
|
||||
get statusPageDelete() {
|
||||
return this.translationService.getTranslation("statusPageDelete");
|
||||
}
|
||||
|
||||
get statusPageUpdate() {
|
||||
return this.translationService.getTranslation("statusPageUpdate");
|
||||
}
|
||||
|
||||
get statusPageNotFound() {
|
||||
return this.translationService.getTranslation("statusPageNotFound");
|
||||
}
|
||||
|
||||
get statusPageByTeamId() {
|
||||
return this.translationService.getTranslation("statusPageByTeamId");
|
||||
}
|
||||
|
||||
get statusPageUrlNotUnique() {
|
||||
return this.translationService.getTranslation("statusPageUrlNotUnique");
|
||||
}
|
||||
|
||||
// Docker Messages
|
||||
get dockerFail() {
|
||||
return this.translationService.getTranslation("dockerFail");
|
||||
}
|
||||
|
||||
get dockerNotFound() {
|
||||
return this.translationService.getTranslation("dockerNotFound");
|
||||
}
|
||||
|
||||
get dockerSuccess() {
|
||||
return this.translationService.getTranslation("dockerSuccess");
|
||||
}
|
||||
|
||||
// Port Messages
|
||||
get portFail() {
|
||||
return this.translationService.getTranslation("portFail");
|
||||
}
|
||||
|
||||
get portSuccess() {
|
||||
return this.translationService.getTranslation("portSuccess");
|
||||
}
|
||||
|
||||
// Alert Messages
|
||||
get alertCreate() {
|
||||
return this.translationService.getTranslation("alertCreate");
|
||||
}
|
||||
|
||||
get alertGetByUser() {
|
||||
return this.translationService.getTranslation("alertGetByUser");
|
||||
}
|
||||
|
||||
get alertGetByMonitor() {
|
||||
return this.translationService.getTranslation("alertGetByMonitor");
|
||||
}
|
||||
|
||||
get alertGetById() {
|
||||
return this.translationService.getTranslation("alertGetById");
|
||||
}
|
||||
|
||||
get alertEdit() {
|
||||
return this.translationService.getTranslation("alertEdit");
|
||||
}
|
||||
|
||||
get alertDelete() {
|
||||
return this.translationService.getTranslation("alertDelete");
|
||||
}
|
||||
|
||||
getDeletedCount(count) {
|
||||
return this.translationService.getTranslation("deletedCount").replace("{count}", count);
|
||||
}
|
||||
|
||||
get pingSuccess() {
|
||||
return this.translationService.getTranslation("pingSuccess");
|
||||
}
|
||||
|
||||
get getAppSettings() {
|
||||
return this.translationService.getTranslation("getAppSettings");
|
||||
}
|
||||
|
||||
get httpNetworkError() {
|
||||
return this.translationService.getTranslation("httpNetworkError");
|
||||
}
|
||||
|
||||
get httpNotJson() {
|
||||
return this.translationService.getTranslation("httpNotJson");
|
||||
}
|
||||
|
||||
get httpJsonPathError() {
|
||||
return this.translationService.getTranslation("httpJsonPathError");
|
||||
}
|
||||
|
||||
get httpEmptyResult() {
|
||||
return this.translationService.getTranslation("httpEmptyResult");
|
||||
}
|
||||
|
||||
get httpMatchSuccess() {
|
||||
return this.translationService.getTranslation("httpMatchSuccess");
|
||||
}
|
||||
|
||||
get httpMatchFail() {
|
||||
return this.translationService.getTranslation("httpMatchFail");
|
||||
}
|
||||
|
||||
get updateAppSettings() {
|
||||
return this.translationService.getTranslation("updateAppSettings");
|
||||
}
|
||||
|
||||
get insufficientPermissions() {
|
||||
return this.translationService.getTranslation("insufficientPermissions");
|
||||
}
|
||||
|
||||
getDbFindMonitorById(monitorId) {
|
||||
return this.translationService.getTranslation("dbFindMonitorById").replace("${monitorId}", monitorId);
|
||||
}
|
||||
|
||||
get dbUserExists() {
|
||||
return this.translationService.getTranslation("dbUserExists");
|
||||
}
|
||||
|
||||
get testEmailSubject() {
|
||||
return this.translationService.getTranslation("testEmailSubject");
|
||||
}
|
||||
|
||||
get verifyOwnerNotFound() {
|
||||
return this.translationService.getTranslation("verifyOwnerNotFound");
|
||||
}
|
||||
|
||||
get verifyOwnerUnauthorized() {
|
||||
return this.translationService.getTranslation("verifyOwnerUnauthorized");
|
||||
}
|
||||
|
||||
get dbUserNotFound() {
|
||||
return this.translationService.getTranslation("dbUserNotFound");
|
||||
}
|
||||
}
|
||||
|
||||
export default StringService;
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
class TranslationService {
|
||||
static SERVICE_NAME = "TranslationService";
|
||||
constructor(logger) {
|
||||
this.logger = logger;
|
||||
this.translations = {};
|
||||
this._language = "en";
|
||||
this.localesDir = path.join(process.cwd(), "locales");
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return TranslationService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
setLanguage(language) {
|
||||
this._language = language;
|
||||
}
|
||||
|
||||
get language() {
|
||||
return this._language;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
await this.loadFromFiles();
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: "TranslationService",
|
||||
method: "initialize",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async loadFromFiles() {
|
||||
try {
|
||||
if (!fs.existsSync(this.localesDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(this.localesDir).filter((file) => file.endsWith(".json"));
|
||||
|
||||
if (files.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
const language = file.replace(".json", "");
|
||||
const filePath = path.join(this.localesDir, file);
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
this.translations[language] = JSON.parse(content);
|
||||
}
|
||||
|
||||
this.logger.info({
|
||||
message: "Translations loaded from files successfully",
|
||||
service: "TranslationService",
|
||||
method: "loadFromFiles",
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: "TranslationService",
|
||||
method: "loadFromFiles",
|
||||
stack: error.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getTranslation(key) {
|
||||
let language = this._language;
|
||||
|
||||
try {
|
||||
return this.translations[language]?.[key] || this.translations["en"]?.[key] || key;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: "TranslationService",
|
||||
method: "getTranslation",
|
||||
stack: error.stack,
|
||||
});
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TranslationService;
|
||||
Reference in new issue
Block a user