This commit is contained in:
Alex Holliday committed 2026-02-23 00:00:45 +00:00
1 parent 19fc85ae2d
commit 35c022df0f
4 files changed
+248 -99

No files matched your search

+1 -1
View File
@@ -29,7 +29,7 @@ import {
} from "@/Components/inputs";
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
import { useMonitorForm } from "@/Hooks/useMonitorForm";
import type { Monitor, MonitorType, GamesMap, GeoCheckLocation } from "@/Types/Monitor";
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";
@@ -457,116 +457,133 @@ 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" },
},
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" },
},
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 } },
],
groupedGeoChecks: [
{ $match: { geoResults: { $exists: true, $ne: null, $not: { $size: 0 } } } },
{ $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"] }],
{
$project: {
_id: 0,
percentage: {
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
},
},
},
},
{ $sort: { bucketDate: 1, continent: 1 } },
],
],
groupedAvgResponseTime: [
{
$group: {
_id: null,
avgResponseTime: { $avg: "$responseTime" },
},
},
],
groupedChecks: [
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
},
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" },
},
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;
const groupedGeoChecks = result?.groupedGeoChecks ?? [];
return {
monitorType,
@@ -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 {