initial commit

This commit is contained in:
Alex Holliday committed 2026-02-23 17:48:53 +00:00
1 parent 2d023de568
commit 42fdc47d86
14 files changed
+530

No files matched your search

+57
View File
@@ -0,0 +1,57 @@
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: string;
}
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 GroupedGeoCheck {
bucketDate: string;
continent: GeoContinent;
avgResponseTime: number;
totalChecks: number;
uptimePercentage: number;
}
export interface GeoChecksResult {
monitorType: string;
groupedGeoChecks: GroupedGeoCheck[];
}
+5
View File
@@ -1,5 +1,7 @@
import type { GroupedCheck, CheckSnapshot } from "@/Types/Check";
import type { PageSpeedGroupedCheck } from "@/Types/Check";
import type { GeoContinent } from "@/Types/GeoCheck";
export type { GeoContinent } from "@/Types/GeoCheck";
export const MonitorTypes = [
"http",
@@ -59,6 +61,9 @@ export interface Monitor {
selectedDisks: string[];
gameId?: string;
group: string | null;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[];
geoCheckInterval?: number;
recentChecks: CheckSnapshot[];
createdAt: string;
updatedAt: string;
+7
View File
@@ -1,4 +1,5 @@
import { z } from "zod";
import { GeoContinents } from "@/Types/GeoCheck";
// URL schema with custom error message
const urlSchema = z.url({ message: "Please enter a valid URL" });
@@ -20,6 +21,12 @@ const baseSchema = z.object({
.number({ message: "Threshold percentage is required" })
.min(1, "Incident percentage must be at least 1")
.max(100, "Incident percentage must be at most 100"),
geoCheckEnabled: z.boolean().optional(),
geoCheckLocations: z.array(z.enum(GeoContinents)).optional(),
geoCheckInterval: z
.number()
.min(300000, "Interval must be at least 5 minutes")
.optional(),
});
// HTTP monitor schema
+5
View File
@@ -46,6 +46,7 @@ import jmespath from "jmespath";
import {
MongoMonitorsRepository,
MongoChecksRepository,
MongoGeoChecksRepository,
MongoMonitorStatsRepository,
MongoStatusPagesRepository,
MongoUsersRepository,
@@ -57,6 +58,7 @@ import {
MongoMaintenanceWindowsRepository,
IMonitorsRepository,
IChecksRepository,
IGeoChecksRepository,
IMonitorStatsRepository,
IStatusPagesRepository,
IUsersRepository,
@@ -94,6 +96,7 @@ export type InitializedServices = {
// Repositories
monitorsRepository: IMonitorsRepository;
checksRepository: IChecksRepository;
geoChecksRepository: IGeoChecksRepository;
monitorStatsRepository: IMonitorStatsRepository;
statusPagesRepository: IStatusPagesRepository;
usersRepository: IUsersRepository;
@@ -126,6 +129,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();
@@ -278,6 +282,7 @@ export const initializeServices = async ({
// Repositories
monitorsRepository,
checksRepository,
geoChecksRepository,
monitorStatsRepository,
statusPagesRepository,
usersRepository,
+118
View File
@@ -0,0 +1,118 @@
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.monitorId": 1, "results.location.continent": 1, createdAt: -1 });
GeoCheckSchema.index({ "metadata.teamId": 1, createdAt: -1 });
GeoCheckSchema.index({ "metadata.teamId": 1, "results.status": 1, createdAt: -1 });
GeoCheckSchema.index({ createdAt: 1 });
const GeoCheckModel = model<GeoCheckDocument>("GeoCheck", GeoCheckSchema);
export type { GeoCheckMetadataDocument };
export { GeoCheckModel };
export default GeoCheckModel;
+12
View File
@@ -169,6 +169,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: [],
+3
View File
@@ -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";
@@ -0,0 +1,9 @@
import type { GeoCheck, GroupedGeoCheck } from "@/types/geoCheck.js";
export interface IGeoChecksRepository {
create(geoCheck: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">): Promise<GeoCheck>;
findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise<GeoCheck[]>;
findGroupedByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date, dateFormat: string): Promise<GroupedGeoCheck[]>;
deleteByMonitorId(monitorId: string): Promise<number>;
deleteByTeamId(teamId: string): Promise<number>;
}
@@ -0,0 +1,233 @@
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck } from "@/types/geoCheck.js";
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
import mongoose from "mongoose";
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),
};
};
create = async (geoCheck: Omit<GeoCheck, "id" | "__v" | "createdAt" | "updatedAt">): Promise<GeoCheck> => {
try {
const doc = await GeoCheckModel.create({
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 this.toEntity(doc);
} catch (error: any) {
this.logger.error({
message: `Error creating geo check: ${error.message}`,
service: SERVICE_NAME,
method: "create",
});
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): 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",
},
// 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;
}
};
}
export { MongoGeoChecksRepository };
export default MongoGeoChecksRepository;
+3
View File
@@ -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";
+59
View File
@@ -0,0 +1,59 @@
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 GroupedGeoCheck {
bucketDate: string;
continent: GeoContinent;
avgResponseTime: number;
totalChecks: number;
uptimePercentage: number;
}
export interface GeoChecksResult {
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
groupedGeoChecks: GroupedGeoCheck[];
}
+1
View File
@@ -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";
+5
View File
@@ -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", "unknown"] as const;
export type MonitorType = (typeof MonitorTypes)[number];
@@ -43,6 +45,9 @@ export interface Monitor {
selectedDisks: string[];
gameId?: string;
group: string | null;
geoCheckEnabled?: boolean;
geoCheckLocations?: GeoContinent[];
geoCheckInterval?: number;
recentChecks: CheckSnapshot[];
createdAt: string;
updatedAt: string;
+13
View File
@@ -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
@@ -172,6 +173,12 @@ const createMonitorBodyValidation = joi.object({
gameId: 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(),
});
const createMonitorsBodyValidation = joi.array().items(
@@ -203,6 +210,12 @@ const editMonitorBodyValidation = joi
gameId: 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 });