implement port monitoring
This commit is contained in:
1 parent
caa863afe8
commit
5d825537ae
10 files changed
+195
-47
No files matched your search
+1
-1
@@ -5,7 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"i18n:display": "i18n-unused display-unused > test.log",
|
||||
"dev": "vite --mode development",
|
||||
"dev": "vite --host 127.0.0.1 --mode development",
|
||||
"build": "tsc -b && vite build --mode production",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
|
||||
@@ -20,6 +20,7 @@ export const useInitForm = ({
|
||||
const defaults: z.infer<typeof monitorSchema> = {
|
||||
type: initialData?.type || "https",
|
||||
url: initialData?.url || "",
|
||||
port: initialData?.port || 80,
|
||||
n: initialData?.n || 3,
|
||||
notificationChannels: initialData?.notificationChannels || [],
|
||||
name: initialData?.name || "",
|
||||
|
||||
@@ -132,6 +132,11 @@ export const UptimeForm = ({
|
||||
"Use HTTP to monitor your website or API endpoint."
|
||||
}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="port"
|
||||
label={"Port"}
|
||||
description={"Check whether your port is open or not."}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="ping"
|
||||
label={t("pingMonitoring")}
|
||||
@@ -174,6 +179,22 @@ export const UptimeForm = ({
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{selectedType === "port" && (
|
||||
<Controller
|
||||
name="port"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
type="number"
|
||||
fieldLabel={"Port"}
|
||||
fullWidth
|
||||
error={!!errors.port}
|
||||
helperText={errors.port ? errors.port.message : ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
|
||||
@@ -4,13 +4,14 @@ import type { IMonitorStats } from "./monitorStats";
|
||||
export const MonitorTypes = [
|
||||
"http",
|
||||
"https",
|
||||
"port",
|
||||
"ping",
|
||||
"infrastructure",
|
||||
"pagespeed",
|
||||
] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const UptimeMonitorTypes = ["http", "https", "ping"] as const;
|
||||
export const UptimeMonitorTypes = ["http", "https", "port", "ping"] as const;
|
||||
export type UptimeMonitorType = (typeof UptimeMonitorTypes)[number];
|
||||
|
||||
export const MonitorStatuses = [
|
||||
|
||||
@@ -88,17 +88,50 @@ const optionalString = <T extends z.ZodTypeAny>(schema: T) =>
|
||||
schema.optional()
|
||||
);
|
||||
|
||||
export const monitorSchema = z.object({
|
||||
type: z.string().min(1, "You must select an option"),
|
||||
url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"),
|
||||
n: z.coerce
|
||||
.number({ message: "Number required" })
|
||||
.min(1, "Minimum value is 1")
|
||||
.max(25, "Maximum value is 25"),
|
||||
notificationChannels: z.array(z.string()).optional().default([]),
|
||||
name: z.string().min(1, "Display name is required"),
|
||||
interval: durationSchema,
|
||||
});
|
||||
export const monitorSchema = z
|
||||
.object({
|
||||
type: z.string().min(1, "You must select an option"),
|
||||
url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"),
|
||||
n: z.coerce
|
||||
.number({ message: "Number required" })
|
||||
.min(1, "Minimum value is 1")
|
||||
.max(25, "Maximum value is 25"),
|
||||
port: z.coerce.number().optional(),
|
||||
notificationChannels: z.array(z.string()).optional().default([]),
|
||||
name: z.string().min(1, "Display name is required"),
|
||||
interval: durationSchema,
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.type === "port") {
|
||||
if (!data.port) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Port is required for port monitoring",
|
||||
path: ["port"],
|
||||
});
|
||||
}
|
||||
if (isNaN(Number(data.port))) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Port must be a number",
|
||||
path: ["port"],
|
||||
});
|
||||
}
|
||||
if (Number(data.port) < 1 || Number(data.port) > 65535) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Port must be between 1 and 65535",
|
||||
path: ["port"],
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.type !== "port") {
|
||||
return { ...data, port: undefined };
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
export const monitorSchemaPageSpeed = z.object({
|
||||
type: z.string().min(1, "You must select an option"),
|
||||
@@ -212,13 +245,9 @@ export const maintenanceSchema = z
|
||||
|
||||
export const systemSettingsSchema = z.object({
|
||||
systemEmailHost: optionalString(
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, { message: "SMTP host is required" })
|
||||
.max(255, {
|
||||
message: "SMTP host must be 255 characters or fewer",
|
||||
})
|
||||
z.string().trim().min(1, { message: "SMTP host is required" }).max(255, {
|
||||
message: "SMTP host must be 255 characters or fewer",
|
||||
})
|
||||
),
|
||||
systemEmailPort: optionalString(
|
||||
z
|
||||
@@ -237,35 +266,24 @@ export const systemSettingsSchema = z.object({
|
||||
z.string().email({ message: "SMTP email must be valid" })
|
||||
),
|
||||
systemEmailPassword: optionalString(
|
||||
z
|
||||
.string()
|
||||
.max(255, {
|
||||
message: "SMTP password must be 255 characters or fewer",
|
||||
})
|
||||
z.string().max(255, {
|
||||
message: "SMTP password must be 255 characters or fewer",
|
||||
})
|
||||
),
|
||||
systemEmailUser: optionalString(
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.max(255, {
|
||||
message: "SMTP username must be 255 characters or fewer",
|
||||
})
|
||||
z.string().trim().max(255, {
|
||||
message: "SMTP username must be 255 characters or fewer",
|
||||
})
|
||||
),
|
||||
systemEmailConnectionHost: optionalString(
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.max(255, {
|
||||
message: "Connection host must be 255 characters or fewer",
|
||||
})
|
||||
z.string().trim().max(255, {
|
||||
message: "Connection host must be 255 characters or fewer",
|
||||
})
|
||||
),
|
||||
systemEmailTLSServername: optionalString(
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.max(255, {
|
||||
message: "TLS server name must be 255 characters or fewer",
|
||||
})
|
||||
z.string().trim().max(255, {
|
||||
message: "TLS server name must be 255 characters or fewer",
|
||||
})
|
||||
),
|
||||
systemEmailSecure: z.boolean().optional(),
|
||||
systemEmailPool: z.boolean().optional(),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Check, MonitorStats } from "@/db/models/index.js";
|
||||
export const MonitorTypes = [
|
||||
"http",
|
||||
"https",
|
||||
"port",
|
||||
"ping",
|
||||
"infrastructure",
|
||||
"pagespeed",
|
||||
@@ -17,12 +18,14 @@ export const MonitorStatuses = [
|
||||
"initializing",
|
||||
] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export interface IMonitor extends Document {
|
||||
_id: Types.ObjectId;
|
||||
orgId: Types.ObjectId;
|
||||
teamId: Types.ObjectId;
|
||||
name: string;
|
||||
url: string;
|
||||
port?: number;
|
||||
secret?: string;
|
||||
type: MonitorType;
|
||||
interval: number; // in ms
|
||||
@@ -47,6 +50,7 @@ const MonitorSchema = new Schema<IMonitor>(
|
||||
teamId: { type: Schema.Types.ObjectId, ref: "Team", required: true },
|
||||
name: { type: String, required: true, trim: true, maxlength: 100 },
|
||||
url: { type: String, required: true, trim: true },
|
||||
port: { type: Number, required: false },
|
||||
secret: { type: String, required: false },
|
||||
type: {
|
||||
type: String,
|
||||
|
||||
@@ -188,6 +188,8 @@ class CheckService implements ICheckService {
|
||||
|
||||
case "ping":
|
||||
return this.buildBaseCheck(statusResponse);
|
||||
case "port":
|
||||
return this.buildBaseCheck(statusResponse);
|
||||
default:
|
||||
throw new Error(`Unsupported monitor type: ${type}`);
|
||||
}
|
||||
|
||||
@@ -657,7 +657,7 @@ class MonitorService implements IMonitorService {
|
||||
|
||||
export = async (teamId: string) => {
|
||||
return Monitor.find({ teamId })
|
||||
.select("name url type interval n secret -_id")
|
||||
.select("name url port type interval n secret -_id")
|
||||
.lean();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Got, HTTPError } from "got";
|
||||
import ping from "ping";
|
||||
import net from "net";
|
||||
import { IMonitor } from "@/db/models/index.js";
|
||||
import { GotTimings } from "@/db/models/checks/Check.js";
|
||||
import type { Response } from "got";
|
||||
@@ -12,16 +13,16 @@ import { MonitorType, MonitorStatus } from "@/db/models/monitors/Monitor.js";
|
||||
import ApiError from "@/utils/ApiError.js";
|
||||
import { config } from "@/config/index.js";
|
||||
import CacheableLookup from "cacheable-lookup";
|
||||
import http from "http";
|
||||
import https from "https";
|
||||
|
||||
import { getChildLogger } from "@/logger/Logger.js";
|
||||
const SERVICE_NAME = "NetworkService";
|
||||
const logger = getChildLogger(SERVICE_NAME);
|
||||
export interface INetworkService {
|
||||
requestHttp: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
requestInfrastructure: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
requestStatus: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
requestPagespeed: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
requestPing: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
requestPort: (monitor: IMonitor) => Promise<StatusResponse>;
|
||||
}
|
||||
|
||||
export interface ICapturePayload {
|
||||
@@ -216,6 +217,78 @@ class NetworkService implements INetworkService {
|
||||
};
|
||||
};
|
||||
|
||||
requestPort = async (monitor: IMonitor) => {
|
||||
const response = {
|
||||
monitorId: monitor._id.toString(),
|
||||
teamId: monitor.teamId.toString(),
|
||||
type: monitor.type,
|
||||
status: "down" as MonitorStatus,
|
||||
message: "Port check failed",
|
||||
responseTime: 0,
|
||||
timings: { phases: {} } as GotTimings,
|
||||
};
|
||||
|
||||
try {
|
||||
const host = monitor.url;
|
||||
const port = monitor.port;
|
||||
if (!port) {
|
||||
throw new Error("No port provided for port monitor");
|
||||
}
|
||||
|
||||
const { status, responseTime, message } = await new Promise<{
|
||||
status: MonitorStatus;
|
||||
responseTime: number;
|
||||
message: string;
|
||||
}>((resolve) => {
|
||||
const start = performance.now();
|
||||
const socket = net.createConnection({
|
||||
host,
|
||||
port,
|
||||
});
|
||||
|
||||
socket.setTimeout(5000);
|
||||
|
||||
socket.on("connect", () => {
|
||||
socket.destroy();
|
||||
const duration = performance.now() - start;
|
||||
resolve({
|
||||
status: "up",
|
||||
responseTime: duration,
|
||||
message: "Port is open",
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
socket.destroy();
|
||||
const duration = performance.now() - start;
|
||||
resolve({
|
||||
status: "down",
|
||||
responseTime: duration,
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("timeout", () => {
|
||||
socket.destroy();
|
||||
const duration = performance.now() - start;
|
||||
resolve({
|
||||
status: "down",
|
||||
responseTime: duration,
|
||||
message: "Port check timed out",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
response.status = status;
|
||||
response.responseTime = responseTime;
|
||||
response.message = message;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
requestStatus = async (monitor: IMonitor) => {
|
||||
switch (monitor?.type) {
|
||||
case "http":
|
||||
@@ -228,6 +301,8 @@ class NetworkService implements INetworkService {
|
||||
return await this.requestPagespeed(monitor); // uses GOT
|
||||
case "ping":
|
||||
return await this.requestPing(monitor); // uses PING
|
||||
case "port":
|
||||
return await this.requestPort(monitor);
|
||||
default:
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export const monitorSchema = z
|
||||
type: z.string().min(1, "You must select an option"),
|
||||
url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"),
|
||||
name: z.string().min(1, "Display name is required"),
|
||||
port: z.coerce.number().optional(),
|
||||
n: z
|
||||
.number({ message: "Number required" })
|
||||
.min(1, "Minimum value is 1")
|
||||
@@ -81,6 +82,30 @@ export const monitorSchema = z
|
||||
message: `Secret is required for infrastructure monitors`,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.type === "port") {
|
||||
if (!data.port) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Port is required for port monitoring",
|
||||
path: ["port"],
|
||||
});
|
||||
}
|
||||
if (isNaN(Number(data.port))) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Port must be a number",
|
||||
path: ["port"],
|
||||
});
|
||||
}
|
||||
if (Number(data.port) < 1 || Number(data.port) > 65535) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Port must be between 1 and 65535",
|
||||
path: ["port"],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const monitorImportSchema = z.object({
|
||||
@@ -90,6 +115,7 @@ export const monitorImportSchema = z.object({
|
||||
.object({
|
||||
name: z.string().trim().min(1, "Monitor name is required"),
|
||||
url: z.string().trim().regex(urlRegex, "Invalid URL"),
|
||||
port: z.number().min(1).max(65535).optional(),
|
||||
secret: z.string().trim().optional(),
|
||||
type: z.enum(MonitorTypes),
|
||||
interval: z.number().min(1, "Interval must be greater than 0"),
|
||||
|
||||
Reference in new issue
Block a user