Files
Checkmate/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts
T

408 lines
13 KiB
TypeScript
Raw Normal View History

2025-07-25 15:15:14 -07:00
const SERVICE_NAME = "JobQueueHelper";
2026-01-15 22:06:46 +00:00
import type { Monitor } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
2026-01-18 23:38:40 +00:00
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
2026-02-14 21:05:31 +00:00
import type { StatusChangeResult, MonitorStatusResponse, HardwareStatusPayload, MonitorStatus } from "@/types/index.js";
2026-01-19 23:41:24 +00:00
import IncidentService from "@/service/business/incidentService.js";
2026-02-17 19:20:08 +00:00
import {
IMaintenanceWindowsRepository,
IMonitorsRepository,
ITeamsRepository,
IMonitorStatsRepository,
IChecksRepository,
2026-02-17 19:39:24 +00:00
IIncidentsRepository,
2026-02-17 19:20:08 +00:00
} from "@/repositories/index.js";
2026-01-19 23:41:24 +00:00
2026-02-14 21:05:31 +00:00
export interface MonitorActionDecision {
shouldCreateIncident: boolean;
shouldResolveIncident: boolean;
shouldSendNotification: boolean;
incidentReason: "status_down" | "threshold_breach" | null;
notificationReason: "status_change" | "threshold_breach" | null;
thresholdBreaches?: {
cpu?: boolean;
memory?: boolean;
disk?: boolean;
temp?: boolean;
};
}
2025-06-27 12:38:06 +08:00
class SuperSimpleQueueHelper {
2025-07-25 15:15:14 -07:00
static SERVICE_NAME = SERVICE_NAME;
2025-08-18 10:55:12 -07:00
2026-01-15 22:06:46 +00:00
private logger: any;
2026-01-15 23:45:40 +00:00
private networkService: INetworkService;
2026-01-18 23:38:40 +00:00
private statusService: IStatusService;
2026-01-18 22:52:59 +00:00
private notificationsService: INotificationsService;
2026-01-16 18:11:04 +00:00
private checkService: any;
private buffer: any;
2026-01-19 23:41:24 +00:00
private incidentService: IncidentService;
2026-01-22 18:11:54 +00:00
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
2026-02-11 19:38:32 +00:00
private monitorsRepository: IMonitorsRepository;
2026-02-17 19:03:17 +00:00
private teamsRepository: ITeamsRepository;
private monitorStatsRepository: IMonitorStatsRepository;
2026-02-17 19:20:08 +00:00
private checksRepository: IChecksRepository;
2026-02-17 19:39:24 +00:00
private incidentsRepository: IIncidentsRepository;
2026-01-15 22:06:46 +00:00
constructor({
logger,
networkService,
statusService,
2026-01-18 22:52:59 +00:00
notificationsService,
2026-01-16 18:11:04 +00:00
checkService,
buffer,
2026-01-19 23:41:24 +00:00
incidentService,
2026-01-22 18:11:54 +00:00
maintenanceWindowsRepository,
2026-02-11 19:38:32 +00:00
monitorsRepository,
2026-02-17 19:03:17 +00:00
teamsRepository,
monitorStatsRepository,
2026-02-17 19:20:08 +00:00
checksRepository,
2026-02-17 19:39:24 +00:00
incidentsRepository,
2026-01-15 22:06:46 +00:00
}: {
logger: any;
2026-01-15 23:45:40 +00:00
networkService: INetworkService;
2026-01-18 23:38:40 +00:00
statusService: IStatusService;
2026-01-18 22:52:59 +00:00
notificationsService: INotificationsService;
2026-01-16 18:11:04 +00:00
checkService: any;
buffer: any;
2026-01-19 23:41:24 +00:00
incidentService: IncidentService;
2026-01-22 18:11:54 +00:00
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
2026-02-11 19:38:32 +00:00
monitorsRepository: IMonitorsRepository;
2026-02-17 19:03:17 +00:00
teamsRepository: ITeamsRepository;
monitorStatsRepository: IMonitorStatsRepository;
2026-02-17 19:20:08 +00:00
checksRepository: IChecksRepository;
2026-02-17 19:39:24 +00:00
incidentsRepository: IIncidentsRepository;
2026-01-15 22:06:46 +00:00
}) {
2025-06-27 12:38:06 +08:00
this.logger = logger;
this.networkService = networkService;
this.statusService = statusService;
2026-01-16 18:11:04 +00:00
this.checkService = checkService;
this.buffer = buffer;
2026-01-18 22:52:59 +00:00
this.notificationsService = notificationsService;
2026-01-19 23:41:24 +00:00
this.incidentService = incidentService;
2026-01-22 18:11:54 +00:00
this.maintenanceWindowsRepository = maintenanceWindowsRepository;
2026-02-11 19:38:32 +00:00
this.monitorsRepository = monitorsRepository;
2026-02-17 19:03:17 +00:00
this.teamsRepository = teamsRepository;
this.monitorStatsRepository = monitorStatsRepository;
2026-02-17 19:20:08 +00:00
this.checksRepository = checksRepository;
2026-02-17 19:39:24 +00:00
this.incidentsRepository = incidentsRepository;
2025-06-27 12:38:06 +08:00
}
2025-07-25 15:15:14 -07:00
get serviceName() {
return SuperSimpleQueueHelper.SERVICE_NAME;
}
2025-06-27 12:38:06 +08:00
getMonitorJob = () => {
2026-01-15 22:06:46 +00:00
return async (monitor: Monitor) => {
2025-06-27 12:38:06 +08:00
try {
2026-01-13 20:20:00 +00:00
const monitorId = monitor.id;
const teamId = monitor.teamId;
2025-06-27 12:38:06 +08:00
if (!monitorId) {
2026-01-15 22:06:46 +00:00
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" });
2025-06-27 12:38:06 +08:00
}
// Step 1. Check for maintenance window, if found, skip the check
2026-02-11 19:38:32 +00:00
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
2025-06-27 12:38:06 +08:00
if (maintenanceWindowActive) {
2026-01-15 22:06:46 +00:00
this.logger.debug({
2025-06-27 12:38:06 +08:00
message: `Monitor ${monitorId} is in maintenance window`,
service: SERVICE_NAME,
method: "getMonitorJob",
});
2026-02-11 19:38:32 +00:00
if (monitor.status !== "maintenance") {
await this.monitorsRepository.updateById(monitorId, teamId, { status: "maintenance" });
}
2025-06-27 12:38:06 +08:00
return;
}
2025-08-15 16:04:43 -07:00
2026-01-15 22:06:46 +00:00
// Step 2. Request monitor status
const status = await this.networkService.requestStatus(monitor);
if (!status) {
2025-06-27 12:38:06 +08:00
throw new Error("No network response");
}
2026-01-16 18:11:04 +00:00
// Step 3. Build check
const check = await this.checkService.buildCheck(status);
if (!check) {
this.logger.warn({
message: `No check could be built for monitor ${monitorId}`,
service: SERVICE_NAME,
method: "getMonitorJob",
details: { code: status.code, message: status.message },
});
return;
}
2026-01-16 18:11:04 +00:00
// Step 4 Add check to buffer
2026-02-17 22:27:52 +00:00
this.buffer.addToBuffer(check);
2026-01-16 18:11:04 +00:00
// Step 4. Update monitor status
const statusChangeResult = await this.statusService.updateMonitorStatus(status, check);
2026-02-14 22:25:15 +00:00
// Step 5. Get decisions
const decision = this.evaluateMonitorAction(statusChangeResult);
2026-02-15 18:09:21 +00:00
// Step 6. Handle notifications (best effort, continue even in event of failure, don't wait)
if (decision.shouldSendNotification) {
2026-02-15 18:20:42 +00:00
this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: any) => {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "getMonitorJob",
details: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error.message}`,
stack: error.stack,
2026-01-19 21:39:02 +00:00
});
2026-02-15 18:20:42 +00:00
});
2026-02-15 18:09:21 +00:00
}
2026-01-19 23:41:24 +00:00
2026-02-15 18:09:21 +00:00
// Step 7. Handle incidents (best effort, don't wait)
2026-02-17 17:42:11 +00:00
this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: any) => {
2026-02-15 18:09:21 +00:00
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "getMonitorJob",
details: `Error handling incident for job ${monitor.id}: ${error.message}`,
stack: error.stack,
2026-01-20 19:49:40 +00:00
});
2026-02-15 18:09:21 +00:00
});
2026-01-15 22:06:46 +00:00
} catch (error: any) {
2025-06-27 12:38:06 +08:00
this.logger.warn({
message: error.message,
service: error.service || SERVICE_NAME,
method: error.method || "getMonitorJob",
stack: error.stack,
});
throw error;
}
};
};
2026-02-17 18:30:22 +00:00
getCleanupOrphanedJob = () => {
return async () => {
try {
2026-02-17 19:03:17 +00:00
this.logger.info({
message: "Starting cleanup of orphaned data",
service: SERVICE_NAME,
method: "getCleanupOrphanedJob",
});
// Get all valid team IDs
const validTeamIds = await this.teamsRepository.findAllTeamIds();
this.logger.debug({
message: `Found ${validTeamIds.length} valid teams`,
service: SERVICE_NAME,
method: "getCleanupOrphanedJob",
});
// Remove orphaned monitors (monitors without a valid team)
const deletedMonitorCount = await this.monitorsRepository.deleteByTeamIdsNotIn(validTeamIds);
if (deletedMonitorCount > 0) {
this.logger.info({
message: `Deleted ${deletedMonitorCount} orphaned monitors`,
service: SERVICE_NAME,
method: "getCleanupOrphanedJob",
});
}
// Remove orphaned monitorStats (stats without a valid monitor)
const allMonitorIds = await this.monitorsRepository.findAllMonitorIds();
this.logger.debug({
message: `Found ${allMonitorIds.length} valid monitors`,
service: SERVICE_NAME,
method: "getCleanupOrphanedJob",
});
const deletedStatsCount = await this.monitorStatsRepository.deleteByMonitorIdsNotIn(allMonitorIds);
if (deletedStatsCount > 0) {
this.logger.info({
message: `Deleted ${deletedStatsCount} orphaned monitor stats`,
service: SERVICE_NAME,
method: "getCleanupOrphanedJob",
});
}
2026-02-17 18:30:22 +00:00
// Remove orphaned checks
2026-02-17 19:20:08 +00:00
const deletedChecksCount = await this.checksRepository.deleteByMonitorIdsNotIn(allMonitorIds);
if (deletedChecksCount > 0) {
this.logger.info({
message: `Deleted ${deletedChecksCount} orphaned checks`,
service: SERVICE_NAME,
method: "getCleanupOrphanedJob",
});
}
2026-02-17 18:30:22 +00:00
// Remove orphaned incidents
2026-02-17 19:39:24 +00:00
const deletedIncidentsCount = await this.incidentsRepository.deleteByMonitorIdsNotIn(allMonitorIds);
if (deletedIncidentsCount > 0) {
this.logger.info({
message: `Deleted ${deletedIncidentsCount} orphaned incidents`,
service: SERVICE_NAME,
method: "getCleanupOrphanedJob",
});
}
2026-02-17 19:03:17 +00:00
this.logger.info({
message: "Cleanup of orphaned data completed",
service: SERVICE_NAME,
method: "getCleanupOrphanedJob",
});
2026-02-17 18:30:22 +00:00
} catch (error: any) {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "getCleanupOrphanedJob",
stack: error.stack,
});
throw error;
}
};
};
2026-02-24 17:39:09 +00:00
getGeoCheckJob = () => {
return async (monitor: Monitor) => {
try {
const monitorId = monitor.id;
const teamId = monitor.teamId;
// Step 1: Validate monitor eligibility
if (!monitorId) {
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
}
if (monitor.type !== "http") {
this.logger.debug({
message: `Monitor ${monitorId} is not HTTP type, skipping geo check`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
if (!monitor.geoCheckEnabled) {
return;
}
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
this.logger.warn({
message: `No geo check locations configured for monitor ${monitorId}`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
// Step 2: Check for maintenance window
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
if (maintenanceWindowActive) {
this.logger.debug({
message: `Monitor ${monitorId} is in maintenance window, skipping geo check`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
return;
}
// Step 3: Call GlobalPing API
// TODO: Implement GlobalPing API service
// const measurementId = await this.globalPingService.createMeasurement(monitor.url, monitor.geoCheckLocations);
// const geoResults = await this.globalPingService.pollForResults(measurementId);
// Step 4: Create GeoCheck document
// TODO: Build and save GeoCheck
// const geoCheck = {
// metadata: {
// monitorId,
// teamId,
// type: monitor.type,
// },
// results: geoResults,
// expiry: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
// };
// await this.geoChecksRepository.create(geoCheck);
this.logger.debug({
message: `Geo check job executed for monitor ${monitorId}`,
service: SERVICE_NAME,
method: "getGeoCheckJob",
});
} catch (error: any) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "getGeoCheckJob",
stack: error.stack,
});
// Don't throw - geo check failures shouldn't crash the job scheduler
}
};
};
2026-01-15 22:06:46 +00:00
async isInMaintenanceWindow(monitorId: string, teamId: string) {
2026-01-22 18:11:54 +00:00
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
2025-06-27 12:38:06 +08:00
// Check for active maintenance window:
2026-01-15 22:06:46 +00:00
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => {
2025-06-27 12:38:06 +08:00
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;
}
2026-02-14 21:05:31 +00:00
2026-02-14 22:25:15 +00:00
private evaluateMonitorAction(statusChangeResult: StatusChangeResult): MonitorActionDecision {
2026-02-14 22:52:14 +00:00
const { monitor, statusChanged, prevStatus } = statusChangeResult;
2026-02-14 21:05:31 +00:00
// Initialize result
const decision: MonitorActionDecision = {
shouldCreateIncident: false,
shouldResolveIncident: false,
shouldSendNotification: false,
incidentReason: null,
notificationReason: null,
};
2026-02-14 22:52:14 +00:00
if (!statusChanged) {
2026-02-14 21:05:31 +00:00
return decision;
}
2026-02-14 22:52:14 +00:00
if (monitor.status === "down") {
// Monitor went down (unreachable)
2026-02-14 21:05:31 +00:00
decision.shouldCreateIncident = true;
decision.shouldSendNotification = true;
decision.incidentReason = "status_down";
decision.notificationReason = "status_change";
2026-02-14 22:52:14 +00:00
} else if (monitor.status === "breached") {
// Hardware monitor exceeded thresholds
2026-02-14 21:05:31 +00:00
decision.shouldCreateIncident = true;
decision.shouldSendNotification = true;
decision.incidentReason = "threshold_breach";
decision.notificationReason = "threshold_breach";
2026-02-14 22:52:14 +00:00
} else if (monitor.status === "up" && (prevStatus === "down" || prevStatus === "breached")) {
// Monitor recovered from down or breached state
2026-02-14 22:29:06 +00:00
decision.shouldResolveIncident = true;
2026-02-14 22:52:14 +00:00
decision.shouldSendNotification = true;
decision.notificationReason = "status_change";
2026-02-14 21:05:31 +00:00
}
return decision;
}
2025-06-27 12:38:06 +08:00
}
export default SuperSimpleQueueHelper;