diff --git a/server/src/init/services.ts b/server/src/init/services.ts index 5ffcd2ee3..fb53e0df8 100644 --- a/server/src/init/services.ts +++ b/server/src/init/services.ts @@ -38,13 +38,14 @@ import { MongoChecksRepository, MongoMonitorStatsRepository, } from "@/repositories/index.js"; +import type { CheckEntity } from "@/types/domain/index.js"; export const initServices = async () => { const monitorRepository = new MongoMonitorRepository(); const checksRepository = new MongoChecksRepository(); const monitorStatsRepository = new MongoMonitorStatsRepository(); - const checkService = new CheckService(); + const checkService = new CheckService(checksRepository, monitorRepository); const inviteService = new InviteService(); const maintenanceService = new MaintenanceService(); const monitorStatsService = new MonitorStatsService( diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 713a4fcfa..4762bf3e2 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -2,12 +2,37 @@ import type { AggregateCheck, CheckEntity, Monitor, + MonitorStatus, } from "@/types/domain/index.js"; import type { LatestChecksByMonitor } from "@/repositories/checks/MongoCheckRepository.js"; export interface IChecksRepository { // Create + create(checkData: Partial): Promise; // Single fetch + findById(checkId: string, teamId: string): Promise; // Collection fetch + findByTeamIdAndStatus( + status: MonitorStatus, + teamId: string, + startDate: Date, + page: number, + rowsPerPage: number + ): Promise; + findByMonitorIdAndStatus( + status: MonitorStatus, + teamId: string, + monitorId: string, + startDate: Date, + page: number, + rowsPerPage: number + ): Promise; + findCountByMonitorId(monitorId: string): Promise; + findPageByMonitorId( + monitorId: string, + page: number, + rowsPerPage: number + ): Promise; + findLatestChecksByMonitorIds( monitorIds: string[] ): Promise; @@ -22,6 +47,7 @@ export interface IChecksRepository { ): Promise; // Update // Delete + deleteManyExcludedByMonitorIds(monitorIds: string[]): Promise; } export default IChecksRepository; diff --git a/server/src/repositories/checks/MongoCheckRepository.ts b/server/src/repositories/checks/MongoCheckRepository.ts index 13641b860..e0c495a24 100644 --- a/server/src/repositories/checks/MongoCheckRepository.ts +++ b/server/src/repositories/checks/MongoCheckRepository.ts @@ -1,5 +1,10 @@ import type { IChecksRepository } from "@/repositories/index.js"; -import { CheckEntity, AggregateCheck, Monitor } from "@/types/domain/index.js"; +import type { + CheckEntity, + AggregateCheck, + Monitor, + MonitorStatus, +} from "@/types/domain/index.js"; import type { ICheck } from "@/db/models/index.js"; import { Check } from "@/db/models/index.js"; import mongoose from "mongoose"; @@ -66,11 +71,11 @@ class MongoCheckRepository implements IChecksRepository { }; if (doc.cpu || doc.memory || doc.disk || doc.host || doc.net) { - if (doc.cpu) agg.cpu = doc.cpu; - if (doc.memory) agg.memory = doc.memory; - if (Array.isArray(doc.disk)) agg.disk = doc.disk; - if (doc.host) agg.host = doc.host; - if (Array.isArray(doc.net)) agg.net = doc.net; + if (doc.cpu) (agg as any).cpu = doc.cpu; + if (doc.memory) (agg as any).memory = doc.memory; + if (Array.isArray(doc.disk)) (agg as any).disk = doc.disk; + if (doc.host) (agg as any).host = doc.host; + if (Array.isArray(doc.net)) (agg as any).net = doc.net; } return agg; }; @@ -438,6 +443,111 @@ class MongoCheckRepository implements IChecksRepository { return {}; }; + create = async (checkData: Partial) => { + const check = new Check({ ...checkData }); + check.metadata.monitorId = new mongoose.Types.ObjectId(checkData.monitorId); + check.metadata.teamId = new mongoose.Types.ObjectId(checkData.teamId); + check.metadata.type = checkData.type || "http"; + const inserted = await check.save(); + return this.toEntity(inserted); + }; + + findById = async (checkId: string, teamId: string) => { + const check = await Check.findOne({ + _id: new mongoose.Types.ObjectId(checkId), + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + }); + + if (!check) { + return null; + } + return this.toEntity(check); + }; + + findCountByMonitorId = async (monitorId: string) => { + return await Check.countDocuments({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + }); + }; + + findByTeamIdAndStatus = async ( + status: MonitorStatus, + teamId: string, + startDate: Date, + page: number, + rowsPerPage: number + ) => { + const pageSizePlusOne = rowsPerPage + 1; + + const checks = await Check.find({ + status, + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + createdAt: { $gte: new Date(startDate), $lte: new Date() }, + }) + .sort({ createdAt: -1 }) + .skip(page * rowsPerPage) + .limit(pageSizePlusOne) + .select({ + _id: 1, + status: 1, + httpStatusCode: 1, + message: 1, + responseTime: 1, + createdAt: 1, + "metadata.monitorId": 1, + "metadata.teamId": 1, + "metadata.type": 1, + }); + return checks.map(this.toEntity); + }; + + findByMonitorIdAndStatus = async ( + status: MonitorStatus, + teamId: string, + monitorId: string, + startDate: Date, + page: number, + rowsPerPage: number + ) => { + const pageSizePlusOne = rowsPerPage + 1; + + const checks = await Check.find({ + status, + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + createdAt: { $gte: startDate, $lte: new Date() }, + }) + .sort({ createdAt: -1 }) + .skip(page * rowsPerPage) + .limit(pageSizePlusOne) + .select({ + _id: 1, + status: 1, + httpStatusCode: 1, + message: 1, + responseTime: 1, + createdAt: 1, + "metadata.monitorId": 1, + "metadata.teamId": 1, + "metadata.type": 1, + }); + return checks.map(this.toEntity); + }; + + findPageByMonitorId = async ( + monitorId: string, + page: number, + rowsPerPage: number + ) => { + const checks = await Check.find({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + }) + .sort({ createdAt: -1 }) + .skip(page * rowsPerPage) + .limit(rowsPerPage); + return checks.map(this.toEntity); + }; + findLatestChecksByMonitorIds = async (monitorIds: string[]) => { const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); const checkMap = await Check.aggregate([ @@ -562,7 +672,6 @@ class MongoCheckRepository implements IChecksRepository { .allowDiskUse(true) .hint({ "metadata.monitorId": 1, createdAt: 1 }); - console.log(checks); return checks.map(this.toAggregateCheck); }; @@ -633,6 +742,15 @@ class MongoCheckRepository implements IChecksRepository { return checks.map(this.toAggregateCheck); }; + + deleteManyExcludedByMonitorIds = async (monitorIds: string[]) => { + const result = await Check.deleteMany({ + "metadata.monitorId": { + $nin: monitorIds.map((id) => new mongoose.Types.ObjectId(id)), + }, + }); + return result.deletedCount ?? 0; + }; } export default MongoCheckRepository; diff --git a/server/src/services/business/CheckService.ts b/server/src/services/business/CheckService.ts index a210d35ed..1b01d7f0c 100644 --- a/server/src/services/business/CheckService.ts +++ b/server/src/services/business/CheckService.ts @@ -1,29 +1,35 @@ -import { ICheck, Check, Monitor, ISystemInfo } from "@/db/models/index.js"; +import { ISystemInfo } from "@/types/domain/index.js"; import type { IDockerPayload } from "@/services/infrastructure/NetworkService.js"; import { MonitorStatus, MonitorType } from "@/types/domain/index.js"; -import { StatusResponse } from "../infrastructure/NetworkService.js"; +import { StatusResponse } from "@/services/infrastructure/NetworkService.js"; import type { ICapturePayload, ILighthousePayload, -} from "../infrastructure/NetworkService.js"; -import mongoose from "mongoose"; +} from "@/services/infrastructure/NetworkService.js"; import ApiError from "@/utils/ApiError.js"; import { getChildLogger } from "@/logger/Logger.js"; import { getStartDate } from "@/utils/TimeUtils.js"; +import type { CheckEntity } from "@/types/domain/index.js"; +import type { + IChecksRepository, + IMonitorRepository, +} from "@/repositories/index.js"; + const SERVICE_NAME = "CheckService"; const logger = getChildLogger(SERVICE_NAME); export interface ICheckService { + createCheck: (checkData: Partial) => Promise; buildCheck: ( statusResponse: StatusResponse, type: MonitorType - ) => Promise; + ) => Promise>; getMonitorChecks: ( monitorId: string, page: number, rowsPerPage: number - ) => Promise<{ checks: ICheck[]; count: number }>; + ) => Promise<{ checks: CheckEntity[]; count: number }>; getChecksByStatus: ( status: MonitorStatus, teamId: string, @@ -31,17 +37,27 @@ export interface ICheckService { page: number, rowsPerPage: number, range: string - ) => Promise<{ checks: ICheck[]; hasMore: boolean }>; + ) => Promise<{ checks: CheckEntity[]; hasMore: boolean }>; - getCheckById: (checkId: string, teamId: string) => Promise; + getCheckById: ( + checkId: string, + teamId: string + ) => Promise; cleanupOrphanedChecks: () => Promise; } class CheckService implements ICheckService { public SERVICE_NAME: string; - constructor() { + private checksRepository: IChecksRepository; + private monitorRepository: IMonitorRepository; + constructor( + checksRepository: IChecksRepository, + monitorRepository: IMonitorRepository + ) { this.SERVICE_NAME = SERVICE_NAME; + this.checksRepository = checksRepository; + this.monitorRepository = monitorRepository; } private isCapturePayload = (payload: any): payload is ICapturePayload => { @@ -118,14 +134,10 @@ class CheckService implements ICheckService { }; private buildBaseCheck = (statusResponse: StatusResponse) => { - const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId); - const teamId = new mongoose.Types.ObjectId(statusResponse.teamId); - const checkData: Partial = { - metadata: { - monitorId: monitorId, - teamId: teamId, - type: statusResponse?.type, - }, + const check: Partial = { + monitorId: statusResponse.monitorId, + teamId: statusResponse.teamId, + type: statusResponse?.type, status: statusResponse?.status, httpStatusCode: statusResponse?.code, message: statusResponse?.message, @@ -133,7 +145,6 @@ class CheckService implements ICheckService { timings: statusResponse?.timings, }; - const check: ICheck = new Check(checkData); return check; }; @@ -194,10 +205,15 @@ class CheckService implements ICheckService { return check; }; + createCheck = async (checkData: Partial) => { + const check = await this.checksRepository.create(checkData); + return check; + }; + buildCheck = async ( statusResponse: StatusResponse, type: MonitorType - ): Promise => { + ): Promise> => { switch (type) { case "infrastructure": return this.buildInfrastructureCheck( @@ -227,11 +243,12 @@ class CheckService implements ICheckService { cleanupOrphanedChecks = async () => { try { - const monitorIds = await Monitor.find().distinct("_id"); - const result = await Check.deleteMany({ - "metadata.monitorId": { $nin: monitorIds }, - }); - logger.info(`Deleted ${result.deletedCount} orphaned Checks.`); + const monitorIds = (await this.monitorRepository.findAll()).map( + (m) => m.id + ); + const deletedCount = + await this.checksRepository.deleteManyExcludedByMonitorIds(monitorIds); + logger.info(`Deleted ${deletedCount} orphaned Checks.`); return true; } catch (error) { logger.error("Error cleaning up orphaned Checks:", error); @@ -244,16 +261,13 @@ class CheckService implements ICheckService { page: number, rowsPerPage: number ) => { - const count = await Check.countDocuments({ - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - }); + const count = await this.checksRepository.findCountByMonitorId(monitorId); - const checks = await Check.find({ - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - }) - .sort({ createdAt: -1 }) - .skip(page * rowsPerPage) - .limit(rowsPerPage); + const checks = await this.checksRepository.findPageByMonitorId( + monitorId, + page, + rowsPerPage + ); return { checks, count }; }; @@ -265,89 +279,43 @@ class CheckService implements ICheckService { rowsPerPage: number, range: string ) => { - let match; const startDate = getStartDate(range); + let checks: CheckEntity[] = []; if (monitorId) { - const authorized = await Monitor.exists({ - _id: monitorId, - teamId, - }); + const authorized = await this.monitorRepository.findById( + monitorId, + teamId + ); + if (!authorized) { throw new ApiError("Not authorized", 403); } - - match = { + checks = await this.checksRepository.findByMonitorIdAndStatus( status, - "metadata.teamId": new mongoose.Types.ObjectId(teamId), - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - createdAt: { $gte: startDate, $lte: new Date() }, - }; + teamId, + monitorId, + startDate, + page, + rowsPerPage + ); } else { - match = { + checks = await this.checksRepository.findByTeamIdAndStatus( status, - "metadata.teamId": new mongoose.Types.ObjectId(teamId), - createdAt: { $gte: startDate, $lte: new Date() }, - }; + teamId, + startDate, + page, + rowsPerPage + ); } - const pageSizePlusOne = rowsPerPage + 1; - const docs = await Check.find(match) - .sort({ createdAt: -1 }) - .skip(page * rowsPerPage) - .limit(pageSizePlusOne) - .select({ - _id: 1, - status: 1, - httpStatusCode: 1, - message: 1, - responseTime: 1, - createdAt: 1, - "metadata.monitorId": 1, - "metadata.teamId": 1, - "metadata.type": 1, - }); - - const hasMore = docs.length > rowsPerPage; - const limited = hasMore ? docs.slice(0, rowsPerPage) : docs; - - const monitorIds = Array.from( - new Set( - limited - .map((d) => d?.metadata?.monitorId) - .filter((v): v is mongoose.Types.ObjectId => !!v) - .map((v) => v.toString()) - ) - ); - - let nameById = new Map(); - if (monitorIds.length > 0) { - const monitors = await Monitor.find({ _id: { $in: monitorIds } }).select({ - _id: 1, - name: 1, - }); - nameById = new Map(monitors.map((m) => [m._id.toString(), m.name])); - } - - const checks = limited.map((doc) => { - const asObj = doc.toObject(); - const idStr = asObj?.metadata?.monitorId?.toString?.() ?? ""; - const name = idStr ? nameById.get(idStr) : undefined; - if (name) { - asObj.metadata = asObj.metadata || {}; - (asObj.metadata as any).monitorId = { name } as any; - } - return asObj as ICheck; - }); + const hasMore = checks.length > rowsPerPage; return { checks, hasMore }; }; getCheckById = async (checkId: string, teamId: string) => { - return await Check.findOne({ - _id: new mongoose.Types.ObjectId(checkId), - "metadata.teamId": new mongoose.Types.ObjectId(teamId), - }); + return await this.checksRepository.findById(checkId, teamId); }; } diff --git a/server/src/services/business/IncidentService.ts b/server/src/services/business/IncidentService.ts index ec5625bbc..c35d7a194 100644 --- a/server/src/services/business/IncidentService.ts +++ b/server/src/services/business/IncidentService.ts @@ -3,7 +3,6 @@ import { Incident, IMonitor, Monitor, - ICheck, IUser, } from "@/db/models/index.js"; import type { ResolutionType } from "@/db/models/index.js"; @@ -11,7 +10,10 @@ import mongoose from "mongoose"; import { getChildLogger } from "@/logger/Logger.js"; import { ThresholdEvaluationResult } from "@/services/infrastructure/StatusService.js"; import { getStartDate } from "@/utils/TimeUtils.js"; -import type { Monitor as MonitorEntity } from "@/types/domain/monitor.js"; +import type { + Monitor as MonitorEntity, + CheckEntity, +} from "@/types/domain/index.js"; type IncidentPopulated = Omit & { monitorId: IMonitor; @@ -21,11 +23,11 @@ type IncidentPopulated = Omit & { export interface IIncidentService { handleStatusChange: ( updatedMonitor: MonitorEntity, - lastCheck: ICheck + lastCheck: CheckEntity ) => Promise; handleThresholdBreach: ( updatedMonitor: MonitorEntity, - lastCheck: ICheck, + lastCheck: CheckEntity, evalResult: ThresholdEvaluationResult ) => Promise; create: ( @@ -47,7 +49,7 @@ export interface IIncidentService { teamId: string, incidentId: string, resolutionType: ResolutionType, - lastCheck?: ICheck, + lastCheck?: CheckEntity, resolvedBy?: string, resolutionNote?: string ) => Promise; @@ -69,13 +71,13 @@ class IncidentService implements IIncidentService { handleStatusChange = async ( updatedMonitor: MonitorEntity, - lastCheck: ICheck + lastCheck: CheckEntity ) => { if (updatedMonitor.status === "down") { const incident = await this.create( new mongoose.Types.ObjectId(updatedMonitor.teamId), new mongoose.Types.ObjectId(updatedMonitor.id), - lastCheck._id + new mongoose.Types.ObjectId(lastCheck.id) ); return incident; } else if (updatedMonitor.status === "up") { @@ -102,7 +104,7 @@ class IncidentService implements IIncidentService { handleThresholdBreach = async ( updatedMonitor: MonitorEntity, - lastCheck: ICheck, + lastCheck: CheckEntity, evalResult: ThresholdEvaluationResult ) => { const existing = await Incident.findOne({ @@ -116,7 +118,7 @@ class IncidentService implements IIncidentService { const incident = await this.create( new mongoose.Types.ObjectId(updatedMonitor.teamId), new mongoose.Types.ObjectId(updatedMonitor.id), - lastCheck._id + new mongoose.Types.ObjectId(lastCheck.id) ); if (evalResult.notes.length) { await Incident.updateOne( @@ -147,7 +149,7 @@ class IncidentService implements IIncidentService { return existing; } else if (existing) { const resolved = await this.resolve( - updatedMonitor.teamId.toString(), + updatedMonitor.teamId, existing._id.toString(), "auto", lastCheck @@ -227,7 +229,7 @@ class IncidentService implements IIncidentService { teamId: string, incidentId: string, resolutionType: ResolutionType, - lastCheck?: ICheck, + lastCheck?: CheckEntity, resolvedBy?: string, resolutionNote?: string ) => { @@ -237,7 +239,9 @@ class IncidentService implements IIncidentService { $set: { resolved: true, endedAt: new Date(), - endCheck: lastCheck ? lastCheck._id : undefined, + endCheck: lastCheck + ? new mongoose.Types.ObjectId(lastCheck.id) + : undefined, resolvedBy: resolvedBy ? resolvedBy : undefined, resolutionType, resolutionNote: resolutionNote ? resolutionNote : undefined, diff --git a/server/src/services/infrastructure/JobGenerator.ts b/server/src/services/infrastructure/JobGenerator.ts index b538ad2f9..ce18089a1 100644 --- a/server/src/services/infrastructure/JobGenerator.ts +++ b/server/src/services/infrastructure/JobGenerator.ts @@ -69,8 +69,11 @@ class JobGenerator implements IJobGenerator { const status = await this.networkService.requestStatus(monitor); // Step 3. Build check - const check = await this.checkService.buildCheck(status, monitor.type); - await check.save(); + const checkToInsert = await this.checkService.buildCheck( + status, + monitor.type + ); + const check = await this.checkService.createCheck(checkToInsert); // Step 4. Update monitor's status const [updatedMonitor, statusChanged] = diff --git a/server/src/services/infrastructure/StatusService.ts b/server/src/services/infrastructure/StatusService.ts index f10b6b930..98cc73f1b 100644 --- a/server/src/services/infrastructure/StatusService.ts +++ b/server/src/services/infrastructure/StatusService.ts @@ -4,7 +4,11 @@ import { IMonitorRepository, IMonitorStatsRepository, } from "@/repositories/index.js"; -import type { Monitor, MonitorStats } from "@/types/domain/index.js"; +import type { + Monitor, + MonitorStats, + CheckEntity, +} from "@/types/domain/index.js"; import ApiError from "@/utils/ApiError.js"; const emptyMetrics = (): ThresholdEvaluationResult["metrics"] => ({ @@ -52,7 +56,7 @@ export interface IStatusService { ) => Promise; evaluateThresholds: ( monitor: Monitor, - check: ICheck + check: CheckEntity ) => Promise; } @@ -211,7 +215,7 @@ class StatusService implements IStatusService { evaluateThresholds = async ( monitor: Monitor, - check: ICheck + check: CheckEntity ): Promise => { try { if (monitor.type !== "infrastructure" || monitor.status !== "up")