mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-20 00:18:47 -05:00
resolve conflicts
This commit is contained in:
@@ -2,6 +2,7 @@ import MonitorController from "../controllers/monitorController.js";
|
||||
import AuthController from "../controllers/authController.js";
|
||||
import SettingsController from "../controllers/settingsController.js";
|
||||
import CheckController from "../controllers/checkController.js";
|
||||
import GeoCheckController from "../controllers/geoCheckController.js";
|
||||
import InviteController from "../controllers/inviteController.js";
|
||||
import MaintenanceWindowController from "../controllers/maintenanceWindowController.js";
|
||||
import QueueController from "../controllers/queueController.js";
|
||||
@@ -17,6 +18,7 @@ export interface InitializedControllers {
|
||||
monitorController: MonitorController;
|
||||
settingsController: SettingsController;
|
||||
checkController: CheckController;
|
||||
geoCheckController: GeoCheckController;
|
||||
inviteController: InviteController;
|
||||
maintenanceWindowController: MaintenanceWindowController;
|
||||
queueController: QueueController;
|
||||
@@ -32,6 +34,7 @@ export const initializeControllers = (services: InitializedServices): Initialize
|
||||
monitorController: new MonitorController(services.monitorService),
|
||||
settingsController: new SettingsController(services.settingsService, services.emailService),
|
||||
checkController: new CheckController(services.checkService),
|
||||
geoCheckController: new GeoCheckController(services.geoChecksService),
|
||||
inviteController: new InviteController(services.inviteService),
|
||||
maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService),
|
||||
queueController: new QueueController(services.jobQueue),
|
||||
|
||||
@@ -6,6 +6,7 @@ import AuthRoutes from "../routes/authRoute.js";
|
||||
import InviteRoutes from "../routes/inviteRoute.js";
|
||||
import MonitorRoutes from "../routes/monitorRoute.js";
|
||||
import CheckRoutes from "../routes/checkRoute.js";
|
||||
import GeoCheckRoutes from "../routes/geoCheckRoutes.js";
|
||||
import SettingsRoutes from "../routes/settingsRoute.js";
|
||||
import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js";
|
||||
import StatusPageRoutes from "../routes/statusPageRoute.js";
|
||||
@@ -22,6 +23,7 @@ export const setupRoutes = (app: any, controllers: Record<string, any>, services
|
||||
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
|
||||
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
|
||||
const checkRoutes = new CheckRoutes(controllers.checkController);
|
||||
const geoCheckRoutes = new GeoCheckRoutes(controllers.geoCheckController);
|
||||
const inviteRoutes = new InviteRoutes(controllers.inviteController, verifyJWT);
|
||||
const maintenanceWindowRoutes = new MaintenanceWindowRoutes(controllers.maintenanceWindowController);
|
||||
const queueRoutes = new QueueRoutes(controllers.queueController);
|
||||
@@ -36,6 +38,7 @@ export const setupRoutes = (app: any, controllers: Record<string, any>, services
|
||||
app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter());
|
||||
app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter());
|
||||
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
|
||||
app.use("/api/v1/geo-checks", verifyJWT, geoCheckRoutes.getRouter());
|
||||
app.use("/api/v1/invite", inviteRoutes.getRouter());
|
||||
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
|
||||
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
|
||||
|
||||
@@ -17,6 +17,8 @@ import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/S
|
||||
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||
import UserService from "../service/business/userService.js";
|
||||
import CheckService from "../service/business/checkService.js";
|
||||
import GeoChecksService from "../service/business/geoChecksService.js";
|
||||
import GlobalPingService from "../service/infrastructure/globalPingService.js";
|
||||
import DiagnosticService from "../service/business/diagnosticService.js";
|
||||
import InviteService from "../service/business/inviteService.js";
|
||||
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
|
||||
@@ -48,6 +50,7 @@ import * as protoLoader from "@grpc/proto-loader";
|
||||
import {
|
||||
MongoMonitorsRepository,
|
||||
MongoChecksRepository,
|
||||
MongoGeoChecksRepository,
|
||||
MongoMonitorStatsRepository,
|
||||
MongoStatusPagesRepository,
|
||||
MongoUsersRepository,
|
||||
@@ -59,6 +62,7 @@ import {
|
||||
MongoMaintenanceWindowsRepository,
|
||||
IMonitorsRepository,
|
||||
IChecksRepository,
|
||||
IGeoChecksRepository,
|
||||
IMonitorStatsRepository,
|
||||
IStatusPagesRepository,
|
||||
IUsersRepository,
|
||||
@@ -83,12 +87,13 @@ export type InitializedServices = {
|
||||
jobQueue: any;
|
||||
userService: any;
|
||||
checkService: any;
|
||||
geoChecksService: any;
|
||||
diagnosticService: any;
|
||||
inviteService: any;
|
||||
maintenanceWindowService: any;
|
||||
monitorService: any;
|
||||
incidentService: any;
|
||||
logger: any;
|
||||
logger: ILogger;
|
||||
notificationsService: INotificationsService;
|
||||
statusPageService: IStatusPageService;
|
||||
notificationMessageBuilder: INotificationMessageBuilder;
|
||||
@@ -96,6 +101,7 @@ export type InitializedServices = {
|
||||
// Repositories
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
checksRepository: IChecksRepository;
|
||||
geoChecksRepository: IGeoChecksRepository;
|
||||
monitorStatsRepository: IMonitorStatsRepository;
|
||||
statusPagesRepository: IStatusPagesRepository;
|
||||
usersRepository: IUsersRepository;
|
||||
@@ -128,6 +134,7 @@ export const initializeServices = async ({
|
||||
// Repositories
|
||||
const monitorsRepository = new MongoMonitorsRepository();
|
||||
const checksRepository = new MongoChecksRepository(logger);
|
||||
const geoChecksRepository = new MongoGeoChecksRepository(logger);
|
||||
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||
const statusPagesRepository = new MongoStatusPagesRepository();
|
||||
const usersRepository = new MongoUsersRepository();
|
||||
@@ -138,40 +145,25 @@ 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,
|
||||
grpc,
|
||||
protoLoader,
|
||||
});
|
||||
const networkService = new NetworkService(axios, got, https, jmespath, GameDig, ping, logger, Docker, net, settingsService, grpc, protoLoader);
|
||||
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
||||
|
||||
const notificationMessageBuilder = new NotificationMessageBuilder();
|
||||
|
||||
const incidentService = new IncidentService({
|
||||
const incidentService = new IncidentService(logger, incidentsRepository, monitorsRepository, usersRepository, notificationMessageBuilder);
|
||||
|
||||
const checkService = new CheckService(monitorsRepository, logger, checksRepository);
|
||||
|
||||
const globalPingService = new GlobalPingService(logger);
|
||||
|
||||
const geoChecksService = new GeoChecksService({
|
||||
logger,
|
||||
incidentsRepository,
|
||||
geoChecksRepository,
|
||||
globalPingService,
|
||||
monitorsRepository,
|
||||
usersRepository,
|
||||
notificationMessageBuilder,
|
||||
});
|
||||
|
||||
const checkService = new CheckService({
|
||||
monitorsRepository,
|
||||
logger,
|
||||
checksRepository,
|
||||
});
|
||||
|
||||
const bufferService = new BufferService({ logger, checkService, settingsService });
|
||||
const bufferService = new BufferService(logger, checkService, geoChecksService, settingsService);
|
||||
|
||||
const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository);
|
||||
|
||||
@@ -196,13 +188,13 @@ export const initializeServices = async ({
|
||||
notificationMessageBuilder
|
||||
);
|
||||
|
||||
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
|
||||
const superSimpleQueueHelper = new SuperSimpleQueueHelper(
|
||||
logger,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationsService,
|
||||
checkService,
|
||||
buffer: bufferService,
|
||||
bufferService,
|
||||
incidentService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
@@ -210,13 +202,11 @@ export const initializeServices = async ({
|
||||
monitorStatsRepository,
|
||||
checksRepository,
|
||||
incidentsRepository,
|
||||
});
|
||||
geoChecksService,
|
||||
geoChecksRepository
|
||||
);
|
||||
|
||||
const superSimpleQueue = await SuperSimpleQueue.create({
|
||||
logger,
|
||||
helper: superSimpleQueueHelper,
|
||||
monitorsRepository,
|
||||
});
|
||||
const superSimpleQueue = await SuperSimpleQueue.create(logger, superSimpleQueueHelper, monitorsRepository);
|
||||
|
||||
// Business services
|
||||
const userService = new UserService({
|
||||
@@ -251,6 +241,7 @@ export const initializeServices = async ({
|
||||
games,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
geoChecksRepository,
|
||||
monitorStatsRepository,
|
||||
statusPagesRepository,
|
||||
incidentsRepository,
|
||||
@@ -269,6 +260,7 @@ export const initializeServices = async ({
|
||||
jobQueue: superSimpleQueue,
|
||||
userService,
|
||||
checkService,
|
||||
geoChecksService,
|
||||
diagnosticService,
|
||||
inviteService,
|
||||
maintenanceWindowService,
|
||||
@@ -282,6 +274,7 @@ export const initializeServices = async ({
|
||||
// Repositories
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
geoChecksRepository,
|
||||
monitorStatsRepository,
|
||||
statusPagesRepository,
|
||||
usersRepository,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/joi.js";
|
||||
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
||||
|
||||
const SERVICE_NAME = "geoCheckController";
|
||||
|
||||
class GeoCheckController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private geoChecksService: IGeoChecksService;
|
||||
constructor(geoChecksService: IGeoChecksService) {
|
||||
this.geoChecksService = geoChecksService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return GeoCheckController.SERVICE_NAME;
|
||||
}
|
||||
|
||||
getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getChecksParamValidation.validateAsync(req.params);
|
||||
await getChecksQueryValidation.validateAsync(req.query);
|
||||
|
||||
const result = await this.geoChecksService.getGeoChecksByMonitor({
|
||||
monitorId: req?.params?.monitorId as string,
|
||||
query: req?.query,
|
||||
teamId: req?.user?.teamId as string,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Geo checks retrieved successfully",
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default GeoCheckController;
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from "./controllerUtils.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorService } from "@/service/index.js";
|
||||
import { GeoContinent } from "@/types/geoCheck.js";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
class MonitorController {
|
||||
@@ -134,6 +135,38 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
const monitorId = requireString(req?.params?.monitorId, "Monitor ID");
|
||||
const dateRange = requireString(req?.query?.dateRange, "dateRange");
|
||||
const continentParam = req?.query?.continent;
|
||||
const continents = continentParam
|
||||
? Array.isArray(continentParam)
|
||||
? (continentParam as GeoContinent[])
|
||||
: [continentParam as GeoContinent]
|
||||
: undefined;
|
||||
const teamId = requireTeamId(req?.user?.teamId);
|
||||
|
||||
const data = await this.monitorService.getGeoChecksByMonitorId({
|
||||
teamId,
|
||||
monitorId,
|
||||
dateRange,
|
||||
continents,
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { Schema, model, Types } from "mongoose";
|
||||
import { MonitorTypes, type MonitorType } from "@/types/monitor.js";
|
||||
import type { GeoCheck, GeoCheckLocation, GeoCheckMetadata, GeoCheckResult, GeoCheckTimings } from "@/types/geoCheck.js";
|
||||
|
||||
type GeoCheckMetadataDocument = Omit<GeoCheckMetadata, "monitorId" | "teamId"> & {
|
||||
monitorId: Types.ObjectId;
|
||||
teamId: Types.ObjectId;
|
||||
type: MonitorType;
|
||||
};
|
||||
|
||||
type GeoCheckDocumentBase = Omit<GeoCheck, "id" | "metadata" | "expiry" | "createdAt" | "updatedAt" | "results"> & {
|
||||
metadata: GeoCheckMetadataDocument;
|
||||
results: GeoCheckResult[];
|
||||
expiry: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
__v: number;
|
||||
};
|
||||
|
||||
export interface GeoCheckDocument extends GeoCheckDocumentBase {
|
||||
_id: Types.ObjectId;
|
||||
}
|
||||
|
||||
const geoCheckMetadataSchema = new Schema<GeoCheckMetadataDocument>(
|
||||
{
|
||||
monitorId: { type: Schema.Types.ObjectId, required: true, index: true },
|
||||
teamId: { type: Schema.Types.ObjectId, required: true, index: true },
|
||||
type: { type: String, required: true, enum: MonitorTypes },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const geoCheckTimingsSchema = new Schema<GeoCheckTimings>(
|
||||
{
|
||||
total: { type: Number, default: 0 },
|
||||
dns: { type: Number, default: 0 },
|
||||
tcp: { type: Number, default: 0 },
|
||||
tls: { type: Number, default: 0 },
|
||||
firstByte: { type: Number, default: 0 },
|
||||
download: { type: Number, default: 0 },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const geoCheckLocationSchema = new Schema<GeoCheckLocation>(
|
||||
{
|
||||
continent: { type: String, required: true },
|
||||
region: { type: String, default: "" },
|
||||
country: { type: String, default: "" },
|
||||
state: { type: String, default: "" },
|
||||
city: { type: String, default: "" },
|
||||
longitude: { type: Number, default: 0 },
|
||||
latitude: { type: Number, default: 0 },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const geoCheckResultSchema = new Schema<GeoCheckResult>(
|
||||
{
|
||||
location: {
|
||||
type: geoCheckLocationSchema,
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
statusCode: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
timings: {
|
||||
type: geoCheckTimingsSchema,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const GeoCheckSchema = new Schema<GeoCheckDocument>(
|
||||
{
|
||||
metadata: {
|
||||
type: geoCheckMetadataSchema,
|
||||
required: true,
|
||||
},
|
||||
results: {
|
||||
type: [geoCheckResultSchema],
|
||||
required: true,
|
||||
default: [],
|
||||
},
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
strict: false,
|
||||
timeseries: {
|
||||
timeField: "createdAt",
|
||||
metaField: "metadata",
|
||||
granularity: "seconds",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
GeoCheckSchema.index({ "metadata.monitorId": 1, createdAt: -1 });
|
||||
GeoCheckSchema.index({ "metadata.monitorId": 1, createdAt: 1 });
|
||||
GeoCheckSchema.index({ "metadata.teamId": 1, createdAt: -1 });
|
||||
GeoCheckSchema.index({ createdAt: 1 });
|
||||
|
||||
const GeoCheckModel = model<GeoCheckDocument>("GeoCheck", GeoCheckSchema);
|
||||
|
||||
export type { GeoCheckMetadataDocument };
|
||||
export { GeoCheckModel };
|
||||
export default GeoCheckModel;
|
||||
@@ -173,6 +173,18 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
return value && value.trim() ? value.trim() : null;
|
||||
},
|
||||
},
|
||||
geoCheckEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
geoCheckLocations: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
geoCheckInterval: {
|
||||
type: Number,
|
||||
default: 300000,
|
||||
},
|
||||
recentChecks: {
|
||||
type: [checkSnapshotSchema],
|
||||
default: [],
|
||||
|
||||
@@ -33,3 +33,6 @@ export { default as TeamModel } from "@/db/models/Team.js";
|
||||
|
||||
export * from "@/db/models/MaintenanceWindow.js";
|
||||
export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow.js";
|
||||
|
||||
export * from "@/db/models/GeoCheck.js";
|
||||
export { default as GeoCheckModel } from "@/db/models/GeoCheck.js";
|
||||
|
||||
@@ -15,18 +15,10 @@ import type {
|
||||
} from "@/types/index.js";
|
||||
import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
const dateRangeLookup: Record<string, Date | undefined> = {
|
||||
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
||||
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
|
||||
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: undefined,
|
||||
};
|
||||
|
||||
export type LatestChecksMap = Record<string, Check[]>;
|
||||
type DateRange = { start: Date; end: Date };
|
||||
type HardwareUpChecks = { totalChecks: number };
|
||||
@@ -248,9 +240,9 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
const matchStage: Record<string, any> = {
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
...(typeof status !== "undefined" && { status }),
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
...(getDateForRange(dateRange) && {
|
||||
createdAt: {
|
||||
$gte: dateRangeLookup[dateRange],
|
||||
$gte: getDateForRange(dateRange),
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -299,9 +291,9 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
|
||||
const matchStage: Record<string, any> = {
|
||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
...(getDateForRange(dateRange) && {
|
||||
createdAt: {
|
||||
$gte: dateRangeLookup[dateRange],
|
||||
$gte: getDateForRange(dateRange),
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -386,9 +378,9 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
findSummaryByTeamId = async (teamId: string, dateRange: string) => {
|
||||
const baseMatch = {
|
||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||
...(dateRangeLookup[dateRange] && {
|
||||
...(getDateForRange(dateRange) && {
|
||||
createdAt: {
|
||||
$gte: dateRangeLookup[dateRange],
|
||||
$gte: getDateForRange(dateRange),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { GeoCheck, GroupedGeoCheck } from "@/types/geoCheck.js";
|
||||
import type { GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
||||
|
||||
export interface GeoChecksQueryResult {
|
||||
geoChecksCount: number;
|
||||
geoChecks: GeoCheck[];
|
||||
}
|
||||
|
||||
export interface FlatGeoChecksQueryResult {
|
||||
geoChecksCount: number;
|
||||
geoChecks: FlatGeoCheck[];
|
||||
}
|
||||
|
||||
export interface IGeoChecksRepository {
|
||||
createGeoChecks(geoChecks: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">[]): Promise<GeoCheck[]>;
|
||||
findByMonitorId(
|
||||
monitorId: string,
|
||||
sortOrder: string,
|
||||
dateRange: string,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
continents?: GeoContinent[]
|
||||
): Promise<FlatGeoChecksQueryResult>;
|
||||
findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise<GeoCheck[]>;
|
||||
findGroupedByMonitorIdAndDateRange(
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateFormat: string,
|
||||
continents?: GeoContinent[]
|
||||
): Promise<GroupedGeoCheck[]>;
|
||||
deleteByMonitorId(monitorId: string): Promise<number>;
|
||||
deleteByTeamId(teamId: string): Promise<number>;
|
||||
deleteByMonitorIdsNotIn(monitorIds: string[]): Promise<number>;
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
|
||||
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
||||
import type { GeoChecksQueryResult, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
||||
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||
|
||||
const SERVICE_NAME = "GeoChecksRepository";
|
||||
|
||||
class MongoGeoChecksRepository implements IGeoChecksRepository {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: any;
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private toEntity = (doc: GeoCheckDocument): GeoCheck => {
|
||||
const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
|
||||
};
|
||||
|
||||
const toDateString = (value?: Date | string | null): string => {
|
||||
if (!value) {
|
||||
return new Date(0).toISOString();
|
||||
}
|
||||
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
||||
};
|
||||
|
||||
const mapMetadata = (metadata: any): GeoCheckMetadata => ({
|
||||
monitorId: toStringId(metadata.monitorId),
|
||||
teamId: toStringId(metadata.teamId),
|
||||
type: metadata.type,
|
||||
});
|
||||
|
||||
const mapResults = (results: any[]): GeoCheckResult[] => {
|
||||
if (!results || !Array.isArray(results)) {
|
||||
return [];
|
||||
}
|
||||
return results.map((result) => ({
|
||||
location: {
|
||||
continent: result.location?.continent ?? "",
|
||||
region: result.location?.region ?? "",
|
||||
country: result.location?.country ?? "",
|
||||
state: result.location?.state ?? "",
|
||||
city: result.location?.city ?? "",
|
||||
longitude: result.location?.longitude ?? 0,
|
||||
latitude: result.location?.latitude ?? 0,
|
||||
},
|
||||
status: result.status ?? false,
|
||||
statusCode: result.statusCode ?? 0,
|
||||
timings: {
|
||||
total: result.timings?.total ?? 0,
|
||||
dns: result.timings?.dns ?? 0,
|
||||
tcp: result.timings?.tcp ?? 0,
|
||||
tls: result.timings?.tls ?? 0,
|
||||
firstByte: result.timings?.firstByte ?? 0,
|
||||
download: result.timings?.download ?? 0,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
metadata: mapMetadata(doc.metadata),
|
||||
results: mapResults(doc.results),
|
||||
expiry: toDateString(doc.expiry),
|
||||
__v: doc.__v ?? 0,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
createGeoChecks = async (geoChecks: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">[]): Promise<GeoCheck[]> => {
|
||||
try {
|
||||
const docs = await GeoCheckModel.insertMany(
|
||||
geoChecks.map((geoCheck) => ({
|
||||
metadata: {
|
||||
monitorId: new mongoose.Types.ObjectId(geoCheck.metadata.monitorId),
|
||||
teamId: new mongoose.Types.ObjectId(geoCheck.metadata.teamId),
|
||||
type: geoCheck.metadata.type,
|
||||
},
|
||||
results: geoCheck.results,
|
||||
expiry: new Date(geoCheck.expiry),
|
||||
}))
|
||||
);
|
||||
return docs.map((doc) => this.toEntity(doc));
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Failed to createGeoChecks: ${error.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createGeoChecks",
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
findByMonitorId = async (
|
||||
monitorId: string,
|
||||
sortOrder: string,
|
||||
dateRange: string,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
continents?: GeoContinent[]
|
||||
): Promise<FlatGeoChecksQueryResult> => {
|
||||
try {
|
||||
const matchStage: Record<string, any> = {
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
...(getDateForRange(dateRange) && {
|
||||
createdAt: {
|
||||
$gte: getDateForRange(dateRange),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const convertedSortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
let skip = 0;
|
||||
if (page && rowsPerPage) {
|
||||
skip = page * rowsPerPage;
|
||||
}
|
||||
|
||||
if (continents && continents.length > 0) {
|
||||
const pipeline: any[] = [
|
||||
{ $match: matchStage },
|
||||
{ $unwind: "$results" },
|
||||
{
|
||||
$match: {
|
||||
"results.location.continent": { $in: continents },
|
||||
},
|
||||
},
|
||||
];
|
||||
} else {
|
||||
const pipeline: any[] = [{ $match: matchStage }, { $unwind: "$results" }];
|
||||
}
|
||||
|
||||
// Common pipeline stages for both paths
|
||||
const pipeline: any[] = [
|
||||
{ $match: matchStage },
|
||||
{ $unwind: "$results" },
|
||||
// Filter by continent if specified
|
||||
...(continents && continents.length > 0
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
"results.location.continent": { $in: continents },
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Project to flat structure
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
monitorId: "$metadata.monitorId",
|
||||
teamId: "$metadata.teamId",
|
||||
type: "$metadata.type",
|
||||
location: "$results.location",
|
||||
status: "$results.status",
|
||||
statusCode: "$results.statusCode",
|
||||
timings: "$results.timings",
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
},
|
||||
},
|
||||
{ $sort: { createdAt: convertedSortOrder } },
|
||||
{ $skip: skip },
|
||||
{ $limit: rowsPerPage },
|
||||
];
|
||||
|
||||
// Count pipeline
|
||||
const countPipeline: any[] = [
|
||||
{ $match: matchStage },
|
||||
{ $unwind: "$results" },
|
||||
...(continents && continents.length > 0
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
"results.location.continent": { $in: continents },
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ $count: "count" },
|
||||
];
|
||||
|
||||
const [countResult, dataResults] = await Promise.all([GeoCheckModel.aggregate(countPipeline), GeoCheckModel.aggregate(pipeline)]);
|
||||
|
||||
const geoChecksCount = countResult[0]?.count || 0;
|
||||
const geoChecks: FlatGeoCheck[] = dataResults.map((doc) => ({
|
||||
id: `${doc.monitorId.toString()}-${new Date(doc.createdAt).getTime()}-${doc.location.continent}-${doc.location.city}-${Math.random().toString(36).substring(2, 15)}`,
|
||||
monitorId: doc.monitorId.toString(),
|
||||
teamId: doc.teamId.toString(),
|
||||
type: doc.type,
|
||||
location: doc.location,
|
||||
status: doc.status,
|
||||
statusCode: doc.statusCode,
|
||||
timings: doc.timings,
|
||||
createdAt: new Date(doc.createdAt).toISOString(),
|
||||
updatedAt: new Date(doc.updatedAt).toISOString(),
|
||||
}));
|
||||
|
||||
return { geoChecksCount, geoChecks };
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error finding geo checks by monitor ID: ${error.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "findByMonitorId",
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
findByMonitorIdAndDateRange = async (monitorId: string, startDate: Date, endDate: Date): Promise<GeoCheck[]> => {
|
||||
try {
|
||||
const docs = await GeoCheckModel.find({
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
createdAt: {
|
||||
$gte: startDate,
|
||||
$lte: endDate,
|
||||
},
|
||||
}).sort({ createdAt: -1 });
|
||||
return docs.map(this.toEntity);
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error finding geo checks by monitor ID and date range: ${error.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "findByMonitorIdAndDateRange",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
findGroupedByMonitorIdAndDateRange = async (
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateFormat: string,
|
||||
continents?: GeoContinent[]
|
||||
): Promise<GroupedGeoCheck[]> => {
|
||||
try {
|
||||
const pipeline: any[] = [
|
||||
// Match geo checks for this monitor in date range
|
||||
{
|
||||
$match: {
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
createdAt: {
|
||||
$gte: startDate,
|
||||
$lte: endDate,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Unwind the results array to process each location separately
|
||||
{
|
||||
$unwind: "$results",
|
||||
},
|
||||
// Filter by continent if specified
|
||||
...(continents && continents.length > 0
|
||||
? [
|
||||
{
|
||||
$match: {
|
||||
"results.location.continent": { $in: continents },
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Group by date bucket and continent
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
bucketDate: {
|
||||
$dateToString: {
|
||||
format: dateFormat,
|
||||
date: "$createdAt",
|
||||
},
|
||||
},
|
||||
continent: "$results.location.continent",
|
||||
},
|
||||
avgResponseTime: { $avg: "$results.timings.total" },
|
||||
totalChecks: { $sum: 1 },
|
||||
upChecks: {
|
||||
$sum: {
|
||||
$cond: ["$results.status", 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// Calculate uptime percentage
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
bucketDate: "$_id.bucketDate",
|
||||
continent: "$_id.continent",
|
||||
avgResponseTime: { $round: ["$avgResponseTime", 2] },
|
||||
totalChecks: 1,
|
||||
uptimePercentage: {
|
||||
$round: [
|
||||
{
|
||||
$multiply: [
|
||||
{
|
||||
$divide: ["$upChecks", "$totalChecks"],
|
||||
},
|
||||
100,
|
||||
],
|
||||
},
|
||||
2,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Sort by date and continent
|
||||
{
|
||||
$sort: {
|
||||
bucketDate: 1,
|
||||
continent: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const results = await GeoCheckModel.aggregate(pipeline);
|
||||
return results as GroupedGeoCheck[];
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error finding grouped geo checks: ${error.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "findGroupedByMonitorIdAndDateRange",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteByMonitorId = async (monitorId: string): Promise<number> => {
|
||||
try {
|
||||
const result = await GeoCheckModel.deleteMany({
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
});
|
||||
return result.deletedCount || 0;
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error deleting geo checks by monitor ID: ${error.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteByMonitorId",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteByTeamId = async (teamId: string): Promise<number> => {
|
||||
try {
|
||||
const result = await GeoCheckModel.deleteMany({
|
||||
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
|
||||
});
|
||||
return result.deletedCount || 0;
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error deleting geo checks by team ID: ${error.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteByTeamId",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise<number> => {
|
||||
try {
|
||||
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } });
|
||||
return result.deletedCount || 0;
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error deleting orphaned geo checks: ${error.message}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "deleteByMonitorIdsNotIn",
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { MongoGeoChecksRepository };
|
||||
export default MongoGeoChecksRepository;
|
||||
@@ -33,3 +33,6 @@ export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeams
|
||||
|
||||
export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js";
|
||||
export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js";
|
||||
|
||||
export * from "@/repositories/geo-checks/IGeoChecksRepository.js";
|
||||
export { default as MongoGeoChecksRepository } from "@/repositories/geo-checks/MongoGeoChecksRepository.js";
|
||||
|
||||
@@ -355,6 +355,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
@@ -411,6 +414,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Router } from "express";
|
||||
|
||||
class GeoCheckRoutes {
|
||||
private router: Router;
|
||||
private geoCheckController: any;
|
||||
|
||||
constructor(geoCheckController: any) {
|
||||
this.router = Router();
|
||||
this.geoCheckController = geoCheckController;
|
||||
this.initRoutes();
|
||||
}
|
||||
|
||||
initRoutes() {
|
||||
this.router.get("/:monitorId", this.geoCheckController.getGeoChecksByMonitor);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
return this.router;
|
||||
}
|
||||
}
|
||||
|
||||
export default GeoCheckRoutes;
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import type { Monitor, GeoCheck } from "@/types/index.js";
|
||||
import type { GeoCheckResult } from "@/types/geoCheck.js";
|
||||
import { Types } from "mongoose";
|
||||
import type { IGeoChecksRepository } from "@/repositories/index.js";
|
||||
import type { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import type { IGlobalPingService } from "@/service/infrastructure/globalPingService.js";
|
||||
import type { ILogger } from "@/utils/logger.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
const SERVICE_NAME = "GeoChecksService";
|
||||
|
||||
export interface IGeoChecksService {
|
||||
readonly serviceName: string;
|
||||
buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null>;
|
||||
createGeoChecks(geoChecks: GeoCheck[]): Promise<GeoCheck[]>;
|
||||
getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise<any>;
|
||||
}
|
||||
|
||||
class GeoChecksService implements IGeoChecksService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: ILogger;
|
||||
private geoChecksRepository: IGeoChecksRepository;
|
||||
private globalPingService: IGlobalPingService;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
|
||||
constructor({
|
||||
logger,
|
||||
geoChecksRepository,
|
||||
globalPingService,
|
||||
monitorsRepository,
|
||||
}: {
|
||||
logger: ILogger;
|
||||
geoChecksRepository: IGeoChecksRepository;
|
||||
globalPingService: IGlobalPingService;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
}) {
|
||||
this.logger = logger;
|
||||
this.geoChecksRepository = geoChecksRepository;
|
||||
this.globalPingService = globalPingService;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return GeoChecksService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async buildGeoCheck(monitor: Monitor): Promise<GeoCheck | null> {
|
||||
try {
|
||||
if (!monitor.url) {
|
||||
this.logger.warn({
|
||||
message: "Monitor missing URL for geo check",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildGeoCheck",
|
||||
details: { monitorId: monitor.id },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
|
||||
this.logger.warn({
|
||||
message: "Monitor missing geo check locations",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildGeoCheck",
|
||||
details: { monitorId: monitor.id },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 1: Create measurement request
|
||||
const measurementId = await this.globalPingService.createMeasurement(monitor.url, monitor.geoCheckLocations);
|
||||
|
||||
if (!measurementId) {
|
||||
// GlobalPing API is down, skip this check
|
||||
this.logger.debug({
|
||||
message: "Skipping geo check due to API unavailability",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildGeoCheck",
|
||||
details: { monitorId: monitor.id },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 2: Poll for results
|
||||
const results = await this.globalPingService.pollForResults(measurementId);
|
||||
|
||||
if (results.length === 0) {
|
||||
// No successful results (all locations timed out or failed)
|
||||
this.logger.debug({
|
||||
message: "No successful geo check results",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildGeoCheck",
|
||||
details: { monitorId: monitor.id, measurementId },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 3: Build GeoCheck document
|
||||
const geoCheck = this.createGeoCheckDocument(monitor, results);
|
||||
|
||||
this.logger.debug({
|
||||
message: `Geo check completed for monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
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: "buildGeoCheck",
|
||||
details: { monitorId: monitor.id, error: error.message },
|
||||
stack: error.stack,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private createGeoCheckDocument(monitor: Monitor, results: GeoCheckResult[]): GeoCheck {
|
||||
const now = new Date();
|
||||
const ttl = 90 * 24 * 60 * 60 * 1000; // 90 days in ms
|
||||
const expiryDate = new Date(now.getTime() + ttl);
|
||||
|
||||
return {
|
||||
id: new Types.ObjectId().toString(),
|
||||
metadata: {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
},
|
||||
results,
|
||||
expiry: expiryDate.toISOString(),
|
||||
__v: 0,
|
||||
createdAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
createGeoChecks = async (geoChecks: GeoCheck[]) => {
|
||||
return this.geoChecksRepository.createGeoChecks(geoChecks);
|
||||
};
|
||||
|
||||
getGeoChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => {
|
||||
if (!monitorId) {
|
||||
throw new AppError({
|
||||
message: "No monitor ID in request",
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoChecksByMonitor",
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
if (!teamId) {
|
||||
throw new AppError({
|
||||
message: "No team ID in request",
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoChecksByMonitor",
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
if (!monitor) {
|
||||
throw new AppError({
|
||||
message: `Monitor with ID ${monitorId} not found.`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoChecksByMonitor",
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
let { sortOrder, dateRange, page, rowsPerPage, continent } = query;
|
||||
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
|
||||
|
||||
this.logger.debug({
|
||||
message: "getGeoChecksByMonitor query params",
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoChecksByMonitor",
|
||||
details: { continent, continents, query },
|
||||
});
|
||||
|
||||
const parsedPage = page ? parseInt(page) : page;
|
||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
||||
|
||||
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export default GeoChecksService;
|
||||
@@ -3,42 +3,29 @@ import type { Monitor } from "@/types/monitor.js";
|
||||
import type { MonitorStatusResponse } from "@/types/network.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { ParseBoolean } from "@/utils/utils.js";
|
||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||
import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js";
|
||||
import type { Incident } from "@/types/index.js";
|
||||
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
|
||||
|
||||
const dateRangeLookup: Record<string, Date | undefined> = {
|
||||
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
|
||||
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
|
||||
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: undefined,
|
||||
};
|
||||
import type { ILogger } from "@/utils/logger.js";
|
||||
|
||||
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 +138,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 +147,7 @@ class IncidentService {
|
||||
service: SERVICE_NAME,
|
||||
method: "resolveIncident",
|
||||
message: error.message,
|
||||
details: incidentId,
|
||||
details: { id: incidentId },
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
@@ -182,7 +169,7 @@ class IncidentService {
|
||||
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentsByTeam", status: 400 });
|
||||
}
|
||||
|
||||
const startDate = dateRangeLookup[dateRange];
|
||||
const startDate = getDateForRange(dateRange);
|
||||
|
||||
const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
|
||||
const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
|
||||
@@ -207,7 +194,7 @@ class IncidentService {
|
||||
service: SERVICE_NAME,
|
||||
method: "getIncidentsByTeam",
|
||||
message: error.message,
|
||||
details: teamId,
|
||||
details: { teamId },
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
@@ -229,7 +216,7 @@ class IncidentService {
|
||||
service: SERVICE_NAME,
|
||||
method: "getIncidentSummary",
|
||||
message: error.message,
|
||||
details: teamId,
|
||||
details: { teamId },
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
@@ -250,7 +237,7 @@ class IncidentService {
|
||||
service: SERVICE_NAME,
|
||||
method: "getIncidentById",
|
||||
message: error.message,
|
||||
details: incidentId,
|
||||
details: { incidentId },
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
|
||||
@@ -8,8 +8,10 @@ import type {
|
||||
PageSpeedDetailsResult,
|
||||
GamesMap,
|
||||
} from "@/types/monitor.js";
|
||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||
import type {
|
||||
IChecksRepository,
|
||||
IGeoChecksRepository,
|
||||
IIncidentsRepository,
|
||||
IMonitorsRepository,
|
||||
IMonitorStatsRepository,
|
||||
@@ -36,6 +38,7 @@ export interface IMonitorService {
|
||||
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<UptimeDetailsResult>;
|
||||
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<HardwareDetailsResult>;
|
||||
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<PageSpeedDetailsResult>;
|
||||
getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; dateRange: string; continents?: GeoContinent[] }): Promise<any>;
|
||||
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||
getMonitorsByTeamId(args: {
|
||||
teamId: string;
|
||||
@@ -84,6 +87,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 +99,7 @@ export class MonitorService implements IMonitorService {
|
||||
games,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
geoChecksRepository,
|
||||
monitorStatsRepository,
|
||||
statusPagesRepository,
|
||||
incidentsRepository,
|
||||
@@ -105,6 +110,7 @@ export class MonitorService implements IMonitorService {
|
||||
games: any;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
checksRepository: IChecksRepository;
|
||||
geoChecksRepository: IGeoChecksRepository;
|
||||
monitorStatsRepository: IMonitorStatsRepository;
|
||||
statusPagesRepository: IStatusPagesRepository;
|
||||
incidentsRepository: IIncidentsRepository;
|
||||
@@ -115,6 +121,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;
|
||||
@@ -316,6 +323,40 @@ export class MonitorService implements IMonitorService {
|
||||
monitorStats,
|
||||
};
|
||||
};
|
||||
|
||||
getGeoChecksByMonitorId = async ({
|
||||
teamId,
|
||||
monitorId,
|
||||
dateRange,
|
||||
continents,
|
||||
}: {
|
||||
teamId: string;
|
||||
monitorId: string;
|
||||
dateRange: string;
|
||||
continents?: GeoContinent[];
|
||||
}): Promise<any> => {
|
||||
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),
|
||||
continents
|
||||
);
|
||||
|
||||
return { groupedGeoChecks };
|
||||
};
|
||||
|
||||
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
return monitor;
|
||||
@@ -439,6 +480,14 @@ export class MonitorService implements IMonitorService {
|
||||
});
|
||||
});
|
||||
|
||||
await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: any) => {
|
||||
this.logger.warn({
|
||||
message: `Error deleting geo checks for monitor ${monitor.id} with name ${monitor.name}`,
|
||||
service: SERVICE_NAME,
|
||||
stack: err.stack,
|
||||
});
|
||||
});
|
||||
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
return monitor;
|
||||
};
|
||||
@@ -450,6 +499,7 @@ export class MonitorService implements IMonitorService {
|
||||
try {
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
await this.checksRepository.deleteByMonitorId(monitor.id);
|
||||
await this.geoChecksRepository.deleteByMonitorId(monitor.id);
|
||||
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
|
||||
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import Scheduler from "super-simple-scheduler";
|
||||
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
|
||||
type QueueJobFailure = {
|
||||
@@ -54,22 +56,12 @@ export interface ISuperSimpleQueue {
|
||||
class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: any;
|
||||
private helper: any;
|
||||
private logger: ILogger;
|
||||
private helper: ISuperSimpleQueueHelper;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private readonly scheduler: Scheduler;
|
||||
|
||||
constructor({
|
||||
logger,
|
||||
helper,
|
||||
monitorsRepository,
|
||||
scheduler,
|
||||
}: {
|
||||
logger: any;
|
||||
helper: any;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
scheduler: Scheduler;
|
||||
}) {
|
||||
constructor(logger: ILogger, helper: ISuperSimpleQueueHelper, monitorsRepository: IMonitorsRepository, scheduler: Scheduler) {
|
||||
this.logger = logger;
|
||||
this.helper = helper;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
@@ -80,14 +72,14 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
return SuperSimpleQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create({ logger, helper, monitorsRepository }: { logger: any; helper: any; monitorsRepository: IMonitorsRepository }) {
|
||||
static async create(logger: ILogger, helper: ISuperSimpleQueueHelper, monitorsRepository: IMonitorsRepository) {
|
||||
const scheduler = new Scheduler({
|
||||
// storeType: "mongo",
|
||||
// storeType: "redis",
|
||||
logLevel: "debug",
|
||||
// dbUri: envSettings.dbConnectionString,
|
||||
});
|
||||
const instance = new SuperSimpleQueue({ logger, helper, monitorsRepository, scheduler });
|
||||
const instance = new SuperSimpleQueue(logger, helper, monitorsRepository, scheduler);
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
@@ -97,6 +89,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||
this.scheduler.addTemplate("geo-check-job", this.helper.getGeoCheckJob());
|
||||
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
|
||||
const monitors = await this.monitorsRepository.findAll();
|
||||
if (!monitors) {
|
||||
@@ -112,7 +105,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true });
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: "Failed to initialize SuperSimpleQueue",
|
||||
service: SERVICE_NAME,
|
||||
@@ -131,17 +124,30 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
|
||||
// Add geo check job if enabled for HTTP monitors
|
||||
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
||||
this.scheduler.addJob({
|
||||
id: `${monitorId}-geo`,
|
||||
template: "geo-check-job",
|
||||
repeat: monitor.geoCheckInterval,
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
deleteJob = async (monitor: any) => {
|
||||
this.scheduler.removeJob(monitor.id);
|
||||
this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||
};
|
||||
|
||||
pauseJob = async (monitor: any) => {
|
||||
const result = await this.scheduler.pauseJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
throw new Error("Failed to pause monitor");
|
||||
}
|
||||
await this.scheduler.pauseJob(`${monitor.id}-geo`);
|
||||
this.logger.debug({
|
||||
message: `Paused monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
@@ -154,6 +160,9 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
|
||||
await this.scheduler.resumeJob(`${monitor.id}-geo`);
|
||||
|
||||
this.logger.debug({
|
||||
message: `Resumed monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
@@ -163,6 +172,29 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
|
||||
updateJob = async (monitor: any) => {
|
||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||
|
||||
// Handle geo check job lifecycle
|
||||
const geoJobId = `${monitor.id}-geo`;
|
||||
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
||||
// Check if geo job exists
|
||||
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
||||
if (existingGeoJob) {
|
||||
// Update existing geo job
|
||||
this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, active: monitor.isActive, data: monitor });
|
||||
} else {
|
||||
// Create new geo job
|
||||
this.scheduler.addJob({
|
||||
id: geoJobId,
|
||||
template: "geo-check-job",
|
||||
repeat: monitor.geoCheckInterval,
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Remove geo job if disabled or monitor type changed
|
||||
this.scheduler.removeJob(geoJobId);
|
||||
}
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AppError } from "@/utils/AppError.js";
|
||||
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
|
||||
import type { StatusChangeResult, MonitorStatusResponse, HardwareStatusPayload, MonitorStatus } from "@/types/index.js";
|
||||
import IncidentService from "@/service/business/incidentService.js";
|
||||
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
||||
import {
|
||||
IMaintenanceWindowsRepository,
|
||||
IMonitorsRepository,
|
||||
@@ -11,7 +12,18 @@ import {
|
||||
IMonitorStatsRepository,
|
||||
IChecksRepository,
|
||||
IIncidentsRepository,
|
||||
IGeoChecksRepository,
|
||||
} from "@/repositories/index.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import { IBufferService } from "../bufferService.js";
|
||||
|
||||
export interface ISuperSimpleQueueHelper {
|
||||
readonly serviceName: string;
|
||||
getMonitorJob(): (monitor: Monitor) => Promise<void>;
|
||||
getCleanupOrphanedJob(): () => Promise<void>;
|
||||
getGeoCheckJob(): (monitor: Monitor) => Promise<void>;
|
||||
isInMaintenanceWindow(monitorId: string, teamId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface MonitorActionDecision {
|
||||
shouldCreateIncident: boolean;
|
||||
@@ -27,7 +39,7 @@ export interface MonitorActionDecision {
|
||||
};
|
||||
}
|
||||
|
||||
class SuperSimpleQueueHelper {
|
||||
class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: any;
|
||||
@@ -35,7 +47,7 @@ class SuperSimpleQueueHelper {
|
||||
private statusService: IStatusService;
|
||||
private notificationsService: INotificationsService;
|
||||
private checkService: any;
|
||||
private buffer: any;
|
||||
private buffer: IBufferService;
|
||||
private incidentService: IncidentService;
|
||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
@@ -43,36 +55,26 @@ class SuperSimpleQueueHelper {
|
||||
private monitorStatsRepository: IMonitorStatsRepository;
|
||||
private checksRepository: IChecksRepository;
|
||||
private incidentsRepository: IIncidentsRepository;
|
||||
private geoChecksService: IGeoChecksService;
|
||||
private geoChecksRepository: IGeoChecksRepository;
|
||||
|
||||
constructor({
|
||||
logger,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationsService,
|
||||
checkService,
|
||||
buffer,
|
||||
incidentService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
teamsRepository,
|
||||
monitorStatsRepository,
|
||||
checksRepository,
|
||||
incidentsRepository,
|
||||
}: {
|
||||
logger: any;
|
||||
networkService: INetworkService;
|
||||
statusService: IStatusService;
|
||||
notificationsService: INotificationsService;
|
||||
checkService: any;
|
||||
buffer: any;
|
||||
incidentService: IncidentService;
|
||||
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
teamsRepository: ITeamsRepository;
|
||||
monitorStatsRepository: IMonitorStatsRepository;
|
||||
checksRepository: IChecksRepository;
|
||||
incidentsRepository: IIncidentsRepository;
|
||||
}) {
|
||||
constructor(
|
||||
logger: ILogger,
|
||||
networkService: INetworkService,
|
||||
statusService: IStatusService,
|
||||
notificationsService: INotificationsService,
|
||||
checkService: any,
|
||||
buffer: IBufferService,
|
||||
incidentService: IncidentService,
|
||||
maintenanceWindowsRepository: IMaintenanceWindowsRepository,
|
||||
monitorsRepository: IMonitorsRepository,
|
||||
teamsRepository: ITeamsRepository,
|
||||
monitorStatsRepository: IMonitorStatsRepository,
|
||||
checksRepository: IChecksRepository,
|
||||
incidentsRepository: IIncidentsRepository,
|
||||
geoChecksService: IGeoChecksService,
|
||||
geoChecksRepository: IGeoChecksRepository
|
||||
) {
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
@@ -86,6 +88,8 @@ class SuperSimpleQueueHelper {
|
||||
this.monitorStatsRepository = monitorStatsRepository;
|
||||
this.checksRepository = checksRepository;
|
||||
this.incidentsRepository = incidentsRepository;
|
||||
this.geoChecksService = geoChecksService;
|
||||
this.geoChecksRepository = geoChecksRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -240,6 +244,16 @@ class SuperSimpleQueueHelper {
|
||||
});
|
||||
}
|
||||
|
||||
// Remove orphaned geo checks
|
||||
const deletedGeoChecksCount = await this.geoChecksRepository.deleteByMonitorIdsNotIn(allMonitorIds);
|
||||
if (deletedGeoChecksCount > 0) {
|
||||
this.logger.info({
|
||||
message: `Deleted ${deletedGeoChecksCount} orphaned geo checks`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getCleanupOrphanedJob",
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.info({
|
||||
message: "Cleanup of orphaned data completed",
|
||||
service: SERVICE_NAME,
|
||||
@@ -257,6 +271,81 @@ class SuperSimpleQueueHelper {
|
||||
};
|
||||
};
|
||||
|
||||
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: 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}`,
|
||||
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
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
async isInMaintenanceWindow(monitorId: string, teamId: string) {
|
||||
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
|
||||
// Check for active maintenance window:
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import type { Check } from "@/types/index.js";
|
||||
|
||||
import type { GeoCheck } from "@/types/index.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 {
|
||||
addToBuffer(check: Check): void;
|
||||
addGeoCheckToBuffer(geoCheck: GeoCheck): void;
|
||||
removeCheckFromBuffer(check: Check): boolean;
|
||||
scheduleNextFlush(): void;
|
||||
flushBuffer(): Promise<void>;
|
||||
flushGeoBuffer(): Promise<void>;
|
||||
}
|
||||
|
||||
class BufferService implements IBufferService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private BUFFER_TIMEOUT: number;
|
||||
private logger: any;
|
||||
private logger: ILogger;
|
||||
private SERVICE_NAME: string;
|
||||
private buffer: any[];
|
||||
private geoBuffer: any[];
|
||||
private bufferTimer: NodeJS.Timeout | null = null;
|
||||
private checksService: any;
|
||||
private geoChecksService: IGeoChecksService;
|
||||
|
||||
constructor({ logger, checkService, settingsService }: { logger: any; checkService: 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;
|
||||
this.geoChecksService = geoChecksService;
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.buffer = [];
|
||||
this.geoBuffer = [];
|
||||
this.scheduleNextFlush();
|
||||
this.logger.info({
|
||||
message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`,
|
||||
@@ -49,6 +58,19 @@ class BufferService implements IBufferService {
|
||||
}
|
||||
}
|
||||
|
||||
addGeoCheckToBuffer(geoCheck: GeoCheck) {
|
||||
try {
|
||||
this.geoBuffer.push(geoCheck);
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "addGeoCheckToBuffer",
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeCheckFromBuffer(checkToRemove: Check) {
|
||||
try {
|
||||
if (!checkToRemove) {
|
||||
@@ -94,6 +116,7 @@ class BufferService implements IBufferService {
|
||||
this.bufferTimer = setTimeout(async () => {
|
||||
try {
|
||||
await this.flushBuffer();
|
||||
await this.flushGeoBuffer();
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error in flush cycle: ${error.message}`,
|
||||
@@ -110,18 +133,47 @@ class BufferService implements IBufferService {
|
||||
async flushBuffer() {
|
||||
try {
|
||||
if (this.buffer.length > 0) {
|
||||
this.logger.debug({
|
||||
message: `Flushing ${this.buffer.length} checks to database`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffer",
|
||||
});
|
||||
await this.checksService.createChecks(this.buffer);
|
||||
this.buffer = [];
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
message: `Error flushing checks buffer: ${error.message}`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushBuffer",
|
||||
stack: error.stack,
|
||||
});
|
||||
// Clear buffer even on error to prevent infinite retry loops
|
||||
this.buffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
this.buffer = [];
|
||||
async flushGeoBuffer() {
|
||||
try {
|
||||
if (this.geoBuffer.length > 0) {
|
||||
this.logger.debug({
|
||||
message: `Flushing ${this.geoBuffer.length} geo checks to database`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushGeoBuffer",
|
||||
});
|
||||
await this.geoChecksService.createGeoChecks(this.geoBuffer);
|
||||
this.geoBuffer = [];
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error flushing geo checks buffer: ${error.message}`,
|
||||
service: this.SERVICE_NAME,
|
||||
method: "flushGeoBuffer",
|
||||
stack: error.stack,
|
||||
});
|
||||
// Clear buffer even on error to prevent infinite retry loops
|
||||
this.geoBuffer = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
|
||||
import type { ILogger } from "@/utils/logger.js";
|
||||
import got from "got";
|
||||
|
||||
const SERVICE_NAME = "GlobalPingService";
|
||||
const GLOBAL_PING_API_BASE = "https://api.globalping.io/v1";
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
const MAX_POLL_TIMEOUT_MS = 30000;
|
||||
|
||||
interface GlobalPingMeasurementRequest {
|
||||
type: "http";
|
||||
target: string;
|
||||
locations: Array<{ continent: GeoContinent }>;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
interface GlobalPingMeasurementResponse {
|
||||
id: string;
|
||||
type: string;
|
||||
status: "in-progress" | "finished" | "failed";
|
||||
probesCount: number;
|
||||
results?: GlobalPingProbeResult[];
|
||||
}
|
||||
|
||||
interface GlobalPingProbeResult {
|
||||
probe: {
|
||||
continent: GeoContinent;
|
||||
region: string;
|
||||
country: string;
|
||||
state: string | null;
|
||||
city: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
};
|
||||
result: {
|
||||
status: "finished" | "failed" | "timeout";
|
||||
statusCode?: number;
|
||||
statusCodeName?: string;
|
||||
timings?: {
|
||||
total: number;
|
||||
dns: number;
|
||||
tcp: number;
|
||||
tls: number;
|
||||
firstByte: number;
|
||||
download: number;
|
||||
};
|
||||
rawOutput?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IGlobalPingService {
|
||||
readonly serviceName: string;
|
||||
createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null>;
|
||||
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
|
||||
}
|
||||
|
||||
class GlobalPingService implements IGlobalPingService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: ILogger;
|
||||
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return GlobalPingService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null> {
|
||||
try {
|
||||
// GlobalPing API expects target without protocol (http:// or https://)
|
||||
const cleanTarget = url.replace(/^https?:\/\//, "");
|
||||
|
||||
const requestBody: GlobalPingMeasurementRequest = {
|
||||
type: "http",
|
||||
target: cleanTarget,
|
||||
locations: locations.map((continent) => ({ continent })),
|
||||
limit: locations.length,
|
||||
};
|
||||
|
||||
const response = await got.post<GlobalPingMeasurementResponse>(`${GLOBAL_PING_API_BASE}/measurements`, {
|
||||
json: requestBody,
|
||||
responseType: "json",
|
||||
timeout: { request: 10000 },
|
||||
});
|
||||
|
||||
const measurementId = response.body.id;
|
||||
|
||||
this.logger.debug({
|
||||
message: `Created GlobalPing measurement: ${measurementId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createMeasurement",
|
||||
details: { measurementId, url, locations },
|
||||
});
|
||||
|
||||
return measurementId;
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: "GlobalPing API unavailable, skipping geo check",
|
||||
service: SERVICE_NAME,
|
||||
method: "createMeasurement",
|
||||
details: error,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async pollForResults(measurementId: string, timeoutMs: number = MAX_POLL_TIMEOUT_MS): Promise<GeoCheckResult[]> {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < timeoutMs) {
|
||||
try {
|
||||
const response = await got.get<GlobalPingMeasurementResponse>(`${GLOBAL_PING_API_BASE}/measurements/${measurementId}`, {
|
||||
responseType: "json",
|
||||
timeout: { request: 5000 },
|
||||
});
|
||||
|
||||
const measurement = response.body;
|
||||
|
||||
if (measurement.status === "finished") {
|
||||
const results = this.transformResults(measurement.results || []);
|
||||
this.logger.debug({
|
||||
message: `GlobalPing measurement completed: ${measurementId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "pollForResults",
|
||||
details: { measurementId, resultsCount: results.length },
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
if (measurement.status === "failed") {
|
||||
this.logger.warn({
|
||||
message: `GlobalPing measurement failed: ${measurementId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "pollForResults",
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
// Still in-progress, wait and poll again
|
||||
await this.sleep(POLL_INTERVAL_MS);
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: "Error polling GlobalPing API",
|
||||
service: SERVICE_NAME,
|
||||
method: "pollForResults",
|
||||
details: error.message,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout reached
|
||||
this.logger.warn({
|
||||
message: `GlobalPing measurement polling timeout: ${measurementId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "pollForResults",
|
||||
details: { measurementId, timeoutMs },
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
private transformResults(probeResults: GlobalPingProbeResult[]): GeoCheckResult[] {
|
||||
const successfulResults: GeoCheckResult[] = [];
|
||||
|
||||
for (const probeResult of probeResults) {
|
||||
// Skip failed or timeout results
|
||||
if (probeResult.result.status !== "finished" || !probeResult.result.statusCode || !probeResult.result.timings) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const location: GeoCheckLocation = {
|
||||
continent: probeResult.probe.continent,
|
||||
region: probeResult.probe.region,
|
||||
country: probeResult.probe.country,
|
||||
state: probeResult.probe.state || "",
|
||||
city: probeResult.probe.city,
|
||||
longitude: probeResult.probe.longitude,
|
||||
latitude: probeResult.probe.latitude,
|
||||
};
|
||||
|
||||
const timings: GeoCheckTimings = {
|
||||
total: probeResult.result.timings.total,
|
||||
dns: probeResult.result.timings.dns,
|
||||
tcp: probeResult.result.timings.tcp,
|
||||
tls: probeResult.result.timings.tls,
|
||||
firstByte: probeResult.result.timings.firstByte,
|
||||
download: probeResult.result.timings.download,
|
||||
};
|
||||
|
||||
const result: GeoCheckResult = {
|
||||
location,
|
||||
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
||||
statusCode: probeResult.result.statusCode,
|
||||
timings,
|
||||
};
|
||||
|
||||
successfulResults.push(result);
|
||||
}
|
||||
|
||||
return successfulResults;
|
||||
}
|
||||
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
export default GlobalPingService;
|
||||
@@ -1,11 +1,14 @@
|
||||
import { HTTPError, RequestError } from "got";
|
||||
import type { Got, Response } from "got";
|
||||
import type { Monitor, MonitorStatusResponse, GrpcStatusPayload } from "@/types/index.js";
|
||||
import type { AxiosStatic } from "axios";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import CacheableLookup from "cacheable-lookup";
|
||||
import { ISettingsService } from "../system/settingsService.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
const SERVICE_NAME = "NetworkService";
|
||||
|
||||
type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
|
||||
@@ -50,16 +53,16 @@ class NetworkService implements INetworkService {
|
||||
private NETWORK_ERROR: number;
|
||||
private PING_ERROR: number;
|
||||
|
||||
private axios: any;
|
||||
private axios: AxiosStatic;
|
||||
private got: Got;
|
||||
private https: any;
|
||||
private jmespath: any;
|
||||
private GameDig: any;
|
||||
private ping: any;
|
||||
private logger: any;
|
||||
private logger: ILogger;
|
||||
private Docker: any;
|
||||
private net: any;
|
||||
private settingsService: any;
|
||||
private settingsService: ISettingsService;
|
||||
private grpc: any;
|
||||
private protoLoader: any;
|
||||
|
||||
@@ -117,35 +120,20 @@ class NetworkService implements INetworkService {
|
||||
};
|
||||
};
|
||||
|
||||
constructor({
|
||||
axios,
|
||||
got,
|
||||
https,
|
||||
jmespath,
|
||||
GameDig,
|
||||
ping,
|
||||
logger,
|
||||
http,
|
||||
Docker,
|
||||
net,
|
||||
settingsService,
|
||||
grpc,
|
||||
protoLoader,
|
||||
}: {
|
||||
axios: any;
|
||||
got: Got;
|
||||
https: any;
|
||||
jmespath: any;
|
||||
GameDig: any;
|
||||
ping: any;
|
||||
logger: any;
|
||||
http: any;
|
||||
Docker: any;
|
||||
net: any;
|
||||
settingsService: any;
|
||||
grpc: any;
|
||||
protoLoader: any;
|
||||
}) {
|
||||
constructor(
|
||||
axios: AxiosStatic,
|
||||
got: Got,
|
||||
https: any,
|
||||
jmespath: any,
|
||||
GameDig: any,
|
||||
ping: any,
|
||||
logger: ILogger,
|
||||
Docker: any,
|
||||
net: any,
|
||||
settingsService: ISettingsService,
|
||||
grpc: any,
|
||||
protoLoader: any
|
||||
) {
|
||||
this.TYPE_PING = "ping";
|
||||
this.TYPE_HTTP = "http";
|
||||
this.TYPE_PAGESPEED = "pagespeed";
|
||||
@@ -449,7 +437,6 @@ class NetworkService implements INetworkService {
|
||||
|
||||
const docker = new this.Docker({
|
||||
socketPath: "/var/run/docker.sock",
|
||||
handleError: true, // Enable error handling
|
||||
});
|
||||
|
||||
const dockerResponse = this.buildStatusResponse({
|
||||
@@ -545,6 +532,10 @@ class NetworkService implements INetworkService {
|
||||
private async requestPort(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
try {
|
||||
const { url, port } = monitor;
|
||||
if (!port) {
|
||||
throw new Error("Port is required for port monitor");
|
||||
}
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest(async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = this.net.createConnection(
|
||||
@@ -611,9 +602,9 @@ class NetworkService implements INetworkService {
|
||||
});
|
||||
|
||||
const state = await this.GameDig.query({
|
||||
type: gameId,
|
||||
type: gameId ?? "unknown",
|
||||
host: url,
|
||||
port: port,
|
||||
port: port ?? 0,
|
||||
}).catch((error: any) => {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { MonitorType } from "@/types/index.js";
|
||||
|
||||
export const GeoContinents = ["EU", "NA", "AS", "SA", "AF", "OC"] as const;
|
||||
export type GeoContinent = (typeof GeoContinents)[number];
|
||||
|
||||
export interface GeoCheckMetadata {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
type: MonitorType;
|
||||
}
|
||||
|
||||
export interface GeoCheckTimings {
|
||||
total: number;
|
||||
dns: number;
|
||||
tcp: number;
|
||||
tls: number;
|
||||
firstByte: number;
|
||||
download: number;
|
||||
}
|
||||
|
||||
export interface GeoCheckLocation {
|
||||
continent: GeoContinent;
|
||||
region: string;
|
||||
country: string;
|
||||
state: string;
|
||||
city: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
}
|
||||
|
||||
export interface GeoCheckResult {
|
||||
location: GeoCheckLocation;
|
||||
status: boolean;
|
||||
statusCode: number;
|
||||
timings: GeoCheckTimings;
|
||||
}
|
||||
|
||||
export interface GeoCheck {
|
||||
id: string;
|
||||
metadata: GeoCheckMetadata;
|
||||
results: GeoCheckResult[];
|
||||
expiry: string;
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface FlatGeoCheck {
|
||||
id: string;
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
type: string;
|
||||
location: GeoCheckLocation;
|
||||
status: boolean;
|
||||
statusCode: number;
|
||||
timings: GeoCheckTimings;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface GroupedGeoCheck {
|
||||
bucketDate: string;
|
||||
continent: GeoContinent;
|
||||
avgResponseTime: number;
|
||||
totalChecks: number;
|
||||
uptimePercentage: number;
|
||||
}
|
||||
|
||||
export interface GeoChecksResult {
|
||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
|
||||
groupedGeoChecks: GroupedGeoCheck[];
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "@/types/check.js";
|
||||
export * from "@/types/geoCheck.js";
|
||||
export * from "@/types/monitor.js";
|
||||
export * from "@/types/monitorStats.js";
|
||||
export * from "@/types/statusPage.js";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { CheckSnapshot } from "@/types/check.js";
|
||||
export type { CheckSnapshot } from "@/types/check.js";
|
||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||
export type { GeoContinent } from "@/types/geoCheck.js";
|
||||
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
@@ -44,6 +46,9 @@ export interface Monitor {
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
group: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
geoCheckInterval?: number;
|
||||
recentChecks: CheckSnapshot[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import type { GroupedCheck, NormalizedCheck, NormalizedUptimeCheck, HasResponseTime } from "@/types/index.js";
|
||||
|
||||
export const getDateForRange = (dateRange: string): Date | undefined => {
|
||||
const now = Date.now();
|
||||
switch (dateRange) {
|
||||
case "recent":
|
||||
return new Date(now - 2 * 60 * 60 * 1000); // 2 hours
|
||||
case "hour":
|
||||
return new Date(now - 60 * 60 * 1000); // 1 hour
|
||||
case "day":
|
||||
return new Date(now - 24 * 60 * 60 * 1000); // 1 day
|
||||
case "week":
|
||||
return new Date(now - 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||
case "month":
|
||||
return new Date(now - 30 * 24 * 60 * 60 * 1000); // 30 days
|
||||
case "all":
|
||||
return undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const calculatePercentile = <T extends HasResponseTime>(arr: T[], percentile: number): number => {
|
||||
const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime);
|
||||
const index = (percentile / 100) * (sorted.length - 1);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import joi, { type CustomHelpers } from "joi";
|
||||
import { type UserRole, UserRoles } from "@/types/user.js";
|
||||
import { GeoContinents } from "@/types/geoCheck.js";
|
||||
|
||||
//****************************************
|
||||
// Custom Validators
|
||||
@@ -112,6 +113,7 @@ const getMonitorByIdQueryValidation = joi.object({
|
||||
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
|
||||
numToDisplay: joi.number(),
|
||||
normalize: joi.boolean(),
|
||||
continent: joi.string().valid(...GeoContinents),
|
||||
});
|
||||
|
||||
const getMonitorsByTeamIdParamValidation = joi.object({});
|
||||
@@ -173,6 +175,12 @@ const createMonitorBodyValidation = joi.object({
|
||||
grpcServiceName: joi.string().allow("").default(""),
|
||||
selectedDisks: joi.array().items(joi.string()).optional(),
|
||||
group: joi.string().max(50).trim().allow(null, "").optional(),
|
||||
geoCheckEnabled: joi.boolean().optional(),
|
||||
geoCheckLocations: joi
|
||||
.array()
|
||||
.items(joi.string().valid(...GeoContinents))
|
||||
.optional(),
|
||||
geoCheckInterval: joi.number().min(300000).optional(),
|
||||
});
|
||||
|
||||
const createMonitorsBodyValidation = joi.array().items(
|
||||
@@ -205,6 +213,12 @@ const editMonitorBodyValidation = joi
|
||||
grpcServiceName: joi.string().allow(""),
|
||||
selectedDisks: joi.array().items(joi.string()).optional(),
|
||||
group: joi.string().max(50).trim().allow(null, "").optional(),
|
||||
geoCheckEnabled: joi.boolean().optional(),
|
||||
geoCheckLocations: joi
|
||||
.array()
|
||||
.items(joi.string().valid(...GeoContinents))
|
||||
.optional(),
|
||||
geoCheckInterval: joi.number().min(300000).optional(),
|
||||
})
|
||||
.options({ stripUnknown: true });
|
||||
|
||||
@@ -312,6 +326,7 @@ const getChecksQueryValidation = joi.object({
|
||||
page: joi.number(),
|
||||
rowsPerPage: joi.number(),
|
||||
status: joi.boolean(),
|
||||
continent: joi.alternatives().try(joi.string().valid(...GeoContinents), joi.array().items(joi.string().valid(...GeoContinents))),
|
||||
});
|
||||
|
||||
const getTeamChecksQueryValidation = joi.object({
|
||||
|
||||
Reference in New Issue
Block a user