Merge pull request #3304 from bluewave-labs/feat/grpc-monitoring
Feat: gRPC Monitoring
This commit is contained in:
19 files changed
+329
-118
No files matched your search
@@ -7,7 +7,15 @@ import type { MonitorType } from "@/Types/Monitor";
|
||||
import { Typography, useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const types = ["http", "ping", "port", "docker", "game"];
|
||||
const types = ["http", "ping", "port", "docker", "game", "grpc"];
|
||||
const typeDisplayNames: Record<string, string> = {
|
||||
http: "HTTP",
|
||||
ping: "Ping",
|
||||
port: "Port",
|
||||
docker: "Docker",
|
||||
game: "Game",
|
||||
grpc: "gRPC",
|
||||
};
|
||||
const statuses = ["up", "down"];
|
||||
const states = ["active", "paused"];
|
||||
|
||||
@@ -52,7 +60,7 @@ export const ControlsFilter = ({
|
||||
key={type}
|
||||
value={type}
|
||||
>
|
||||
<Typography textTransform={"capitalize"}>{type}</Typography>
|
||||
<Typography>{typeDisplayNames[type] ?? type}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -73,6 +73,16 @@ export const useMonitorForm = ({
|
||||
gameId: data?.gameId || "",
|
||||
};
|
||||
break;
|
||||
case "grpc":
|
||||
defaults = {
|
||||
...base,
|
||||
type: "grpc",
|
||||
url: data?.url || "",
|
||||
port: data?.port || 50051,
|
||||
grpcServiceName: data?.grpcServiceName || "",
|
||||
ignoreTlsErrors: data?.ignoreTlsErrors || false,
|
||||
};
|
||||
break;
|
||||
case "pagespeed":
|
||||
defaults = {
|
||||
...base,
|
||||
|
||||
@@ -42,6 +42,8 @@ interface GeneralSettingsConfig {
|
||||
showPort: boolean;
|
||||
showGameSelect: boolean;
|
||||
showSecret: boolean;
|
||||
showGrpcServiceName: boolean;
|
||||
showIgnoreTls: boolean;
|
||||
}
|
||||
|
||||
const getGeneralSettingsConfig = (
|
||||
@@ -57,6 +59,8 @@ const getGeneralSettingsConfig = (
|
||||
showPort: false,
|
||||
showGameSelect: false,
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
},
|
||||
ping: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
||||
@@ -66,6 +70,8 @@ const getGeneralSettingsConfig = (
|
||||
showPort: false,
|
||||
showGameSelect: false,
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
},
|
||||
docker: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.container.label"),
|
||||
@@ -75,6 +81,8 @@ const getGeneralSettingsConfig = (
|
||||
showPort: false,
|
||||
showGameSelect: false,
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
},
|
||||
port: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -84,6 +92,8 @@ const getGeneralSettingsConfig = (
|
||||
showPort: true,
|
||||
showGameSelect: false,
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
},
|
||||
game: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -93,6 +103,19 @@ const getGeneralSettingsConfig = (
|
||||
showPort: true,
|
||||
showGameSelect: true,
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
},
|
||||
grpc: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
||||
urlPlaceholder: t("pages.createMonitor.form.general.option.host.placeholder"),
|
||||
namePlaceholder: t("pages.createMonitor.form.general.option.name.placeholder"),
|
||||
showUrl: true,
|
||||
showPort: true,
|
||||
showGameSelect: false,
|
||||
showSecret: false,
|
||||
showGrpcServiceName: true,
|
||||
showIgnoreTls: true,
|
||||
},
|
||||
pagespeed: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -102,6 +125,8 @@ const getGeneralSettingsConfig = (
|
||||
showPort: false,
|
||||
showGameSelect: false,
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
},
|
||||
hardware: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -111,6 +136,8 @@ const getGeneralSettingsConfig = (
|
||||
showPort: false,
|
||||
showGameSelect: false,
|
||||
showSecret: true,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
},
|
||||
};
|
||||
return configs[type] || configs.http;
|
||||
@@ -290,6 +317,13 @@ const CreateMonitorPage = () => {
|
||||
"pages.createMonitor.form.type.optionGameDescription"
|
||||
)}
|
||||
/>
|
||||
<RadioWithDescription
|
||||
value="grpc"
|
||||
label={t("pages.createMonitor.form.type.optionGrpc")}
|
||||
description={t(
|
||||
"pages.createMonitor.form.type.optionGrpcDescription"
|
||||
)}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
)}
|
||||
@@ -378,6 +412,30 @@ const CreateMonitorPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* gRPC Service Name field - only for grpc type */}
|
||||
{generalSettingsConfig.showGrpcServiceName && (
|
||||
<Controller
|
||||
name="grpcServiceName"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
type="text"
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.general.option.grpcServiceName.label"
|
||||
)}
|
||||
placeholder={t(
|
||||
"pages.createMonitor.form.general.option.grpcServiceName.placeholder"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Secret field - only for hardware type */}
|
||||
{generalSettingsConfig.showSecret && (
|
||||
<Controller
|
||||
@@ -682,7 +740,7 @@ const CreateMonitorPage = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{watchedType === "http" && (
|
||||
{(watchedType === "http" || watchedType === "grpc") && (
|
||||
<ConfigBox
|
||||
title={t("pages.createMonitor.form.ignoreTls.title")}
|
||||
subtitle={t("pages.createMonitor.form.ignoreTls.description")}
|
||||
|
||||
@@ -68,7 +68,9 @@ const UptimeMonitorsPage = () => {
|
||||
|
||||
// Default to all types when none selected
|
||||
const effectiveTypes =
|
||||
selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port", "game"];
|
||||
selectedTypes.length > 0
|
||||
? selectedTypes
|
||||
: ["http", "ping", "docker", "port", "game", "grpc"];
|
||||
|
||||
// Build URL for monitors with checks
|
||||
const monitorsWithChecksUrl = useMemo(() => {
|
||||
|
||||
@@ -11,6 +11,7 @@ export const MonitorTypes = [
|
||||
"docker",
|
||||
"port",
|
||||
"game",
|
||||
"grpc",
|
||||
"unknown",
|
||||
] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
@@ -60,6 +61,7 @@ export interface Monitor {
|
||||
tempAlertCounter: number;
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
group: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
|
||||
@@ -8,6 +8,7 @@ export const getMonitorPath = (type: MonitorType): string => {
|
||||
port: "uptime",
|
||||
ping: "uptime",
|
||||
game: "uptime",
|
||||
grpc: "uptime",
|
||||
unknown: "uptime",
|
||||
docker: "uptime",
|
||||
hardware: "infrastructure",
|
||||
|
||||
@@ -73,6 +73,18 @@ const gameSchema = baseSchema.extend({
|
||||
gameId: z.string().min(1, "Game type is required"),
|
||||
});
|
||||
|
||||
// gRPC monitor schema
|
||||
const grpcSchema = baseSchema.extend({
|
||||
type: z.literal("grpc"),
|
||||
url: z.string().min(1, "Host is required"),
|
||||
port: z
|
||||
.number()
|
||||
.min(1, "Port must be at least 1")
|
||||
.max(65535, "Port must be at most 65535"),
|
||||
grpcServiceName: z.string().optional(),
|
||||
ignoreTlsErrors: z.boolean(),
|
||||
});
|
||||
|
||||
// PageSpeed monitor schema
|
||||
const pagespeedSchema = baseSchema.extend({
|
||||
type: z.literal("pagespeed"),
|
||||
@@ -110,6 +122,7 @@ export const monitorSchema = z.discriminatedUnion("type", [
|
||||
portSchema,
|
||||
dockerSchema,
|
||||
gameSchema,
|
||||
grpcSchema,
|
||||
pagespeedSchema,
|
||||
hardwareSchema,
|
||||
]);
|
||||
@@ -123,6 +136,7 @@ export {
|
||||
portSchema,
|
||||
dockerSchema,
|
||||
gameSchema,
|
||||
grpcSchema,
|
||||
pagespeedSchema,
|
||||
hardwareSchema,
|
||||
};
|
||||
@@ -483,6 +483,10 @@
|
||||
"port": {
|
||||
"label": "Port to monitor",
|
||||
"placeholder": 80
|
||||
},
|
||||
"grpcServiceName": {
|
||||
"label": "Service name",
|
||||
"placeholder": "e.g. my.service.v1 (leave empty for overall health)"
|
||||
}
|
||||
},
|
||||
"title": "General settings",
|
||||
@@ -493,6 +497,7 @@
|
||||
"docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).",
|
||||
"game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.",
|
||||
"pagespeed": "Track page load performance, Core Web Vitals, and optimization scores for your website.",
|
||||
"grpc": "Enter the hostname and port of the gRPC server, optionally specify a service name for the health check, and add a display name.",
|
||||
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
|
||||
}
|
||||
},
|
||||
@@ -527,6 +532,8 @@
|
||||
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
||||
"optionGame": "Game",
|
||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||
"optionGrpc": "gRPC",
|
||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||
"optionHttp": "HTTP(S)",
|
||||
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
||||
"optionPing": "Ping",
|
||||
|
||||
Generated
+24
-103
@@ -9,6 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@pulsecron/pulse": "1.6.8",
|
||||
"axios": "^1.7.2",
|
||||
"bcryptjs": "3.0.2",
|
||||
@@ -874,7 +876,6 @@
|
||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/generator": "^7.28.6",
|
||||
@@ -1504,7 +1505,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -1549,7 +1549,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -2198,9 +2197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz",
|
||||
"integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==",
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
|
||||
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
@@ -2210,7 +2209,7 @@
|
||||
"node": ">=12.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": {
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
|
||||
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
|
||||
@@ -2228,24 +2227,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
||||
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.2.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/hoek": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
|
||||
@@ -4223,16 +4204,6 @@
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
@@ -4405,7 +4376,6 @@
|
||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^5.0.0",
|
||||
@@ -4566,7 +4536,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -5053,7 +5022,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5632,7 +5600,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -6575,7 +6542,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
|
||||
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssnano-preset-default": "^7.0.9",
|
||||
"lilconfig": "^3.1.3"
|
||||
@@ -6948,6 +6914,24 @@
|
||||
"node": ">= 8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dockerode/node_modules/@grpc/proto-loader": {
|
||||
"version": "0.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
|
||||
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.2.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/dockerode/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
@@ -7330,7 +7314,6 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -7692,7 +7675,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -9341,7 +9323,6 @@
|
||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "30.2.0",
|
||||
"@jest/types": "30.2.0",
|
||||
@@ -12231,7 +12212,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -14173,63 +14153,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
|
||||
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@trysound/sax": "0.2.0",
|
||||
"commander": "^7.2.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^2.3.1",
|
||||
"css-what": "^6.1.0",
|
||||
"csso": "^5.0.5",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"svgo": "bin/svgo"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/svgo"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo/node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo/node_modules/css-tree": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svgo/node_modules/mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
|
||||
"license": "CC0-1.0",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.30.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz",
|
||||
@@ -14513,7 +14436,6 @@
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
@@ -14676,7 +14598,6 @@
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.14.3",
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@pulsecron/pulse": "1.6.8",
|
||||
"axios": "^1.7.2",
|
||||
"bcryptjs": "3.0.2",
|
||||
|
||||
@@ -43,6 +43,8 @@ import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
import { games, GameDig } from "gamedig";
|
||||
import jmespath from "jmespath";
|
||||
import * as grpc from "@grpc/grpc-js";
|
||||
import * as protoLoader from "@grpc/proto-loader";
|
||||
|
||||
// repositories
|
||||
import {
|
||||
@@ -143,7 +145,7 @@ export const initializeServices = async ({
|
||||
const teamsRepository = new MongoTeamsRepository();
|
||||
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||
|
||||
const networkService = new NetworkService(axios, got, https, jmespath, GameDig, ping, logger, Docker, net, settingsService);
|
||||
const networkService = new NetworkService(axios, got, https, jmespath, GameDig, ping, logger, Docker, net, settingsService, grpc, protoLoader);
|
||||
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
|
||||
|
||||
const notificationMessageBuilder = new NotificationMessageBuilder();
|
||||
|
||||
@@ -160,6 +160,10 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
gameId: {
|
||||
type: String,
|
||||
},
|
||||
grpcServiceName: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
trim: true,
|
||||
|
||||
@@ -352,6 +352,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
tempAlertCounter: doc.tempAlertCounter,
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
@@ -410,6 +411,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
tempAlertCounter: doc.tempAlertCounter,
|
||||
selectedDisks: doc.selectedDisks ?? [],
|
||||
gameId: doc.gameId ?? undefined,
|
||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)),
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
|
||||
@@ -225,7 +225,8 @@ export class MonitorService implements IMonitorService {
|
||||
checksData.monitorType !== "ping" &&
|
||||
checksData.monitorType !== "docker" &&
|
||||
checksData.monitorType !== "port" &&
|
||||
checksData.monitorType !== "game"
|
||||
checksData.monitorType !== "game" &&
|
||||
checksData.monitorType !== "grpc"
|
||||
) {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { HTTPError, RequestError } from "got";
|
||||
import type { Got, Response } from "got";
|
||||
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
|
||||
import type { Monitor, MonitorStatusResponse, GrpcStatusPayload } from "@/types/index.js";
|
||||
import type { AxiosStatic } from "axios";
|
||||
import type Docker from "dockerode";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import CacheableLookup from "cacheable-lookup";
|
||||
import { ISettingsService } from "../system/settingsService.js";
|
||||
@@ -46,6 +48,7 @@ class NetworkService implements INetworkService {
|
||||
private TYPE_DOCKER: string;
|
||||
private TYPE_PORT: string;
|
||||
private TYPE_GAME: string;
|
||||
private TYPE_GRPC: string;
|
||||
private SERVICE_NAME: string;
|
||||
private NETWORK_ERROR: number;
|
||||
private PING_ERROR: number;
|
||||
@@ -60,6 +63,8 @@ class NetworkService implements INetworkService {
|
||||
private Docker: any;
|
||||
private net: any;
|
||||
private settingsService: ISettingsService;
|
||||
private grpc: any;
|
||||
private protoLoader: any;
|
||||
|
||||
private buildStatusResponse = <T>({
|
||||
monitor,
|
||||
@@ -125,7 +130,9 @@ class NetworkService implements INetworkService {
|
||||
logger: ILogger,
|
||||
Docker: any,
|
||||
net: any,
|
||||
settingsService: ISettingsService
|
||||
settingsService: ISettingsService,
|
||||
grpc: any,
|
||||
protoLoader: any
|
||||
) {
|
||||
this.TYPE_PING = "ping";
|
||||
this.TYPE_HTTP = "http";
|
||||
@@ -134,6 +141,7 @@ class NetworkService implements INetworkService {
|
||||
this.TYPE_DOCKER = "docker";
|
||||
this.TYPE_PORT = "port";
|
||||
this.TYPE_GAME = "game";
|
||||
this.TYPE_GRPC = "grpc";
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.NETWORK_ERROR = 5000;
|
||||
this.PING_ERROR = 5001;
|
||||
@@ -146,6 +154,8 @@ class NetworkService implements INetworkService {
|
||||
this.Docker = Docker;
|
||||
this.net = net;
|
||||
this.settingsService = settingsService;
|
||||
this.grpc = grpc;
|
||||
this.protoLoader = protoLoader;
|
||||
|
||||
const cacheable = new CacheableLookup();
|
||||
|
||||
@@ -193,6 +203,8 @@ class NetworkService implements INetworkService {
|
||||
return await this.requestPort(monitor);
|
||||
case this.TYPE_GAME:
|
||||
return await this.requestGame(monitor);
|
||||
case this.TYPE_GRPC:
|
||||
return await this.requestGrpc(monitor);
|
||||
default:
|
||||
return this.handleUnsupportedType(type);
|
||||
}
|
||||
@@ -618,6 +630,139 @@ class NetworkService implements INetworkService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
private async requestGrpc(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
try {
|
||||
const { url, port, ignoreTlsErrors } = monitor;
|
||||
const grpcServiceName = monitor.grpcServiceName || "";
|
||||
|
||||
if (!url) {
|
||||
throw new AppError({ message: "Monitor host is required", service: this.SERVICE_NAME, method: "requestGrpc" });
|
||||
}
|
||||
if (!port) {
|
||||
throw new AppError({ message: "Monitor port is required", service: this.SERVICE_NAME, method: "requestGrpc" });
|
||||
}
|
||||
|
||||
const target = `${url}:${port}`;
|
||||
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto");
|
||||
|
||||
const packageDefinition = this.protoLoader.loadSync(protoPath, {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true,
|
||||
});
|
||||
const grpcObject = this.grpc.loadPackageDefinition(packageDefinition);
|
||||
const healthService = grpcObject.grpc.health.v1.Health;
|
||||
|
||||
let credentials;
|
||||
if (ignoreTlsErrors) {
|
||||
credentials = this.grpc.credentials.createSsl(null, null, null, {
|
||||
checkServerIdentity: () => undefined,
|
||||
});
|
||||
} else {
|
||||
credentials = this.grpc.credentials.createInsecure();
|
||||
}
|
||||
|
||||
const client = new healthService(target, credentials);
|
||||
|
||||
const TIMEOUT_MS = 10000;
|
||||
const deadline = new Date(Date.now() + TIMEOUT_MS);
|
||||
|
||||
const grpcResponse = this.buildStatusResponse({
|
||||
monitor,
|
||||
overrides: {
|
||||
status: false,
|
||||
code: this.NETWORK_ERROR,
|
||||
message: "gRPC health check not executed",
|
||||
},
|
||||
});
|
||||
|
||||
const { response, responseTime, error } = await this.timeRequest<GrpcStatusPayload>(() => {
|
||||
return new Promise<GrpcStatusPayload>((resolve, reject) => {
|
||||
client.Check({ service: grpcServiceName }, { deadline }, (err: any, response: any) => {
|
||||
client.close();
|
||||
|
||||
if (err) {
|
||||
const payload: GrpcStatusPayload = {
|
||||
grpcStatusCode: err.code ?? -1,
|
||||
grpcStatusName: this.getGrpcStatusName(err.code),
|
||||
serviceName: grpcServiceName,
|
||||
servingStatus: "UNKNOWN",
|
||||
};
|
||||
const grpcError = new AppError({ message: err.details || err.message, service: this.SERVICE_NAME, method: "requestGrpc" });
|
||||
(grpcError as any).grpcPayload = payload;
|
||||
(grpcError as any).grpcCode = err.code;
|
||||
reject(grpcError);
|
||||
return;
|
||||
}
|
||||
|
||||
const servingStatus = response?.status || "UNKNOWN";
|
||||
resolve({
|
||||
grpcStatusCode: 0,
|
||||
grpcStatusName: "OK",
|
||||
serviceName: grpcServiceName,
|
||||
servingStatus,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const grpcError = error as any;
|
||||
const payload = grpcError.grpcPayload as GrpcStatusPayload | undefined;
|
||||
grpcResponse.status = false;
|
||||
grpcResponse.code = grpcError.grpcCode ?? this.NETWORK_ERROR;
|
||||
grpcResponse.message = grpcError.message || "gRPC health check failed";
|
||||
grpcResponse.responseTime = responseTime;
|
||||
grpcResponse.payload = payload || null;
|
||||
return grpcResponse;
|
||||
}
|
||||
|
||||
const grpcPayload = response as GrpcStatusPayload;
|
||||
const isServing = grpcPayload.servingStatus === "SERVING";
|
||||
|
||||
grpcResponse.status = isServing;
|
||||
grpcResponse.code = isServing ? 200 : this.NETWORK_ERROR;
|
||||
grpcResponse.message = isServing
|
||||
? `gRPC service healthy (${grpcPayload.servingStatus})`
|
||||
: `gRPC service unhealthy (${grpcPayload.servingStatus})`;
|
||||
grpcResponse.responseTime = responseTime;
|
||||
grpcResponse.payload = grpcPayload;
|
||||
|
||||
return grpcResponse;
|
||||
} catch (err: any) {
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestGrpc";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private getGrpcStatusName(code: number): string {
|
||||
const statusNames: Record<number, string> = {
|
||||
0: "OK",
|
||||
1: "CANCELLED",
|
||||
2: "UNKNOWN",
|
||||
3: "INVALID_ARGUMENT",
|
||||
4: "DEADLINE_EXCEEDED",
|
||||
5: "NOT_FOUND",
|
||||
6: "ALREADY_EXISTS",
|
||||
7: "PERMISSION_DENIED",
|
||||
8: "RESOURCE_EXHAUSTED",
|
||||
9: "FAILED_PRECONDITION",
|
||||
10: "ABORTED",
|
||||
11: "OUT_OF_RANGE",
|
||||
12: "UNIMPLEMENTED",
|
||||
13: "INTERNAL",
|
||||
14: "UNAVAILABLE",
|
||||
15: "DATA_LOSS",
|
||||
16: "UNAUTHENTICATED",
|
||||
};
|
||||
return statusNames[code] || "UNKNOWN";
|
||||
}
|
||||
|
||||
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
|
||||
return {
|
||||
monitorId: "unknown",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package grpc.health.v1;
|
||||
|
||||
message HealthCheckRequest {
|
||||
string service = 1;
|
||||
}
|
||||
|
||||
message HealthCheckResponse {
|
||||
enum ServingStatus {
|
||||
UNKNOWN = 0;
|
||||
SERVING = 1;
|
||||
NOT_SERVING = 2;
|
||||
SERVICE_UNKNOWN = 3;
|
||||
}
|
||||
ServingStatus status = 1;
|
||||
}
|
||||
|
||||
service Health {
|
||||
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ export type { CheckSnapshot } from "@/types/check.js";
|
||||
import type { GeoContinent } from "@/types/geoCheck.js";
|
||||
export type { GeoContinent } from "@/types/geoCheck.js";
|
||||
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const;
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const;
|
||||
@@ -44,6 +44,7 @@ export interface Monitor {
|
||||
tempAlertCounter: number;
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
group: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
|
||||
@@ -93,6 +93,13 @@ export interface PortStatusPayload {
|
||||
|
||||
export type GameStatusPayload = Record<string, unknown>;
|
||||
|
||||
export interface GrpcStatusPayload {
|
||||
grpcStatusCode: number;
|
||||
grpcStatusName: string;
|
||||
serviceName: string;
|
||||
servingStatus: string;
|
||||
}
|
||||
|
||||
export interface MonitorPayloadMap {
|
||||
ping: PingStatusPayload;
|
||||
http: HttpStatusPayload;
|
||||
@@ -101,6 +108,7 @@ export interface MonitorPayloadMap {
|
||||
docker: DockerStatusPayload;
|
||||
port: PortStatusPayload;
|
||||
game: GameStatusPayload;
|
||||
grpc: GrpcStatusPayload;
|
||||
default: unknown;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,8 +122,8 @@ const getMonitorsByTeamIdQueryValidation = joi.object({
|
||||
type: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"))
|
||||
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"))
|
||||
),
|
||||
filter: joi.string().allow("", null),
|
||||
});
|
||||
@@ -138,8 +138,8 @@ const getMonitorsWithChecksQueryValidation = joi.object({
|
||||
type: joi
|
||||
.alternatives()
|
||||
.try(
|
||||
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"))
|
||||
joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"),
|
||||
joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc"))
|
||||
)
|
||||
.optional(),
|
||||
explain: joi.boolean().optional(),
|
||||
@@ -172,6 +172,7 @@ const createMonitorBodyValidation = joi.object({
|
||||
expectedValue: joi.string().allow(""),
|
||||
matchMethod: joi.string().allow(null, ""),
|
||||
gameId: joi.string().allow(""),
|
||||
grpcServiceName: joi.string().allow("").default(""),
|
||||
selectedDisks: joi.array().items(joi.string()).optional(),
|
||||
group: joi.string().max(50).trim().allow(null, "").optional(),
|
||||
geoCheckEnabled: joi.boolean().optional(),
|
||||
@@ -209,6 +210,7 @@ const editMonitorBodyValidation = joi
|
||||
diskAlertThreshold: joi.number(),
|
||||
tempAlertThreshold: joi.number(),
|
||||
gameId: joi.string().allow(""),
|
||||
grpcServiceName: joi.string().allow(""),
|
||||
selectedDisks: joi.array().items(joi.string()).optional(),
|
||||
group: joi.string().max(50).trim().allow(null, "").optional(),
|
||||
geoCheckEnabled: joi.boolean().optional(),
|
||||
@@ -315,7 +317,7 @@ const getChecksParamValidation = joi.object({
|
||||
});
|
||||
|
||||
const getChecksQueryValidation = joi.object({
|
||||
type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port", "game"),
|
||||
type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc"),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
limit: joi.number(),
|
||||
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
|
||||
|
||||
Reference in new issue
Block a user