geo job
This commit is contained in:
1 parent
19fc85ae2d
commit
35c022df0f
4 files changed
+248
-99
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user