From 76fa99f7bd7ade0f4691fe8c13b6d0fb498ff143 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 23:59:09 +0000 Subject: [PATCH] getUptimeDetails --- server/src/controllers/monitorController.ts | 13 --- server/src/db/modules/monitorModule.js | 11 -- .../repositories/checks/IChecksRepository.ts | 17 ++- .../checks/MongoChecksRepistory.ts | 100 ++++++++++++++++++ .../monitors/IMonitorsRepository.ts | 2 +- .../monitors/MongoMonitorsRepository.ts | 10 +- server/src/routes/v1/monitorRoute.ts | 1 - server/src/service/business/monitorService.ts | 69 +++++++++--- 8 files changed, 175 insertions(+), 48 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 9c117e9af..527fd334d 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -39,19 +39,6 @@ class MonitorController { } } - getAllMonitors = async (req: Request, res: Response, next: NextFunction) => { - try { - const monitors = await this.monitorService.getAllMonitors(); - return res.status(200).json({ - success: true, - msg: "Retrieved all monitors successfully", - data: monitors, - }); - } catch (error) { - next(error); - } - }; - getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { await getCertificateParamValidation.validateAsync(req.params); diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 9c8ca7e32..08b8e1371 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -194,17 +194,6 @@ class MonitorModule { }; }; - getAllMonitors = async () => { - try { - const monitors = await this.Monitor.find(); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllMonitors"; - throw error; - } - }; - getMonitorById = async (monitorId) => { try { const monitor = await this.Monitor.findById(monitorId); diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index df69cf18a..7741c071b 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,10 +1,17 @@ import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; export interface IChecksRepository { - // create - // single fetch - // collection fetch findLatestChecksByMonitorIds(monitorIds: string[]): Promise; - // update - // delete + findDateRangeChecksByMonitor( + monitorId: string, + startDate: Date, + endDate: Date, + dateString: string + ): Promise<{ + groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; + groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + uptimePercentage: number; + avgResponseTime: number; + }>; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 10b482fb2..76d3658cd 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -209,6 +209,106 @@ class MongoChecksRepistory implements IChecksRepository { return acc; }, {}); }; + + findDateRangeChecksByMonitor = async ( + monitorId: string, + startDate: Date, + endDate: Date, + dateString: string + ): Promise<{ + groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; + groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + uptimePercentage: number; + avgResponseTime: number; + }> => { + const matchStage = { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + updatedAt: { $gte: startDate, $lte: endDate }, + }; + const [result] = await CheckModel.aggregate([ + { $match: matchStage }, + { $sort: { updatedAt: 1 } }, + { + $facet: { + uptimePercentage: [ + { + $group: { + _id: null, + upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } }, + totalChecks: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + percentage: { + $cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }], + }, + }, + }, + ], + groupedAvgResponseTime: [ + { + $group: { + _id: null, + avgResponseTime: { $avg: "$responseTime" }, + }, + }, + ], + groupedChecks: [ + { + $group: { + _id: { + $dateToString: { format: dateString, date: "$createdAt" }, + }, + avgResponseTime: { $avg: "$responseTime" }, + totalChecks: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ], + groupedUpChecks: [ + { $match: { status: true } }, + { + $group: { + _id: { + $dateToString: { format: dateString, date: "$createdAt" }, + }, + totalChecks: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + }, + }, + { $sort: { _id: 1 } }, + ], + groupedDownChecks: [ + { $match: { status: false } }, + { + $group: { + _id: { + $dateToString: { format: dateString, date: "$createdAt" }, + }, + totalChecks: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + }, + }, + { $sort: { _id: 1 } }, + ], + }, + }, + ]).exec(); + + const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0; + const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0; + + return { + groupedChecks: result?.groupedChecks ?? [], + groupedUpChecks: result?.groupedUpChecks ?? [], + groupedDownChecks: result?.groupedDownChecks ?? [], + uptimePercentage, + avgResponseTime, + }; + }; } export default MongoChecksRepistory; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 4d76d49db..0da9c7279 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -15,7 +15,7 @@ export interface IMonitorsRepository { create(monitor: Monitor, teamId: string, userId: string): Promise; createBulkMonitors(monitors: Monitor[]): Promise; // single fetch - findById(monitorId: string): Promise; + findById(monitorId: string, teamId?: string): Promise; // collection fetch findAll(): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 96795569b..c9af6668d 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -21,12 +21,16 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.mapDocuments(inserted); }; - findById = async (MonitorModelId: string): Promise => { - const monitor = await MonitorModel.findById(MonitorModelId); + findById = async (monitorId: string, teamId?: string): Promise => { + const match: { _id: string; teamId?: string } = { _id: monitorId }; + if (teamId) { + match.teamId = teamId; + } + const monitor = await MonitorModel.findOne(match); if (!monitor) { if (monitor === null || monitor === undefined) { throw new AppError({ - message: `Monitor with ID ${MonitorModelId} not found.`, + message: `Monitor with ID ${monitorId} not found.`, status: 404, }); } diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index e68e0019f..a355efbb9 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -38,7 +38,6 @@ class MonitorRoutes { }); // General monitor CRUD routes - this.router.get("/", this.monitorController.getAllMonitors); this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor); this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 004b9f25f..b189f9077 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -1,8 +1,8 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; -import { NormalizeData } from "@/utils/dataUtils.js"; +import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; import type { MonitorType } from "@/types/monitor.js"; -import type { IMonitorsRepository } from "@/repositories/index.js"; +import type { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; @@ -10,6 +10,7 @@ import path from "path"; import { AppError } from "../infrastructure/errorService.js"; const SERVICE_NAME = "MonitorService"; +type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; export interface IMonitorService { readonly serviceName: string; @@ -21,7 +22,6 @@ export interface IMonitorService { addDemoMonitors(args: { userId: string; teamId: string }): Promise; // read - getAllMonitors(): Promise; getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; getMonitorStatsById(args: { teamId: string; @@ -85,7 +85,7 @@ export class MonitorService implements IMonitorService { private errorService: any; private games: any; private monitorsRepository: IMonitorsRepository; - private checksRepository: any; + private checksRepository: IChecksRepository; private fs: any; constructor({ @@ -109,7 +109,7 @@ export class MonitorService implements IMonitorService { errorService: any; games: any; monitorsRepository: IMonitorsRepository; - checksRepository: any; + checksRepository: IChecksRepository; }) { this.db = db; this.jobQueue = jobQueue; @@ -127,6 +127,31 @@ export class MonitorService implements IMonitorService { return MonitorService.SERVICE_NAME; } + private getDateRange = (dateRange: DateRangeKey) => { + const startDates = { + recent: new Date(new Date().setHours(new Date().getHours() - 2)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: new Date(0), + }; + return { + start: startDates[dateRange], + end: new Date(), + }; + }; + + private getDateFormat = (dateRange: DateRangeKey): string => { + const formatLookup = { + recent: "%Y-%m-%dT%H:%M:00Z", + day: "%Y-%m-%dT%H:00:00Z", + week: "%Y-%m-%dT00:00:00Z", + month: "%Y-%m-%dT00:00:00Z", + all: "%Y-%m-%dT00:00:00Z", + }; + return formatLookup[dateRange]; + }; + verifyTeamAccess = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.db.monitorModule.getMonitorById(monitorId); if (!monitor?.teamId?.equals(teamId)) { @@ -225,11 +250,6 @@ export class MonitorService implements IMonitorService { return demoMonitors; }; - getAllMonitors = async (): Promise => { - const monitors = await this.db.monitorModule.getAllMonitors(); - return monitors; - }; - getUptimeDetailsById = async ({ teamId, monitorId, @@ -242,13 +262,34 @@ export class MonitorService implements IMonitorService { normalize?: boolean; }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); - const data = await this.db.monitorModule.getUptimeDetailsById({ + + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); + } + const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const { start, end } = this.getDateRange(rangeKey); + const checksData = await this.checksRepository.findDateRangeChecksByMonitor( + monitorId: monitor.id, + startDate: start, + endDate: end, + dateString: this.getDateFormat(rangeKey), + ); + const monitorStats = await this.db.monitorModule.getMonitorStatsById({ monitorId, - dateRange, - normalize, }); - return data; + return { + monitorData: { + monitor, + groupedChecks: NormalizeDataUptimeDetails(checksData.groupedChecks, 10, 100), + groupedUpChecks: NormalizeDataUptimeDetails(checksData.groupedUpChecks, 10, 100), + groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100), + groupedAvgResponseTime: checksData.avgResponseTime, + groupedUptimePercentage: checksData.uptimePercentage, + }, + monitorStats, + }; }; getMonitorStatsById = async ({