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 || [],
|
notifications: data?.notifications || [],
|
||||||
statusWindowSize: data?.statusWindowSize || 5,
|
statusWindowSize: data?.statusWindowSize || 5,
|
||||||
statusWindowThreshold: data?.statusWindowThreshold || 60,
|
statusWindowThreshold: data?.statusWindowThreshold || 60,
|
||||||
|
geoCheckEnabled: data?.geoCheckEnabled || false,
|
||||||
|
geoCheckLocations: data?.geoCheckLocations || [],
|
||||||
|
geoCheckInterval: data?.geoCheckInterval || 300000,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useMonitorForm = ({
|
export const useMonitorForm = ({
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
||||||
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
||||||
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
|
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
|
||||||
|
import { GeoCheckLocations } from "@/Types/Monitor";
|
||||||
import type { Notification } from "@/Types/Notification";
|
import type { Notification } from "@/Types/Notification";
|
||||||
import type { MonitorFormData } from "@/Validation/monitor";
|
import type { MonitorFormData } from "@/Validation/monitor";
|
||||||
|
|
||||||
@@ -165,6 +166,7 @@ const CreateMonitorPage = () => {
|
|||||||
const watchedType = watch("type") as MonitorType;
|
const watchedType = watch("type") as MonitorType;
|
||||||
|
|
||||||
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
|
const watchedUseAdvancedMatching = watch("useAdvancedMatching") as boolean;
|
||||||
|
const watchGeoCheckEnabled = watch("geoCheckEnabled") as boolean;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
clearErrors();
|
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
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
|
|||||||
@@ -160,6 +160,14 @@ export interface PageSpeedGroupedCheck {
|
|||||||
totalChecks: number;
|
totalChecks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GroupedGeoCheck {
|
||||||
|
bucketDate: string;
|
||||||
|
continent: string;
|
||||||
|
avgResponseTime: number;
|
||||||
|
totalChecks: number;
|
||||||
|
uptimePercentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LatestCheck {
|
export interface LatestCheck {
|
||||||
status: boolean;
|
status: boolean;
|
||||||
responseTime: number;
|
responseTime: number;
|
||||||
@@ -236,6 +244,7 @@ export interface UptimeChecksResult {
|
|||||||
groupedDownChecks: GroupedCheck[];
|
groupedDownChecks: GroupedCheck[];
|
||||||
uptimePercentage: number;
|
uptimePercentage: number;
|
||||||
avgResponseTime: number;
|
avgResponseTime: number;
|
||||||
|
groupedGeoChecks?: GroupedGeoCheck[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChecksSummary {
|
export interface ChecksSummary {
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
|
|||||||
|
|
||||||
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
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 {
|
export interface Monitor {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -59,6 +62,9 @@ export interface Monitor {
|
|||||||
selectedDisks: string[];
|
selectedDisks: string[];
|
||||||
gameId?: string;
|
gameId?: string;
|
||||||
group: string | null;
|
group: string | null;
|
||||||
|
geoCheckEnabled?: boolean;
|
||||||
|
geoCheckLocations?: GeoCheckLocation[];
|
||||||
|
geoCheckInterval?: number;
|
||||||
recentChecks: CheckSnapshot[];
|
recentChecks: CheckSnapshot[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ const baseSchema = z.object({
|
|||||||
.number({ message: "Threshold percentage is required" })
|
.number({ message: "Threshold percentage is required" })
|
||||||
.min(1, "Incident percentage must be at least 1")
|
.min(1, "Incident percentage must be at least 1")
|
||||||
.max(100, "Incident percentage must be at most 100"),
|
.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
|
// HTTP monitor schema
|
||||||
|
|||||||
@@ -431,6 +431,31 @@
|
|||||||
},
|
},
|
||||||
"title": "Advanced settings"
|
"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": {
|
"frequency": {
|
||||||
"description": "How often do you want to check the status of this monitor?",
|
"description": "How often do you want to check the status of this monitor?",
|
||||||
"option": {
|
"option": {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import type {
|
|||||||
CheckMemoryInfo,
|
CheckMemoryInfo,
|
||||||
CheckMetadata,
|
CheckMetadata,
|
||||||
CheckNetworkInterfaceInfo,
|
CheckNetworkInterfaceInfo,
|
||||||
|
GeoCheckResult,
|
||||||
|
GeoCheckTimings,
|
||||||
GotTimings,
|
GotTimings,
|
||||||
ILighthouseAudit,
|
ILighthouseAudit,
|
||||||
} from "@/types/check.js";
|
} from "@/types/check.js";
|
||||||
@@ -175,6 +177,34 @@ const auditsSchema = new Schema<CheckAudits>(
|
|||||||
{ _id: false }
|
{ _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>(
|
const metadataSchema = new Schema<CheckMetadataDocument>(
|
||||||
{
|
{
|
||||||
monitorId: {
|
monitorId: {
|
||||||
@@ -281,6 +311,10 @@ const CheckSchema = new Schema<CheckDocument>(
|
|||||||
type: auditsSchema,
|
type: auditsSchema,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
geoResults: {
|
||||||
|
type: [geoCheckResultSchema],
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
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 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 };
|
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
||||||
|
|
||||||
@@ -169,6 +169,19 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
|||||||
return value && value.trim() ? value.trim() : null;
|
return value && value.trim() ? value.trim() : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
geoCheckEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
geoCheckLocations: {
|
||||||
|
type: [String],
|
||||||
|
enum: GeoCheckLocations,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
geoCheckInterval: {
|
||||||
|
type: Number,
|
||||||
|
default: 300000,
|
||||||
|
},
|
||||||
recentChecks: {
|
recentChecks: {
|
||||||
type: [checkSnapshotSchema],
|
type: [checkSnapshotSchema],
|
||||||
default: [],
|
default: [],
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import type {
|
|||||||
CheckMemoryInfo,
|
CheckMemoryInfo,
|
||||||
CheckMetadata,
|
CheckMetadata,
|
||||||
CheckNetworkInterfaceInfo,
|
CheckNetworkInterfaceInfo,
|
||||||
|
GeoCheckResult,
|
||||||
|
GeoCheckTimings,
|
||||||
GotTimings,
|
GotTimings,
|
||||||
MonitorType,
|
MonitorType,
|
||||||
} from "@/types/index.js";
|
} 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 => ({
|
const mapMetadata = (metadata: CheckDocument["metadata"]): CheckMetadata => ({
|
||||||
monitorId: toStringId(metadata.monitorId),
|
monitorId: toStringId(metadata.monitorId),
|
||||||
teamId: toStringId(metadata.teamId),
|
teamId: toStringId(metadata.teamId),
|
||||||
@@ -194,6 +223,7 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
seo: doc.seo,
|
seo: doc.seo,
|
||||||
performance: doc.performance,
|
performance: doc.performance,
|
||||||
audits: mapAudits(doc.audits),
|
audits: mapAudits(doc.audits),
|
||||||
|
geoResults: mapGeoResults(doc.geoResults),
|
||||||
__v: doc.__v ?? 0,
|
__v: doc.__v ?? 0,
|
||||||
createdAt: toDateString(doc.createdAt),
|
createdAt: toDateString(doc.createdAt),
|
||||||
updatedAt: toDateString(doc.updatedAt),
|
updatedAt: toDateString(doc.updatedAt),
|
||||||
@@ -427,85 +457,131 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
endDate: Date,
|
endDate: Date,
|
||||||
dateString: string
|
dateString: string
|
||||||
) => {
|
) => {
|
||||||
const matchStage = {
|
// Match stage for primary checks (exclude geo checks)
|
||||||
|
const primaryCheckMatchStage = {
|
||||||
"metadata.monitorId": monitorObjectId,
|
"metadata.monitorId": monitorObjectId,
|
||||||
createdAt: { $gte: startDate, $lte: endDate },
|
createdAt: { $gte: startDate, $lte: endDate },
|
||||||
|
geoResults: { $exists: false },
|
||||||
};
|
};
|
||||||
const [result] = await CheckModel.aggregate([
|
|
||||||
{ $match: matchStage },
|
// Match stage for geo checks only
|
||||||
{ $sort: { createdAt: 1 } },
|
const geoCheckMatchStage = {
|
||||||
{
|
"metadata.monitorId": monitorObjectId,
|
||||||
$facet: {
|
createdAt: { $gte: startDate, $lte: endDate },
|
||||||
uptimePercentage: [
|
geoResults: { $exists: true, $ne: null },
|
||||||
{
|
};
|
||||||
$group: {
|
|
||||||
_id: null,
|
// Run two separate aggregations - one for primary checks, one for geo checks
|
||||||
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
|
// TODO, only do this if the monitor has geochecks enabled, otherwise we only need one aggregation
|
||||||
totalChecks: { $sum: 1 },
|
const [primaryResult, geoResults] = await Promise.all([
|
||||||
},
|
CheckModel.aggregate([
|
||||||
},
|
{ $match: primaryCheckMatchStage },
|
||||||
{
|
{ $sort: { createdAt: 1 } },
|
||||||
$project: {
|
{
|
||||||
_id: 0,
|
$facet: {
|
||||||
percentage: {
|
uptimePercentage: [
|
||||||
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
|
{
|
||||||
|
$group: {
|
||||||
|
_id: null,
|
||||||
|
upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } },
|
||||||
|
totalChecks: { $sum: 1 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
],
|
$project: {
|
||||||
groupedAvgResponseTime: [
|
_id: 0,
|
||||||
{
|
percentage: {
|
||||||
$group: {
|
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
|
||||||
_id: null,
|
},
|
||||||
avgResponseTime: { $avg: "$responseTime" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
groupedChecks: [
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: {
|
|
||||||
$dateToString: { format: dateString, date: "$createdAt" },
|
|
||||||
},
|
},
|
||||||
avgResponseTime: { $avg: "$responseTime" },
|
|
||||||
totalChecks: { $sum: 1 },
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
{ $sort: { _id: 1 } },
|
groupedAvgResponseTime: [
|
||||||
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
|
{
|
||||||
],
|
$group: {
|
||||||
groupedUpChecks: [
|
_id: null,
|
||||||
{ $match: { status: true } },
|
avgResponseTime: { $avg: "$responseTime" },
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: {
|
|
||||||
$dateToString: { format: dateString, date: "$createdAt" },
|
|
||||||
},
|
},
|
||||||
totalChecks: { $sum: 1 },
|
|
||||||
avgResponseTime: { $avg: "$responseTime" },
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
{ $sort: { _id: 1 } },
|
groupedChecks: [
|
||||||
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
|
{
|
||||||
],
|
$group: {
|
||||||
groupedDownChecks: [
|
_id: {
|
||||||
{ $match: { status: false } },
|
$dateToString: { format: dateString, date: "$createdAt" },
|
||||||
{
|
},
|
||||||
$group: {
|
avgResponseTime: { $avg: "$responseTime" },
|
||||||
_id: {
|
totalChecks: { $sum: 1 },
|
||||||
$dateToString: { format: dateString, date: "$createdAt" },
|
|
||||||
},
|
},
|
||||||
totalChecks: { $sum: 1 },
|
|
||||||
avgResponseTime: { $avg: "$responseTime" },
|
|
||||||
},
|
},
|
||||||
},
|
{ $sort: { _id: 1 } },
|
||||||
{ $sort: { _id: 1 } },
|
{ $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } },
|
||||||
{ $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 uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0;
|
||||||
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
|
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
|
||||||
|
|
||||||
@@ -516,6 +592,7 @@ class MongoChecksRepository implements IChecksRepository {
|
|||||||
groupedDownChecks: result?.groupedDownChecks ?? [],
|
groupedDownChecks: result?.groupedDownChecks ?? [],
|
||||||
uptimePercentage,
|
uptimePercentage,
|
||||||
avgResponseTime,
|
avgResponseTime,
|
||||||
|
groupedGeoChecks,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -353,6 +353,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
selectedDisks: doc.selectedDisks ?? [],
|
selectedDisks: doc.selectedDisks ?? [],
|
||||||
gameId: doc.gameId ?? undefined,
|
gameId: doc.gameId ?? undefined,
|
||||||
group: doc.group ?? null,
|
group: doc.group ?? null,
|
||||||
|
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||||
|
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||||
|
geoCheckInterval: doc.geoCheckInterval ?? 5,
|
||||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||||
createdAt: toDateString(doc.createdAt),
|
createdAt: toDateString(doc.createdAt),
|
||||||
updatedAt: toDateString(doc.updatedAt),
|
updatedAt: toDateString(doc.updatedAt),
|
||||||
@@ -408,6 +411,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
|||||||
selectedDisks: doc.selectedDisks ?? [],
|
selectedDisks: doc.selectedDisks ?? [],
|
||||||
gameId: doc.gameId ?? undefined,
|
gameId: doc.gameId ?? undefined,
|
||||||
group: doc.group ?? null,
|
group: doc.group ?? null,
|
||||||
|
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||||
|
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||||
|
geoCheckInterval: doc.geoCheckInterval ?? 5,
|
||||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||||
createdAt: toDateString(doc.createdAt),
|
createdAt: toDateString(doc.createdAt),
|
||||||
updatedAt: toDateString(doc.updatedAt),
|
updatedAt: toDateString(doc.updatedAt),
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
this.scheduler.start();
|
this.scheduler.start();
|
||||||
|
|
||||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
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());
|
this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob());
|
||||||
const monitors = await this.monitorsRepository.findAll();
|
const monitors = await this.monitorsRepository.findAll();
|
||||||
if (!monitors) {
|
if (!monitors) {
|
||||||
@@ -131,14 +132,28 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
active: monitor.isActive,
|
active: monitor.isActive,
|
||||||
data: monitor,
|
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) => {
|
deleteJob = async (monitor: any) => {
|
||||||
this.scheduler.removeJob(monitor.id);
|
this.scheduler.removeJob(monitor.id);
|
||||||
|
this.scheduler.removeJob(`${monitor.id}-geo`);
|
||||||
};
|
};
|
||||||
|
|
||||||
pauseJob = async (monitor: any) => {
|
pauseJob = async (monitor: any) => {
|
||||||
const result = await this.scheduler.pauseJob(monitor.id);
|
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) {
|
if (result === false) {
|
||||||
throw new Error("Failed to resume monitor");
|
throw new Error("Failed to resume monitor");
|
||||||
}
|
}
|
||||||
@@ -151,6 +166,8 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
|
|
||||||
resumeJob = async (monitor: any) => {
|
resumeJob = async (monitor: any) => {
|
||||||
const result = await this.scheduler.resumeJob(monitor.id);
|
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) {
|
if (result === false) {
|
||||||
throw new Error("Failed to resume monitor");
|
throw new Error("Failed to resume monitor");
|
||||||
}
|
}
|
||||||
@@ -163,6 +180,29 @@ class SuperSimpleQueue implements ISuperSimpleQueue {
|
|||||||
|
|
||||||
updateJob = async (monitor: any) => {
|
updateJob = async (monitor: any) => {
|
||||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
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 () => {
|
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 = () => {
|
getCleanupOrphanedJob = () => {
|
||||||
return async () => {
|
return async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -90,6 +90,28 @@ export interface ILighthouseAudit {
|
|||||||
numericUnit?: string;
|
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 {
|
export interface Check {
|
||||||
id: string;
|
id: string;
|
||||||
metadata: CheckMetadata;
|
metadata: CheckMetadata;
|
||||||
@@ -113,6 +135,7 @@ export interface Check {
|
|||||||
seo?: number;
|
seo?: number;
|
||||||
performance?: number;
|
performance?: number;
|
||||||
audits?: CheckAudits;
|
audits?: CheckAudits;
|
||||||
|
geoResults?: GeoCheckResult[];
|
||||||
__v: number;
|
__v: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -179,6 +202,14 @@ export interface PageSpeedGroupedCheck {
|
|||||||
totalChecks: number;
|
totalChecks: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GroupedGeoCheck {
|
||||||
|
bucketDate: string;
|
||||||
|
continent: string;
|
||||||
|
avgResponseTime: number;
|
||||||
|
totalChecks: number;
|
||||||
|
uptimePercentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UptimeChecksResult {
|
export interface UptimeChecksResult {
|
||||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
|
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">;
|
||||||
groupedChecks: GroupedCheck[];
|
groupedChecks: GroupedCheck[];
|
||||||
@@ -186,6 +217,7 @@ export interface UptimeChecksResult {
|
|||||||
groupedDownChecks: GroupedCheck[];
|
groupedDownChecks: GroupedCheck[];
|
||||||
uptimePercentage: number;
|
uptimePercentage: number;
|
||||||
avgResponseTime: number;
|
avgResponseTime: number;
|
||||||
|
groupedGeoChecks?: GroupedGeoCheck[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChecksSummary {
|
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 const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
||||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
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 type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
||||||
|
|
||||||
export interface Monitor {
|
export interface Monitor {
|
||||||
@@ -43,6 +46,9 @@ export interface Monitor {
|
|||||||
selectedDisks: string[];
|
selectedDisks: string[];
|
||||||
gameId?: string;
|
gameId?: string;
|
||||||
group: string | null;
|
group: string | null;
|
||||||
|
geoCheckEnabled?: boolean;
|
||||||
|
geoCheckLocations?: GeoCheckLocation[];
|
||||||
|
geoCheckInterval?: number;
|
||||||
recentChecks: CheckSnapshot[];
|
recentChecks: CheckSnapshot[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import joi, { type CustomHelpers } from "joi";
|
import joi, { type CustomHelpers } from "joi";
|
||||||
import { type UserRole, UserRoles } from "@/types/user.js";
|
import { type UserRole, UserRoles } from "@/types/user.js";
|
||||||
|
import { GeoCheckLocations } from "@/types/monitor.js";
|
||||||
|
|
||||||
//****************************************
|
//****************************************
|
||||||
// Custom Validators
|
// Custom Validators
|
||||||
@@ -172,6 +173,12 @@ const createMonitorBodyValidation = joi.object({
|
|||||||
gameId: joi.string().allow(""),
|
gameId: joi.string().allow(""),
|
||||||
selectedDisks: joi.array().items(joi.string()).optional(),
|
selectedDisks: joi.array().items(joi.string()).optional(),
|
||||||
group: joi.string().max(50).trim().allow(null, "").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(
|
const createMonitorsBodyValidation = joi.array().items(
|
||||||
@@ -203,6 +210,12 @@ const editMonitorBodyValidation = joi
|
|||||||
gameId: joi.string().allow(""),
|
gameId: joi.string().allow(""),
|
||||||
selectedDisks: joi.array().items(joi.string()).optional(),
|
selectedDisks: joi.array().items(joi.string()).optional(),
|
||||||
group: joi.string().max(50).trim().allow(null, "").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 });
|
.options({ stripUnknown: true });
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user