Merge pull request #3385 from bluewave-labs/feat/globalping-ping-type
feat: globalping ping type
This commit is contained in:
10 files changed
+97
-58
No files matched your search
@@ -32,7 +32,12 @@ import {
|
||||
import { SPACING, LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { useGet, usePost, usePatch, useDelete } from "@/Hooks/UseApi";
|
||||
import { useMonitorForm } from "@/Hooks/useMonitorForm";
|
||||
import type { Monitor, MonitorType, GamesMap } from "@/Types/Monitor";
|
||||
import {
|
||||
type Monitor,
|
||||
type MonitorType,
|
||||
type GamesMap,
|
||||
supportsGeoCheck,
|
||||
} from "@/Types/Monitor";
|
||||
import type { Notification } from "@/Types/Notification";
|
||||
import type { MonitorFormData } from "@/Validation/monitor";
|
||||
|
||||
@@ -885,7 +890,7 @@ const CreateMonitorPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{watchedType === "http" && (
|
||||
{supportsGeoCheck(watchedType) && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.geoChecks.title")}
|
||||
subtitle={t("pages.createMonitor.form.geoChecks.description")}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useState, useMemo } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useGet } from "@/Hooks/UseApi";
|
||||
import type { MonitorDetailsResponse } from "@/Types/Monitor";
|
||||
import { type MonitorDetailsResponse, supportsGeoCheck } from "@/Types/Monitor";
|
||||
import type { ChecksResponse } from "@/Types/Check";
|
||||
import type {
|
||||
GeoChecksResult,
|
||||
@@ -122,7 +122,7 @@ const UptimeDetailsPage = () => {
|
||||
);
|
||||
|
||||
const geoChecksUrl = useMemo(() => {
|
||||
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
|
||||
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
|
||||
return null;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
@@ -141,7 +141,7 @@ const UptimeDetailsPage = () => {
|
||||
|
||||
// Fetch paginated geo checks for the table
|
||||
const geoChecksTableUrl = useMemo(() => {
|
||||
if (!monitorId || monitor?.type !== "http" || !monitor?.geoCheckEnabled) {
|
||||
if (!monitorId || !supportsGeoCheck(monitor?.type) || !monitor?.geoCheckEnabled) {
|
||||
return null;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -16,6 +16,15 @@ export const MonitorTypes = [
|
||||
] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
|
||||
|
||||
export const supportsGeoCheck = (type: MonitorType | undefined): boolean => {
|
||||
if (!type) {
|
||||
return false;
|
||||
}
|
||||
return GeoCheckSupportedTypes.includes(type);
|
||||
};
|
||||
|
||||
export const MonitorStatuses = [
|
||||
"up",
|
||||
"down",
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { IGeoChecksRepository } from "./IGeoChecksRepository.js";
|
||||
import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js";
|
||||
import type { GeoChecksQueryResult, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
||||
import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
|
||||
import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
|
||||
const SERVICE_NAME = "GeoChecksRepository";
|
||||
|
||||
class MongoGeoChecksRepository implements IGeoChecksRepository {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: any;
|
||||
constructor(logger: any) {
|
||||
private logger: ILogger;
|
||||
constructor(logger: ILogger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export class GeoChecksService implements IGeoChecksService {
|
||||
}
|
||||
|
||||
// Step 1: Create measurement request
|
||||
const measurementId = await this.globalPingService.createMeasurement(monitor.url, monitor.geoCheckLocations);
|
||||
const measurementId = await this.globalPingService.createMeasurement(monitor.type, monitor.url, monitor.geoCheckLocations);
|
||||
|
||||
if (!measurementId) {
|
||||
// GlobalPing API is down, skip this check
|
||||
@@ -76,7 +76,6 @@ export class GeoChecksService implements IGeoChecksService {
|
||||
message: "Skipping geo check due to API unavailability",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildGeoCheck",
|
||||
details: { monitorId: monitor.id },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -90,7 +89,6 @@ export class GeoChecksService implements IGeoChecksService {
|
||||
message: "No successful geo check results",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildGeoCheck",
|
||||
details: { monitorId: monitor.id, measurementId },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -102,17 +100,16 @@ export class GeoChecksService implements IGeoChecksService {
|
||||
message: `Geo check completed for monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "buildGeoCheck",
|
||||
details: { monitorId: monitor.id, resultsCount: results.length },
|
||||
});
|
||||
|
||||
return geoCheck;
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
this.logger.error({
|
||||
message: "Error executing geo check",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildGeoCheck",
|
||||
details: { monitorId: monitor.id, error: error.message },
|
||||
stack: error.stack,
|
||||
details: { monitorId: monitor.id, error: error instanceof Error ? error.message : "Unknown error" },
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -172,19 +169,9 @@ export class GeoChecksService implements IGeoChecksService {
|
||||
|
||||
const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
|
||||
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
|
||||
|
||||
this.logger.debug({
|
||||
message: "getGeoChecksByMonitor query params",
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoChecksByMonitor",
|
||||
details: { continent, continents, query },
|
||||
});
|
||||
|
||||
const parsedPage = page ? parseInt(page) : page;
|
||||
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
|
||||
|
||||
const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents);
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
PageSpeedDetailsResult,
|
||||
GamesMap,
|
||||
} from "@/types/monitor.js";
|
||||
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||
import type {
|
||||
IChecksRepository,
|
||||
@@ -340,7 +341,7 @@ export class MonitorService implements IMonitorService {
|
||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||
}
|
||||
|
||||
if (monitor.type !== "http" || !monitor.geoCheckEnabled) {
|
||||
if (!supportsGeoCheck(monitor.type) || !monitor.geoCheckEnabled) {
|
||||
return { groupedGeoChecks: [] };
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import Scheduler from "super-simple-scheduler";
|
||||
import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
|
||||
type QueueJobFailure = {
|
||||
@@ -125,8 +126,13 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
data: monitor,
|
||||
});
|
||||
|
||||
// Return early if we don't need geo checks
|
||||
if (!supportsGeoCheck(monitor.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add geo check job if enabled for HTTP monitors
|
||||
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
||||
if (monitor.geoCheckEnabled) {
|
||||
this.scheduler.addJob({
|
||||
id: `${monitorId}-geo`,
|
||||
template: "geo-check-job",
|
||||
@@ -175,7 +181,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
|
||||
// Handle geo check job lifecycle
|
||||
const geoJobId = `${monitor.id}-geo`;
|
||||
if (monitor.geoCheckEnabled && monitor.type === "http") {
|
||||
if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) {
|
||||
// Check if geo job exists
|
||||
const existingGeoJob = await this.scheduler.getJob(geoJobId);
|
||||
if (existingGeoJob) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const SERVICE_NAME = "JobQueueHelper";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js";
|
||||
import type { StatusChangeResult } from "@/types/index.js";
|
||||
@@ -280,19 +281,18 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
|
||||
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" });
|
||||
}
|
||||
|
||||
if (monitor.type !== "http") {
|
||||
if (!monitor.geoCheckEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!supportsGeoCheck(monitor.type)) {
|
||||
this.logger.debug({
|
||||
message: `Monitor ${monitorId} is not HTTP type, skipping geo check`,
|
||||
message: `Monitor ${monitorId} type does not support geo checks, skipping`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getGeoCheckJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!monitor.geoCheckEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!monitor.geoCheckLocations || monitor.geoCheckLocations.length === 0) {
|
||||
this.logger.warn({
|
||||
message: `No geo check locations configured for monitor ${monitorId}`,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/types/geoCheck.js";
|
||||
import { supportsGeoCheck } from "@/types/monitor.js";
|
||||
import { MonitorType } from "@/types/index.js";
|
||||
import type { ILogger } from "@/utils/logger.js";
|
||||
import got from "got";
|
||||
|
||||
@@ -8,7 +10,7 @@ const POLL_INTERVAL_MS = 2000;
|
||||
const MAX_POLL_TIMEOUT_MS = 30000;
|
||||
|
||||
interface GlobalPingMeasurementRequest {
|
||||
type: "http";
|
||||
type: MonitorType;
|
||||
target: string;
|
||||
locations: Array<{ continent: GeoContinent }>;
|
||||
limit: number;
|
||||
@@ -44,13 +46,22 @@ interface GlobalPingProbeResult {
|
||||
firstByte: number;
|
||||
download: number;
|
||||
};
|
||||
stats?: {
|
||||
min: number;
|
||||
max: number;
|
||||
avg: number;
|
||||
total: number;
|
||||
loss: number;
|
||||
rcv: number;
|
||||
drop: number;
|
||||
};
|
||||
rawOutput?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IGlobalPingService {
|
||||
readonly serviceName: string;
|
||||
createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null>;
|
||||
createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null>;
|
||||
pollForResults(measurementId: string, timeoutMs?: number): Promise<GeoCheckResult[]>;
|
||||
}
|
||||
|
||||
@@ -67,13 +78,16 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
return GlobalPingService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async createMeasurement(url: string, locations: GeoContinent[]): Promise<string | null> {
|
||||
async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise<string | null> {
|
||||
try {
|
||||
if (!supportsGeoCheck(monitorType)) {
|
||||
throw new Error(`Unsupported monitor type for GlobalPing: ${monitorType}`);
|
||||
}
|
||||
// GlobalPing API expects target without protocol (http:// or https://)
|
||||
const cleanTarget = url.replace(/^https?:\/\//, "");
|
||||
|
||||
const requestBody: GlobalPingMeasurementRequest = {
|
||||
type: "http",
|
||||
type: monitorType,
|
||||
target: cleanTarget,
|
||||
locations: locations.map((continent) => ({ continent })),
|
||||
limit: locations.length,
|
||||
@@ -88,10 +102,9 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
const measurementId = response.body.id;
|
||||
|
||||
this.logger.debug({
|
||||
message: `Created GlobalPing measurement: ${measurementId}`,
|
||||
message: `Created GlobalPing measurement: ${measurementId} for target: ${cleanTarget}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "createMeasurement",
|
||||
details: { measurementId, url, locations },
|
||||
});
|
||||
|
||||
return measurementId;
|
||||
@@ -165,8 +178,7 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
const successfulResults: GeoCheckResult[] = [];
|
||||
|
||||
for (const probeResult of probeResults) {
|
||||
// Skip failed or timeout results
|
||||
if (probeResult.result.status !== "finished" || !probeResult.result.statusCode || !probeResult.result.timings) {
|
||||
if (probeResult.result.status !== "finished") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -180,23 +192,38 @@ export class GlobalPingService implements IGlobalPingService {
|
||||
latitude: probeResult.probe.latitude,
|
||||
};
|
||||
|
||||
const timings: GeoCheckTimings = {
|
||||
total: probeResult.result.timings.total,
|
||||
dns: probeResult.result.timings.dns,
|
||||
tcp: probeResult.result.timings.tcp,
|
||||
tls: probeResult.result.timings.tls,
|
||||
firstByte: probeResult.result.timings.firstByte,
|
||||
download: probeResult.result.timings.download,
|
||||
};
|
||||
// HTTP results have statusCode and timings, ping results have stats
|
||||
if (probeResult.result.statusCode && probeResult.result.timings) {
|
||||
const timings: GeoCheckTimings = {
|
||||
total: probeResult.result.timings.total,
|
||||
dns: probeResult.result.timings.dns,
|
||||
tcp: probeResult.result.timings.tcp,
|
||||
tls: probeResult.result.timings.tls,
|
||||
firstByte: probeResult.result.timings.firstByte,
|
||||
download: probeResult.result.timings.download,
|
||||
};
|
||||
|
||||
const result: GeoCheckResult = {
|
||||
location,
|
||||
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
||||
statusCode: probeResult.result.statusCode,
|
||||
timings,
|
||||
};
|
||||
|
||||
successfulResults.push(result);
|
||||
successfulResults.push({
|
||||
location,
|
||||
status: probeResult.result.statusCode >= 200 && probeResult.result.statusCode < 300,
|
||||
statusCode: probeResult.result.statusCode,
|
||||
timings,
|
||||
});
|
||||
} else if (probeResult.result.stats) {
|
||||
successfulResults.push({
|
||||
location,
|
||||
status: probeResult.result.stats.loss === 0,
|
||||
statusCode: probeResult.result.stats.loss === 0 ? 200 : 5000,
|
||||
timings: {
|
||||
total: probeResult.result.stats.avg,
|
||||
dns: 0,
|
||||
tcp: 0,
|
||||
tls: 0,
|
||||
firstByte: 0,
|
||||
download: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return successfulResults;
|
||||
|
||||
@@ -6,6 +6,9 @@ export type { GeoContinent } from "@/types/geoCheck.js";
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const GeoCheckSupportedTypes: readonly MonitorType[] = ["http", "ping"] as const;
|
||||
export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupportedTypes.includes(type);
|
||||
|
||||
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
|
||||
Reference in new issue
Block a user