Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35c022df0f | ||
|
|
19fc85ae2d | ||
|
|
b4f8fa7d72 | ||
|
|
ef5629d8e4 | ||
|
|
4018125224 |
No files matched your search
@@ -14,6 +14,9 @@ const getBaseDefaults = (data?: Monitor | null) => ({
|
||||
notifications: data?.notifications || [],
|
||||
statusWindowSize: data?.statusWindowSize || 5,
|
||||
statusWindowThreshold: data?.statusWindowThreshold || 60,
|
||||
geoCheckEnabled: data?.geoCheckEnabled || false,
|
||||
geoCheckLocations: data?.geoCheckLocations || [],
|
||||
geoCheckInterval: data?.geoCheckInterval || 300000,
|
||||
});
|
||||
|
||||
export const useMonitorForm = ({
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
||||
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
||||
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
|
||||
import { GeoCheckLocations } from "@/Types/Monitor";
|
||||
import type { Notification } from "@/Types/Notification";
|
||||
import type { MonitorFormData } from "@/Validation/monitor";
|
||||
|
||||
@@ -165,6 +166,7 @@ const CreateMonitorPage = () => {
|
||||
const watchedType = watch("type") as MonitorType;
|
||||
|
||||
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
|
||||
const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean;
|
||||
|
||||
useEffect(() => {
|
||||
clearErrors();
|
||||
@@ -823,6 +825,89 @@ const CreateMonitorPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Geo-distributed Checks Configuration */}
|
||||
{watchedType === "http" && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.geoChecks.title")}
|
||||
subtitle={t("pages.createMonitor.form.geoChecks.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="geoCheckEnabled"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={theme.spacing(2)}
|
||||
>
|
||||
<Switch
|
||||
checked={field.value ?? false}
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
/>
|
||||
<Typography>
|
||||
{t("pages.createMonitor.form.geoChecks.option.enabled.label")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
/>
|
||||
{watchGeoCheckEnabled && (
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="geoCheckLocations"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
const locationOptions = GeoCheckLocations.map((loc) => ({
|
||||
id: loc,
|
||||
name: t(
|
||||
`pages.createMonitor.form.geoChecks.option.locations.options.${loc}`
|
||||
),
|
||||
}));
|
||||
const selectedLocations = locationOptions.filter((loc) =>
|
||||
(field.value ?? []).includes(loc.id)
|
||||
);
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple={true}
|
||||
options={locationOptions}
|
||||
value={selectedLocations}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(_, newValue: typeof locationOptions) => {
|
||||
field.onChange(newValue.map((loc) => loc.id));
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => option.id === value.id}
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.geoChecks.option.locations.label"
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Controller
|
||||
name="geoCheckInterval"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={field.value ?? 300000}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.geoChecks.option.interval.label"
|
||||
)}
|
||||
>
|
||||
<MenuItem value={300000}>5 min</MenuItem>
|
||||
<MenuItem value={600000}>10 min</MenuItem>
|
||||
<MenuItem value={900000}>15 min</MenuItem>
|
||||
<MenuItem value={1800000}>30 min</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
|
||||
@@ -160,6 +160,14 @@ export interface PageSpeedGroupedCheck {
|
||||
totalChecks: number;
|
||||
}
|
||||
|
||||
export interface GroupedGeoCheck {
|
||||
bucketDate: string;
|
||||
continent: string;
|
||||
avgResponseTime: number;
|
||||
totalChecks: number;
|
||||
uptimePercentage: number;
|
||||
}
|
||||
|
||||
export interface LatestCheck {
|
||||
status: boolean;
|
||||
responseTime: number;
|
||||
@@ -236,6 +244,7 @@ export interface UptimeChecksResult {
|
||||
groupedDownChecks: GroupedCheck[];
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
groupedGeoChecks?: GroupedGeoCheck[];
|
||||
}
|
||||
|
||||
export interface ChecksSummary {
|
||||
|
||||
@@ -25,6 +25,9 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
||||
|
||||
export const GeoCheckLocations = ["EU", "NA", "AS", "SA", "AF", "OC"] as const;
|
||||
export type GeoCheckLocation = (typeof GeoCheckLocations)[number];
|
||||
|
||||
export interface Monitor {
|
||||
id: string;
|
||||
userId: string;
|
||||
@@ -59,6 +62,9 @@ export interface Monitor {
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
group: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoCheckLocation[];
|
||||
geoCheckInterval?: number;
|
||||
recentChecks: CheckSnapshot[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -20,6 +20,9 @@ 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(["EU", "NA", "AS", "SA", "AF", "OC"])).optional(),
|
||||
geoCheckInterval: z.number().min(5, "Interval must be at least 5 minutes").optional(),
|
||||
});
|
||||
|
||||
// HTTP monitor schema
|
||||
|
||||
@@ -431,6 +431,31 @@
|
||||
},
|
||||
"title": "Advanced settings"
|
||||
},
|
||||
"geoChecks": {
|
||||
"title": "Geo-distributed checks",
|
||||
"description": "Run checks from multiple geographic locations to monitor global availability and performance.",
|
||||
"option": {
|
||||
"enabled": {
|
||||
"label": "Enable geo-distributed checks"
|
||||
},
|
||||
"locations": {
|
||||
"label": "Locations",
|
||||
"placeholder": "Select locations",
|
||||
"options": {
|
||||
"EU": "Europe",
|
||||
"NA": "North America",
|
||||
"AS": "Asia",
|
||||
"SA": "South America",
|
||||
"AF": "Africa",
|
||||
"OC": "Oceania"
|
||||
}
|
||||
},
|
||||
"interval": {
|
||||
"label": "Check interval (minutes)",
|
||||
"helper": "Minimum 5 minutes between geo checks"
|
||||
}
|
||||
}
|
||||
},
|
||||
"frequency": {
|
||||
"description": "How often do you want to check the status of this monitor?",
|
||||
"option": {
|
||||
|
||||
@@ -11,6 +11,8 @@ import type {
|
||||
CheckMemoryInfo,
|
||||
CheckMetadata,
|
||||
CheckNetworkInterfaceInfo,
|
||||
GeoCheckResult,
|
||||
GeoCheckTimings,
|
||||
GotTimings,
|
||||
ILighthouseAudit,
|
||||
} from "@/types/check.js";
|
||||
@@ -175,6 +177,34 @@ const auditsSchema = new Schema<CheckAudits>(
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const geoCheckTimingsSchema = new Schema<GeoCheckTimings>(
|
||||
{
|
||||
total: { type: Number, required: true },
|
||||
dns: { type: Number },
|
||||
tcp: { type: Number },
|
||||
tls: { type: Number },
|
||||
firstByte: { type: Number },
|
||||
download: { type: Number },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const geoCheckResultSchema = new Schema<GeoCheckResult>(
|
||||
{
|
||||
continent: { type: String, required: true },
|
||||
region: { type: String },
|
||||
country: { type: String, required: true },
|
||||
state: { type: String },
|
||||
city: { type: String, required: true },
|
||||
longitude: { type: Number, required: true },
|
||||
latitude: { type: Number, required: true },
|
||||
status: { type: Boolean, required: true },
|
||||
statusCode: { type: Number, required: true },
|
||||
timings: { type: geoCheckTimingsSchema, required: true },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const metadataSchema = new Schema<CheckMetadataDocument>(
|
||||
{
|
||||
monitorId: {
|
||||
@@ -281,6 +311,10 @@ const CheckSchema = new Schema<CheckDocument>(
|
||||
type: auditsSchema,
|
||||
default: undefined,
|
||||
},
|
||||
geoResults: {
|
||||
type: [geoCheckResultSchema],
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Schema, model, Types, type UpdateQuery } from "mongoose";
|
||||
import { Schema, model, Types } from "mongoose";
|
||||
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
|
||||
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
|
||||
import { MonitorTypes, MonitorStatuses, GeoCheckLocations } from "@/types/monitor.js";
|
||||
|
||||
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
||||
|
||||
@@ -169,6 +169,19 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
return value && value.trim() ? value.trim() : null;
|
||||
},
|
||||
},
|
||||
geoCheckEnabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
geoCheckLocations: {
|
||||
type: [String],
|
||||
enum: GeoCheckLocations,
|
||||
default: [],
|
||||
},
|
||||
geoCheckInterval: {
|
||||
type: Number,
|
||||
default: 300000,
|
||||
},
|
||||
recentChecks: {
|
||||
type: [checkSnapshotSchema],
|
||||
default: [],
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
CheckMemoryInfo,
|
||||
CheckMetadata,
|
||||
CheckNetworkInterfaceInfo,
|
||||
GeoCheckResult,
|
||||
GeoCheckTimings,
|
||||
GotTimings,
|
||||
MonitorType,
|
||||
} from "@/types/index.js";
|
||||
@@ -165,6 +167,33 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
};
|
||||
};
|
||||
|
||||
const mapGeoTimings = (timings?: GeoCheckTimings): GeoCheckTimings => ({
|
||||
total: timings?.total ?? 0,
|
||||
dns: timings?.dns,
|
||||
tcp: timings?.tcp,
|
||||
tls: timings?.tls,
|
||||
firstByte: timings?.firstByte,
|
||||
download: timings?.download,
|
||||
});
|
||||
|
||||
const mapGeoResults = (geoResults?: GeoCheckResult[]): GeoCheckResult[] | undefined => {
|
||||
if (!geoResults?.length) {
|
||||
return undefined;
|
||||
}
|
||||
return geoResults.map((result) => ({
|
||||
continent: result.continent ?? "",
|
||||
region: result.region,
|
||||
country: result.country ?? "",
|
||||
state: result.state,
|
||||
city: result.city ?? "",
|
||||
longitude: result.longitude ?? 0,
|
||||
latitude: result.latitude ?? 0,
|
||||
status: result.status ?? false,
|
||||
statusCode: result.statusCode ?? 0,
|
||||
timings: mapGeoTimings(result.timings),
|
||||
}));
|
||||
};
|
||||
|
||||
const mapMetadata = (metadata: CheckDocument["metadata"]): CheckMetadata => ({
|
||||
monitorId: toStringId(metadata.monitorId),
|
||||
teamId: toStringId(metadata.teamId),
|
||||
@@ -194,6 +223,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
seo: doc.seo,
|
||||
performance: doc.performance,
|
||||
audits: mapAudits(doc.audits),
|
||||
geoResults: mapGeoResults(doc.geoResults),
|
||||
__v: doc.__v ?? 0,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
@@ -427,85 +457,131 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
) => {
|
||||
const matchStage = {
|
||||
// Match stage for primary checks (exclude geo checks)
|
||||
const primaryCheckMatchStage = {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
createdAt: { $gte: startDate, $lte: endDate },
|
||||
geoResults: { $exists: false },
|
||||
};
|
||||
const [result] = await CheckModel.aggregate([
|
||||
{ $match: matchStage },
|
||||
{ $sort: { createdAt: 1 } },
|
||||
{
|
||||
$facet: {
|
||||
uptimePercentage: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
percentage: {
|
||||
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
|
||||
|
||||
// Match stage for geo checks only
|
||||
const geoCheckMatchStage = {
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
createdAt: { $gte: startDate, $lte: endDate },
|
||||
geoResults: { $exists: true, $ne: null },
|
||||
};
|
||||
|
||||
// Run two separate aggregations - one for primary checks, one for geo checks
|
||||
// TODO, only do this if the monitor has geochecks enabled, otherwise we only need one aggregation
|
||||
const [primaryResult, geoResults] = await Promise.all([
|
||||
CheckModel.aggregate([
|
||||
{ $match: primaryCheckMatchStage },
|
||||
{ $sort: { createdAt: 1 } },
|
||||
{
|
||||
$facet: {
|
||||
uptimePercentage: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
groupedAvgResponseTime: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
],
|
||||
groupedChecks: [
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
percentage: {
|
||||
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
|
||||
},
|
||||
},
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
|
||||
],
|
||||
groupedUpChecks: [
|
||||
{ $match: { status: true } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
],
|
||||
groupedAvgResponseTime: [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
totalChecks: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
|
||||
],
|
||||
groupedDownChecks: [
|
||||
{ $match: { status: false } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
],
|
||||
groupedChecks: [
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
},
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
totalChecks: { $sum: 1 },
|
||||
},
|
||||
totalChecks: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
|
||||
],
|
||||
{ $sort: { _id: 1 } },
|
||||
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
|
||||
],
|
||||
groupedUpChecks: [
|
||||
{ $match: { status: true } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
},
|
||||
totalChecks: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
|
||||
],
|
||||
groupedDownChecks: [
|
||||
{ $match: { status: false } },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
$dateToString: { format: dateString, date: "$createdAt" },
|
||||
},
|
||||
totalChecks: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
},
|
||||
},
|
||||
{ $sort: { _id: 1 } },
|
||||
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
CheckModel.aggregate([
|
||||
{ $match: geoCheckMatchStage },
|
||||
{ $sort: { createdAt: 1 } },
|
||||
{ $unwind: "$geoResults" },
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
date: { $dateToString: { format: dateString, date: "$createdAt" } },
|
||||
continent: "$geoResults.continent",
|
||||
},
|
||||
avgResponseTime: { $avg: "$geoResults.timings.total" },
|
||||
totalChecks: { $sum: 1 },
|
||||
upChecks: { $sum: { $cond: [{ $eq: ["$geoResults.status", true] }, 1, 0] } },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
bucketDate: "$_id.date",
|
||||
continent: "$_id.continent",
|
||||
avgResponseTime: 1,
|
||||
totalChecks: 1,
|
||||
uptimePercentage: {
|
||||
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ $sort: { bucketDate: 1, continent: 1 } },
|
||||
]),
|
||||
]);
|
||||
|
||||
const [result] = primaryResult;
|
||||
const groupedGeoChecks = geoResults ?? [];
|
||||
|
||||
const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0;
|
||||
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
|
||||
|
||||
@@ -516,6 +592,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
groupedDownChecks: result?.groupedDownChecks ?? [],
|
||||
uptimePercentage,
|
||||
avgResponseTime,
|
||||
groupedGeoChecks,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -353,6 +353,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||
geoCheckInterval: doc.geoCheckInterval ?? 5,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
@@ -408,6 +411,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||
geoCheckInterval: doc.geoCheckInterval ?? 5,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
|
||||
@@ -97,6 +97,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) {
|
||||
@@ -131,14 +132,28 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
|
||||
// Add geo check job if enabled
|
||||
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);
|
||||
// Also pause geo check job if it exists
|
||||
await this.scheduler.pauseJob(`${monitor.id}-geo`);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
@@ -151,6 +166,8 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
|
||||
resumeJob = async (monitor: any) => {
|
||||
const result = await this.scheduler.resumeJob(monitor.id);
|
||||
// Also resume geo check job if it exists
|
||||
await this.scheduler.resumeJob(`${monitor.id}-geo`);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
@@ -163,6 +180,29 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
|
||||
updateJob = async (monitor: any) => {
|
||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||
|
||||
// Handle geo check job
|
||||
const geoJobId = `${monitor.id}-geo`;
|
||||
const geoJobExists = await this.scheduler.getJob(geoJobId);
|
||||
|
||||
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
||||
if (geoJobExists) {
|
||||
// Update existing geo job
|
||||
this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, data: monitor, active: monitor.isActive });
|
||||
} else {
|
||||
// Add new geo job
|
||||
this.scheduler.addJob({
|
||||
id: geoJobId,
|
||||
template: "geo-check-job",
|
||||
repeat: monitor.geoCheckInterval,
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
}
|
||||
} else if (geoJobExists) {
|
||||
// Remove geo job if it exists but is no longer enabled
|
||||
this.scheduler.removeJob(geoJobId);
|
||||
}
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
|
||||
@@ -176,6 +176,98 @@ class SuperSimpleQueueHelper {
|
||||
};
|
||||
};
|
||||
|
||||
getGeoCheckJob = () => {
|
||||
return async (monitor: Monitor) => {
|
||||
try {
|
||||
const monitorId = monitor.id;
|
||||
const teamId = monitor.teamId;
|
||||
|
||||
if (!monitorId) {
|
||||
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
|
||||
}
|
||||
|
||||
// Safety checks
|
||||
if (!monitor.geoCheckEnabled || monitor.type !== "http") {
|
||||
this.logger.debug({
|
||||
message: `Geo check skipped for monitor ${monitorId} - not enabled or not HTTP type`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
|
||||
this.logger.warn({
|
||||
message: `Geo check skipped for monitor ${monitorId} - no locations configured`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for maintenance window
|
||||
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
|
||||
if (maintenanceWindowActive) {
|
||||
this.logger.debug({
|
||||
message: `Geo check skipped for monitor ${monitorId} - in maintenance window`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug({
|
||||
message: `Starting geo check for monitor ${monitorId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
|
||||
// TODO: Implement GlobalPing API integration
|
||||
// Step 1: Create measurement via GlobalPing API
|
||||
// const measurementId = await this.globalPingService.createMeasurement(
|
||||
// monitor.url,
|
||||
// monitor.geoCheckLocations
|
||||
// );
|
||||
|
||||
// Step 2: Poll for results (blocks 2-30s, but doesn't affect primary monitor checks)
|
||||
// const geoResults = await this.globalPingService.pollForResults(measurementId);
|
||||
|
||||
// Step 3: Create check document with only geo results
|
||||
// const check = {
|
||||
// metadata: {
|
||||
// monitorId: monitor.id,
|
||||
// teamId: monitor.teamId,
|
||||
// type: monitor.type,
|
||||
// },
|
||||
// status: true,
|
||||
// responseTime: 0,
|
||||
// statusCode: 200,
|
||||
// message: "Geo check",
|
||||
// geoResults,
|
||||
// ack: false,
|
||||
// };
|
||||
|
||||
// Step 4: Save check
|
||||
// await this.checksRepository.create([check]);
|
||||
|
||||
this.logger.info({
|
||||
message: `Geo check completed for monitor ${monitorId}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoCheckJob",
|
||||
details: error,
|
||||
stack: error.stack,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
getCleanupOrphanedJob = () => {
|
||||
return async () => {
|
||||
try {
|
||||
|
||||
@@ -90,6 +90,28 @@ export interface ILighthouseAudit {
|
||||
numericUnit?: string;
|
||||
}
|
||||
|
||||
export interface GeoCheckTimings {
|
||||
total: number;
|
||||
dns?: number;
|
||||
tcp?: number;
|
||||
tls?: number;
|
||||
firstByte?: number;
|
||||
download?: number;
|
||||
}
|
||||
|
||||
export interface GeoCheckResult {
|
||||
continent: string;
|
||||
region?: string;
|
||||
country: string;
|
||||
state?: string;
|
||||
city: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
status: boolean;
|
||||
statusCode: number;
|
||||
timings: GeoCheckTimings;
|
||||
}
|
||||
|
||||
export interface Check {
|
||||
id: string;
|
||||
metadata: CheckMetadata;
|
||||
@@ -113,6 +135,7 @@ export interface Check {
|
||||
seo?: number;
|
||||
performance?: number;
|
||||
audits?: CheckAudits;
|
||||
geoResults?: GeoCheckResult[];
|
||||
__v: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -179,6 +202,14 @@ export interface PageSpeedGroupedCheck {
|
||||
totalChecks: number;
|
||||
}
|
||||
|
||||
export interface GroupedGeoCheck {
|
||||
bucketDate: string;
|
||||
continent: string;
|
||||
avgResponseTime: number;
|
||||
totalChecks: number;
|
||||
uptimePercentage: number;
|
||||
}
|
||||
|
||||
export interface UptimeChecksResult {
|
||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
|
||||
groupedChecks: GroupedCheck[];
|
||||
@@ -186,6 +217,7 @@ export interface UptimeChecksResult {
|
||||
groupedDownChecks: GroupedCheck[];
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
groupedGeoChecks?: GroupedGeoCheck[];
|
||||
}
|
||||
|
||||
export interface ChecksSummary {
|
||||
|
||||
@@ -7,6 +7,9 @@ export type MonitorType = (typeof MonitorTypes)[number];
|
||||
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export const GeoCheckLocations = ["EU", "NA", "AS", "SA", "AF", "OC"] as const;
|
||||
export type GeoCheckLocation = (typeof GeoCheckLocations)[number];
|
||||
|
||||
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
||||
|
||||
export interface Monitor {
|
||||
@@ -43,6 +46,9 @@ export interface Monitor {
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
group: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoCheckLocation[];
|
||||
geoCheckInterval?: number;
|
||||
recentChecks: CheckSnapshot[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import joi, { type CustomHelpers } from "joi";
|
||||
import { type UserRole, UserRoles } from "@/types/user.js";
|
||||
import { GeoCheckLocations } from "@/types/monitor.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(...GeoCheckLocations))
|
||||
.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(...GeoCheckLocations))
|
||||
.optional(),
|
||||
geoCheckInterval: joi.number().min(5).optional(),
|
||||
})
|
||||
.options({ stripUnknown: true });
|
||||
|
||||
|
||||
Reference in new issue
Block a user