diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 6ed260a64..2a85c47c1 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -91,7 +91,7 @@ export type InitializedServices = { maintenanceWindowService: any; monitorService: any; incidentService: any; - logger: any; + logger: ILogger; notificationsService: INotificationsService; statusPageService: IStatusPageService; notificationMessageBuilder: INotificationMessageBuilder; @@ -143,52 +143,20 @@ export const initializeServices = async ({ const teamsRepository = new MongoTeamsRepository(); const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); - const networkService = new NetworkService({ - axios, - got, - https, - jmespath, - GameDig, - ping, - logger, - http, - Docker, - net, - settingsService, - }); + const networkService = new NetworkService(axios, got, https, jmespath, GameDig, ping, logger, Docker, net, settingsService); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const notificationMessageBuilder = new NotificationMessageBuilder(); - const incidentService = new IncidentService({ - logger, - incidentsRepository, - monitorsRepository, - usersRepository, - notificationMessageBuilder, - }); + const incidentService = new IncidentService(logger, incidentsRepository, monitorsRepository, usersRepository, notificationMessageBuilder); - const checkService = new CheckService({ - monitorsRepository, - logger, - checksRepository, - }); + const checkService = new CheckService(monitorsRepository, logger, checksRepository); - const globalPingService = new GlobalPingService({ logger }); + const globalPingService = new GlobalPingService(logger); - // Create geoChecksService with circular dependency workaround - const geoChecksService = new GeoChecksService({ - logger, - geoChecksRepository, - globalPingService, - bufferService: null as any, - settingsService, - }); + const geoChecksService = new GeoChecksService(logger, geoChecksRepository, globalPingService); - const bufferService = new BufferService({ logger, checkService, geoChecksService, settingsService }); - - // Set bufferService reference - (geoChecksService as any).bufferService = bufferService; + const bufferService = new BufferService(logger, checkService, geoChecksService, settingsService); const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository); @@ -269,6 +237,7 @@ export const initializeServices = async ({ games, monitorsRepository, checksRepository, + geoChecksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 8bd131dfc..52884163d 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -134,6 +134,31 @@ class MonitorController { } }; + getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => { + try { + await getHardwareDetailsByIdParamValidation.validateAsync(req.params); + await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); + + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const teamId = requireTeamId(req?.user?.teamId); + + const data = await this.monitorService.getGeoChecksByMonitorId({ + teamId, + monitorId, + dateRange, + }); + + return res.status(200).json({ + success: true, + msg: "Geo checks retrieved successfully", + data, + }); + } catch (error) { + next(error); + } + }; + getMonitorById = async (req: Request, res: Response, next: NextFunction) => { try { await getMonitorByIdParamValidation.validateAsync(req.params); diff --git a/server/src/routes/monitorRoute.ts b/server/src/routes/monitorRoute.ts index afe8c7819..476e421ce 100755 --- a/server/src/routes/monitorRoute.ts +++ b/server/src/routes/monitorRoute.ts @@ -30,6 +30,9 @@ class MonitorRoutes { // PageSpeed routes this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById); + // Geo checks routes + this.router.get("/:monitorId/geo-checks", this.monitorController.getGeoChecksByMonitorId); + // General monitor routes this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor); diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 5564d1d7f..fd7f07a55 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,10 +1,10 @@ -import { requireTeamId } from "@/controllers/controllerUtils.js"; import { Types } from "mongoose"; import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; -import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; +import type { MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; import { ParseBoolean } from "@/utils/utils.js"; +import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "checkService"; @@ -14,15 +14,7 @@ class CheckService { private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; private logger: any; - constructor({ - monitorsRepository, - logger, - checksRepository, - }: { - monitorsRepository: IMonitorsRepository; - logger: any; - checksRepository: IChecksRepository; - }) { + constructor(monitorsRepository: IMonitorsRepository, logger: ILogger, checksRepository: IChecksRepository) { this.monitorsRepository = monitorsRepository; this.logger = logger; this.checksRepository = checksRepository; diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index 351943143..a40ce82c7 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -3,42 +3,28 @@ import type { GeoCheckResult } from "@/types/geoCheck.js"; import { Types } from "mongoose"; import type { IGeoChecksRepository } from "@/repositories/index.js"; import type { IGlobalPingService } from "@/service/infrastructure/globalPingService.js"; -import type { IBufferService } from "@/service/infrastructure/bufferService.js"; +import type { ILogger } from "@/utils/logger.js"; +import type { ISettingsService } from "@/service/system/settingsService.js"; const SERVICE_NAME = "GeoChecksService"; export interface IGeoChecksService { readonly serviceName: string; - executeGeoCheck(monitor: Monitor): Promise; + buildGeoCheck(monitor: Monitor): Promise; + createGeoChecks(geoChecks: GeoCheck[]): Promise; } class GeoChecksService implements IGeoChecksService { static SERVICE_NAME = SERVICE_NAME; - private logger: any; + private logger: ILogger; private geoChecksRepository: IGeoChecksRepository; private globalPingService: IGlobalPingService; - private bufferService: IBufferService; - private TTL_DAYS: number; - constructor({ - logger, - geoChecksRepository, - globalPingService, - bufferService, - settingsService, - }: { - logger: any; - geoChecksRepository: IGeoChecksRepository; - globalPingService: IGlobalPingService; - bufferService: IBufferService; - settingsService: any; - }) { + constructor(logger: ILogger, geoChecksRepository: IGeoChecksRepository, globalPingService: IGlobalPingService) { this.logger = logger; this.geoChecksRepository = geoChecksRepository; this.globalPingService = globalPingService; - this.bufferService = bufferService; - this.TTL_DAYS = settingsService.getSettings().checksTTL || 90; } get serviceName() { @@ -46,31 +32,31 @@ class GeoChecksService implements IGeoChecksService { } /** - * Execute a geo-distributed check for a monitor + * Build a geo-distributed check for a monitor * 1. Create measurement request with GlobalPing API * 2. Poll for results (with 30s timeout) - * 3. Transform and save results to buffer + * 3. Transform and return GeoCheck document */ - async executeGeoCheck(monitor: Monitor): Promise { + async buildGeoCheck(monitor: Monitor): Promise { try { if (!monitor.url) { this.logger.warn({ message: "Monitor missing URL for geo check", service: SERVICE_NAME, - method: "executeGeoCheck", + method: "buildGeoCheck", details: { monitorId: monitor.id }, }); - return; + return null; } if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) { this.logger.warn({ message: "Monitor missing geo check locations", service: SERVICE_NAME, - method: "executeGeoCheck", + method: "buildGeoCheck", details: { monitorId: monitor.id }, }); - return; + return null; } // Step 1: Create measurement request @@ -81,10 +67,10 @@ class GeoChecksService implements IGeoChecksService { this.logger.debug({ message: "Skipping geo check due to API unavailability", service: SERVICE_NAME, - method: "executeGeoCheck", + method: "buildGeoCheck", details: { monitorId: monitor.id }, }); - return; + return null; } // Step 2: Poll for results @@ -95,38 +81,39 @@ class GeoChecksService implements IGeoChecksService { this.logger.debug({ message: "No successful geo check results", service: SERVICE_NAME, - method: "executeGeoCheck", + method: "buildGeoCheck", details: { monitorId: monitor.id, measurementId }, }); - return; + return null; } // Step 3: Build GeoCheck document - const geoCheck = this.buildGeoCheck(monitor, results); - - // Step 4: Add to buffer for batched insertion - this.bufferService.addGeoCheckToBuffer(geoCheck); + const geoCheck = this.createGeoCheckDocument(monitor, results); this.logger.debug({ message: `Geo check completed for monitor ${monitor.id}`, service: SERVICE_NAME, - method: "executeGeoCheck", + method: "buildGeoCheck", details: { monitorId: monitor.id, resultsCount: results.length }, }); + + return geoCheck; } catch (error: any) { this.logger.error({ message: "Error executing geo check", service: SERVICE_NAME, - method: "executeGeoCheck", + method: "buildGeoCheck", details: { monitorId: monitor.id, error: error.message }, stack: error.stack, }); + return null; } } - private buildGeoCheck(monitor: Monitor, results: GeoCheckResult[]): GeoCheck { + private createGeoCheckDocument(monitor: Monitor, results: GeoCheckResult[]): GeoCheck { const now = new Date(); - const expiryDate = new Date(now.getTime() + this.TTL_DAYS * 24 * 60 * 60 * 1000); + const ttl = 90 * 24 * 60 * 60 * 1000; // 90 days in ms + const expiryDate = new Date(now.getTime() + ttl); return { id: new Types.ObjectId().toString(), diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 9b3344f11..6559a5c57 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -7,6 +7,7 @@ import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from import type { Incident } from "@/types/index.js"; import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js"; +import type { ILogger } from "@/utils/logger.js"; const dateRangeLookup: Record = { recent: new Date(new Date().setHours(new Date().getHours() - 2)), @@ -20,25 +21,19 @@ const dateRangeLookup: Record = { class IncidentService { static SERVICE_NAME = SERVICE_NAME; - private logger: any; + private logger: ILogger; private incidentsRepository: IIncidentsRepository; private monitorsRepository: IMonitorsRepository; private usersRepository: IUsersRepository; private notificationMessageBuilder: INotificationMessageBuilder; - constructor({ - logger, - incidentsRepository, - monitorsRepository, - usersRepository, - notificationMessageBuilder, - }: { - logger: any; - incidentsRepository: IIncidentsRepository; - monitorsRepository: IMonitorsRepository; - usersRepository: IUsersRepository; - notificationMessageBuilder: INotificationMessageBuilder; - }) { + constructor( + logger: ILogger, + incidentsRepository: IIncidentsRepository, + monitorsRepository: IMonitorsRepository, + usersRepository: IUsersRepository, + notificationMessageBuilder: INotificationMessageBuilder + ) { this.logger = logger; this.incidentsRepository = incidentsRepository; this.monitorsRepository = monitorsRepository; @@ -151,7 +146,7 @@ class IncidentService { service: SERVICE_NAME, method: "resolveIncidentManually", message: `Incident manually resolved by user`, - details: resolvedIncident.id, + details: { incidentId: resolvedIncident.id }, }); return resolvedIncident; @@ -160,7 +155,7 @@ class IncidentService { service: SERVICE_NAME, method: "resolveIncident", message: error.message, - details: incidentId, + details: { id: incidentId }, stack: error.stack, }); throw error; @@ -207,7 +202,7 @@ class IncidentService { service: SERVICE_NAME, method: "getIncidentsByTeam", message: error.message, - details: teamId, + details: { teamId }, stack: error.stack, }); throw error; @@ -229,7 +224,7 @@ class IncidentService { service: SERVICE_NAME, method: "getIncidentSummary", message: error.message, - details: teamId, + details: { teamId }, stack: error.stack, }); throw error; @@ -250,7 +245,7 @@ class IncidentService { service: SERVICE_NAME, method: "getIncidentById", message: error.message, - details: incidentId, + details: { incidentId }, stack: error.stack, }); throw error; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 2005a4a5d..157727755 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -10,6 +10,7 @@ import type { } from "@/types/monitor.js"; import type { IChecksRepository, + IGeoChecksRepository, IIncidentsRepository, IMonitorsRepository, IMonitorStatsRepository, @@ -36,6 +37,7 @@ export interface IMonitorService { getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; dateRange: string }): Promise; getMonitorById(args: { teamId: string; monitorId: string }): Promise; getMonitorsByTeamId(args: { teamId: string; @@ -84,6 +86,7 @@ export class MonitorService implements IMonitorService { private games: any; private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; + private geoChecksRepository: IGeoChecksRepository; private monitorStatsRepository: IMonitorStatsRepository; private statusPagesRepository: IStatusPagesRepository; private incidentsRepository: IIncidentsRepository; @@ -95,6 +98,7 @@ export class MonitorService implements IMonitorService { games, monitorsRepository, checksRepository, + geoChecksRepository, monitorStatsRepository, statusPagesRepository, incidentsRepository, @@ -105,6 +109,7 @@ export class MonitorService implements IMonitorService { games: any; monitorsRepository: IMonitorsRepository; checksRepository: IChecksRepository; + geoChecksRepository: IGeoChecksRepository; monitorStatsRepository: IMonitorStatsRepository; statusPagesRepository: IStatusPagesRepository; incidentsRepository: IIncidentsRepository; @@ -115,6 +120,7 @@ export class MonitorService implements IMonitorService { this.games = games; this.monitorsRepository = monitorsRepository; this.checksRepository = checksRepository; + this.geoChecksRepository = geoChecksRepository; this.monitorStatsRepository = monitorStatsRepository; this.statusPagesRepository = statusPagesRepository; this.incidentsRepository = incidentsRepository; @@ -315,6 +321,24 @@ export class MonitorService implements IMonitorService { monitorStats, }; }; + + getGeoChecksByMonitorId = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); + } + + if (monitor.type !== "http" || !monitor.geoCheckEnabled) { + return { groupedGeoChecks: [] }; + } + + const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const { start, end } = this.getDateRange(rangeKey); + const groupedGeoChecks = await this.geoChecksRepository.findGroupedByMonitorIdAndDateRange(monitor.id, start, end, this.getDateFormat(rangeKey)); + + return { groupedGeoChecks }; + }; + getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); return monitor; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 90827408d..c2304de57 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -306,8 +306,19 @@ class SuperSimpleQueueHelper { return; } - // Step 3: Execute geo check (handles API calls, polling, and saving) - await this.geoChecksService.executeGeoCheck(monitor); + // Step 3: Build geo check (handles API calls and polling) + const geoCheck = await this.geoChecksService.buildGeoCheck(monitor); + if (!geoCheck) { + this.logger.warn({ + message: `No geo check could be built for monitor ${monitorId}`, + service: SERVICE_NAME, + method: "getGeoCheckJob", + }); + return; + } + + // Step 4: Add geo check to buffer + this.buffer.addGeoCheckToBuffer(geoCheck); this.logger.debug({ message: `Geo check job executed for monitor ${monitorId}`, diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index a32a57dde..a56ae4bd8 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -1,7 +1,8 @@ import type { Check } from "@/types/index.js"; import type { GeoCheck } from "@/types/index.js"; -import type Logger from "@/utils/logger.js"; - +import type { IGeoChecksService } from "../business/geoChecksService.js"; +import type { ILogger } from "@/utils/logger.js"; +import type { ISettingsService } from "@/service/system/settingsService.js"; const SERVICE_NAME = "BufferService"; export interface IBufferService { @@ -16,25 +17,15 @@ export interface IBufferService { class BufferService implements IBufferService { static SERVICE_NAME = SERVICE_NAME; private BUFFER_TIMEOUT: number; - private logger: Logger; + private logger: ILogger; private SERVICE_NAME: string; private buffer: any[]; private geoBuffer: any[]; private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; - private geoChecksService: any; + private geoChecksService: IGeoChecksService; - constructor({ - logger, - checkService, - geoChecksService, - settingsService, - }: { - logger: Logger; - checkService: any; - geoChecksService?: any; - settingsService: any; - }) { + constructor(logger: ILogger, checkService: any, geoChecksService: IGeoChecksService, settingsService: ISettingsService) { this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute this.logger = logger; this.checksService = checkService; diff --git a/server/src/service/infrastructure/globalPingService.ts b/server/src/service/infrastructure/globalPingService.ts index 720eb246a..1d49c6679 100644 --- a/server/src/service/infrastructure/globalPingService.ts +++ b/server/src/service/infrastructure/globalPingService.ts @@ -1,5 +1,5 @@ import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js"; -import type Logger from "@/utils/logger.js"; +import type { ILogger } from "@/utils/logger.js"; import got from "got"; const SERVICE_NAME = "GlobalPingService"; @@ -57,9 +57,9 @@ export interface IGlobalPingService { class GlobalPingService implements IGlobalPingService { static SERVICE_NAME = SERVICE_NAME; - private logger: Logger; + private logger: ILogger; - constructor({ logger }: { logger: Logger }) { + constructor(logger: ILogger) { this.logger = logger; } diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 92d269808..914929bfb 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -3,6 +3,8 @@ import type { Got, Response } from "got"; import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; import CacheableLookup from "cacheable-lookup"; +import { ISettingsService } from "../system/settingsService.js"; +import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "NetworkService"; type MonitorStatusResponseOverrides = Partial, "monitorId" | "teamId" | "type">>; @@ -111,31 +113,18 @@ class NetworkService implements INetworkService { }; }; - constructor({ - axios, - got, - https, - jmespath, - GameDig, - ping, - logger, - http, - Docker, - net, - settingsService, - }: { - axios: any; - got: Got; - https: any; - jmespath: any; - GameDig: any; - ping: any; - logger: any; - http: any; - Docker: any; - net: any; - settingsService: any; - }) { + constructor( + axios: any, + got: Got, + https: any, + jmespath: any, + GameDig: any, + ping: any, + logger: ILogger, + Docker: any, + net: any, + settingsService: ISettingsService + ) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed";