diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 9434b3ae5..b31c8a5e9 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -252,6 +252,7 @@ export const initializeServices = async ({ statusService, notificationsService, checkService, + settingsService, bufferService, incidentService, maintenanceWindowsRepository, diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index 7bd1cec6f..b7f88d012 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -21,13 +21,11 @@ type CheckMetadataDocument = Omit & { type: MonitorType; }; -type CheckDocumentBase = Omit & { +type CheckDocumentBase = Omit & { metadata: CheckMetadataDocument; expiry: Date; - ackAt?: Date | null; createdAt: Date; updatedAt: Date; - __v: number; }; interface CheckDocument extends CheckDocumentBase { @@ -229,14 +227,6 @@ const CheckSchema = new Schema( type: Date, default: Date.now, }, - ack: { - type: Boolean, - default: false, - }, - ackAt: { - type: Date, - default: undefined, - }, cpu: { type: cpuSchema, default: () => ({}), diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index c89e5d5fc..b96d14933 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -47,4 +47,5 @@ export interface IChecksRepository { deleteByMonitorId(monitorId: string): Promise; deleteByTeamId(teamId: string): Promise; deleteByMonitorIdsNotIn(monitorIds: string[]): Promise; + deleteOlderThan(date: Date): Promise; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index b019a0430..3a4485f64 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -46,13 +46,6 @@ class MongoChecksRepository implements IChecksRepository { return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); }; - const toOptionalDateString = (value?: Date | string | null): string | undefined => { - if (!value) { - return undefined; - } - return toDateString(value); - }; - const mapTimings = (timings?: GotTimings): GotTimings => { const phases = timings?.phases ?? { wait: 0, @@ -171,8 +164,6 @@ class MongoChecksRepository implements IChecksRepository { timings: mapTimings(doc.timings), statusCode: doc.statusCode ?? 0, message: doc.message ?? "", - ack: doc.ack ?? false, - ackAt: toOptionalDateString(doc.ackAt), expiry: toDateString(doc.expiry), cpu: mapCpu(doc.cpu), memory: mapMemory(doc.memory), @@ -186,7 +177,6 @@ class MongoChecksRepository implements IChecksRepository { seo: doc.seo, performance: doc.performance, audits: mapAudits(doc.audits), - __v: doc.__v ?? 0, createdAt: toDateString(doc.createdAt), updatedAt: toDateString(doc.updatedAt), }; @@ -412,6 +402,11 @@ class MongoChecksRepository implements IChecksRepository { return result.deletedCount ?? 0; }; + deleteOlderThan = async (date: Date): Promise => { + const result = await CheckModel.deleteMany({ createdAt: { $lt: date } }); + return result.deletedCount ?? 0; + }; + private findUptimeDateRangeChecks = async ( monitorType: Exclude, monitorObjectId: mongoose.Types.ObjectId, diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index c83a3d411..a95939947 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,6 +1,14 @@ import { Types } from "mongoose"; import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; -import type { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js"; +import type { + MonitorStatusResponse, + CheckErrorInfo, + Check, + ILighthouseAudit, + ChecksQueryResult, + ChecksSummary, + MonitorPayloadMap, +} from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; import { ParseBoolean } from "@/utils/utils.js"; @@ -10,12 +18,13 @@ const SERVICE_NAME = "checkService"; export interface ICheckService { createChecks(checks: Check[]): Promise; - buildCheck(statusResponse: MonitorStatusResponse): Partial | undefined; + buildCheck(statusResponse: MonitorStatusResponse): Check | undefined; getChecksByMonitor(params: { monitorId: string; query: Record; teamId: string }): Promise; getChecksByTeam(params: { teamId: string; query: Record }): Promise; getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise; deleteChecks(params: { monitorId: string; teamId: string }): Promise; deleteChecksByTeamId(params: { teamId: string }): Promise; + deleteOlderThan(date: Date): Promise; updateChecksTTL(params: { teamId: string; ttl: string }): Promise; } @@ -39,12 +48,14 @@ export class CheckService implements ICheckService { return this.checksRepository.createChecks(checks); }; - buildCheck = (statusResponse: MonitorStatusResponse) => { + buildCheck = (statusResponse: MonitorStatusResponse): Check | undefined => { const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse; - const check: Partial = { + const now = new Date().toISOString(); + const check: Check = { id: new Types.ObjectId().toString(), - createdAt: new Date().toISOString(), + createdAt: now, + updatedAt: now, metadata: { monitorId, teamId, @@ -55,6 +66,7 @@ export class CheckService implements ICheckService { responseTime: responseTime || 0, timings: timings, message, + expiry: now, }; if (type === "pagespeed") { @@ -190,6 +202,15 @@ export class CheckService implements ICheckService { return deletedCount; }; + deleteOlderThan = async (date: Date) => { + if (!date) { + throw new AppError({ message: "No date provided", service: SERVICE_NAME, method: "deleteChecksOlderThan", status: 400 }); + } + + const deletedCount = await this.checksRepository.deleteOlderThan(date); + return deletedCount; + }; + updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => { throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } }); }; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index a96e7855b..d5a0fa12f 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -89,9 +89,10 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { try { this.scheduler.start(); - this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); - this.scheduler.addTemplate("geo-check-job", this.helper.getGeoCheckJob()); + this.scheduler.addTemplate("monitor-job", this.helper.getHeartbeatJob()); + this.scheduler.addTemplate("geo-check-job", this.helper.getHeartbeatGeoJob()); this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob()); + this.scheduler.addTemplate("cleanup-retention-job", this.helper.getCleanupRetentionJob()); const monitors = await this.monitorsRepository.findAll(); if (!monitors) { return true; @@ -104,6 +105,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { } this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true }); + this.scheduler.addJob({ id: "cleanup-retention", template: "cleanup-retention-job", active: true, repeat: 24 * 60 * 60 * 1000 }); return true; } catch (error: any) { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index eef0a75be..eca380537 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -2,8 +2,16 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { supportsGeoCheck } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js"; -import type { StatusChangeResult } from "@/types/index.js"; +import { + ICheckService, + INetworkService, + INotificationsService, + ISettingsService, + IStatusService, + IncidentService, + type IGeoChecksService, +} from "@/service/index.js"; +import type { MaintenanceWindow, StatusChangeResult } from "@/types/index.js"; import { IMaintenanceWindowsRepository, IMonitorsRepository, @@ -18,9 +26,10 @@ import { IBufferService } from "@/service/index.js"; export interface ISuperSimpleQueueHelper { readonly serviceName: string; - getMonitorJob(): (monitor: Monitor) => Promise; + getHeartbeatJob(): (monitor: Monitor) => Promise; + getHeartbeatGeoJob(): (monitor: Monitor) => Promise; getCleanupOrphanedJob(): () => Promise; - getGeoCheckJob(): (monitor: Monitor) => Promise; + getCleanupRetentionJob(): () => Promise; isInMaintenanceWindow(monitorId: string, teamId: string): Promise; } @@ -41,11 +50,12 @@ export interface MonitorActionDecision { export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; - private logger: any; + private logger: ILogger; private networkService: INetworkService; private statusService: IStatusService; private notificationsService: INotificationsService; - private checkService: any; + private checkService: ICheckService; + private settingsService: ISettingsService; private buffer: IBufferService; private incidentService: IncidentService; private maintenanceWindowsRepository: IMaintenanceWindowsRepository; @@ -62,7 +72,8 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { networkService: INetworkService, statusService: IStatusService, notificationsService: INotificationsService, - checkService: any, + checkService: ICheckService, + settingsService: ISettingsService, buffer: IBufferService, incidentService: IncidentService, maintenanceWindowsRepository: IMaintenanceWindowsRepository, @@ -78,6 +89,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.networkService = networkService; this.statusService = statusService; this.checkService = checkService; + this.settingsService = settingsService; this.buffer = buffer; this.notificationsService = notificationsService; this.incidentService = incidentService; @@ -95,7 +107,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return SuperSimpleQueueHelper.SERVICE_NAME; } - getMonitorJob = () => { + getHeartbeatJob = () => { return async (monitor: Monitor) => { try { const monitorId = monitor.id; @@ -126,7 +138,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { } // Step 3. Build check - const check = await this.checkService.buildCheck(status); + const check = this.checkService.buildCheck(status); if (!check) { this.logger.warn({ message: `No check could be built for monitor ${monitorId}`, @@ -146,33 +158,31 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { // Step 6. Handle notifications (best effort, continue even in event of failure, don't wait) if (decision.shouldSendNotification) { - this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: any) => { + this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: unknown) => { this.logger.error({ - message: error.message, + message: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error.message}`, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); }); } // Step 7. Handle incidents (best effort, don't wait) - this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: any) => { + this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: unknown) => { this.logger.warn({ - message: error.message, + message: `Error handling incident for job ${monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error handling incident for job ${monitor.id}: ${error.message}`, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); }); - } catch (error: any) { + } catch (error: unknown) { this.logger.warn({ - message: error.message, - service: error.service || SERVICE_NAME, - method: error.method || "getMonitorJob", - stack: error.stack, + message: error instanceof Error ? error.message : "Unknown error", + service: SERVICE_NAME, + method: "getMonitorJob", + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -258,19 +268,19 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { service: SERVICE_NAME, method: "getCleanupOrphanedJob", }); - } catch (error: any) { + } catch (error: unknown) { this.logger.warn({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "getCleanupOrphanedJob", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } }; }; - getGeoCheckJob = () => { + getHeartbeatGeoJob = () => { return async (monitor: Monitor) => { try { const monitorId = monitor.id; @@ -278,7 +288,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { // Step 1: Validate monitor eligibility if (!monitorId) { - throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" }); + throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getHeartbeatGeoJob" }); } if (!monitor.geoCheckEnabled) { @@ -288,7 +298,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.debug({ message: `Monitor ${monitorId} type does not support geo checks, skipping`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -297,7 +307,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.warn({ message: `No geo check locations configured for monitor ${monitorId}`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -308,7 +318,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.debug({ message: `Monitor ${monitorId} is in maintenance window, skipping geo check`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -319,7 +329,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.warn({ message: `No geo check could be built for monitor ${monitorId}`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -330,14 +340,14 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.debug({ message: `Geo check job executed for monitor ${monitorId}`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, - method: "getGeoCheckJob", - stack: error.stack, + method: "getHeartbeatGeoJob", + stack: error instanceof Error ? error.stack : undefined, }); // Don't throw - geo check failures shouldn't crash the job scheduler } @@ -347,7 +357,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { async isInMaintenanceWindow(monitorId: string, teamId: string) { const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId); // Check for active maintenance window: - const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => { + const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: boolean, window: MaintenanceWindow) => { if (window.active) { const start = new Date(window.start); const end = new Date(window.end); @@ -374,6 +384,31 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return maintenanceWindowIsActive; } + getCleanupRetentionJob = () => { + return async () => { + try { + const settings = await this.settingsService.getDBSettings(); + + const checkTTL = settings.checkTTL; // Check TTL is in DAYS, not MS + const checkTTLInMs = checkTTL * 24 * 60 * 60 * 1000; + const cutoffDate = new Date(Date.now() - checkTTLInMs); + const deleteCount = await this.checkService.deleteOlderThan(cutoffDate); + this.logger.info({ + message: `Deleted ${deleteCount} checks older than ${cutoffDate.toISOString()}`, + service: SERVICE_NAME, + method: "getCleanupRetentionJob", + }); + } catch (error: unknown) { + this.logger.error({ + message: error instanceof Error ? error.message : "Unknown error", + service: SERVICE_NAME, + method: "getCleanupRetentionJob", + stack: error instanceof Error ? error.stack : undefined, + }); + } + }; + }; + private evaluateMonitorAction(statusChangeResult: StatusChangeResult): MonitorActionDecision { const { monitor, statusChanged, prevStatus } = statusChangeResult; diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index e475bc24b..9111ef7b2 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,4 +1,4 @@ -import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; +import type { Monitor, MonitorStatusResponse, MonitorPayloadMap, MonitorType } from "@/types/index.js"; import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; @@ -8,7 +8,7 @@ const SERVICE_NAME = "NetworkService"; export interface INetworkService { readonly serviceName: string; - requestStatus(monitor: Monitor): Promise>; + requestStatus(monitor: Monitor & { type: T }): Promise>; requestWebhook( type: string, url: string, @@ -43,12 +43,12 @@ export class NetworkService implements INetworkService { } // Main entry point - async requestStatus(monitor: Monitor) { + async requestStatus(monitor: Monitor & { type: T }): Promise> { const provider = this.providers.find((p) => p.supports(monitor.type)); if (!provider) { - return this.handleUnsupportedType(monitor.type); + return this.handleUnsupportedType(monitor.type) as Promise>; } - return provider.handle(monitor); + return provider.handle(monitor) as Promise>; } private async handleUnsupportedType(type: string): Promise { diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 6c4c26445..810454fdd 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -98,8 +98,6 @@ export interface Check { timings?: GotTimings; statusCode: number; message: string; - ack: boolean; - ackAt?: string | null; expiry: string; cpu?: CheckCpuInfo; memory?: CheckMemoryInfo; @@ -113,7 +111,6 @@ export interface Check { seo?: number; performance?: number; audits?: CheckAudits; - __v: number; createdAt: string; updatedAt: string; } @@ -205,4 +202,4 @@ export type NormalizedUptimeCheck = T & { originalAvgResponseTime: number; }; -export type CheckSnapshot = Omit; +export type CheckSnapshot = Omit; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 07e7c3736..6c7172724 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -126,7 +126,7 @@ export interface MonitorPayloadMap { game: GameStatusPayload; grpc: GrpcStatusPayload; websocket: WebSocketStatusPayload; - default: unknown; + unknown: unknown; } export type StatusChangeResult = {