From dac78c7647758d4228bcca218d86cb7b17b33151 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Thu, 19 Feb 2026 12:40:03 +0530 Subject: [PATCH 1/9] Feat: Add gRPC to monitortypes --- server/package-lock.json | 127 +++++++----------------------------- server/package.json | 2 + server/src/types/monitor.ts | 3 +- 3 files changed, 28 insertions(+), 104 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index d89613ecf..30b610df5 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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" diff --git a/server/package.json b/server/package.json index c1ef2b8ec..9e005bca4 100755 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index ac7ca02d0..d254c4c9c 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,7 +1,7 @@ import type { CheckSnapshot } from "@/types/check.js"; export type { CheckSnapshot } from "@/types/check.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; @@ -42,6 +42,7 @@ export interface Monitor { tempAlertCounter: number; selectedDisks: string[]; gameId?: string; + grpcServiceName?: string; group: string | null; recentChecks: CheckSnapshot[]; createdAt: string; From 05f8e6a07e84e64c6a062040b51f2f9b12c2ee5f Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Thu, 19 Feb 2026 12:42:26 +0530 Subject: [PATCH 2/9] Feat: Add GrpcStatus to network and Mongoose schema --- server/src/db/models/Monitor.ts | 4 ++++ server/src/types/network.ts | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index a459cdf3e..30575d466 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -160,6 +160,10 @@ const MonitorSchema = new Schema( gameId: { type: String, }, + grpcServiceName: { + type: String, + default: "", + }, group: { type: String, trim: true, diff --git a/server/src/types/network.ts b/server/src/types/network.ts index d87f1ea4f..5a8b1c443 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -93,6 +93,13 @@ export interface PortStatusPayload { export type GameStatusPayload = Record; +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; } From 94a85f8bbb827cfe6a5aad6477536417e02b0309 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Thu, 19 Feb 2026 12:46:23 +0530 Subject: [PATCH 3/9] Feat: Impement requestGrpc in NetworkService --- .../service/infrastructure/networkService.ts | 149 +++++++++++++++++- .../infrastructure/protos/health.proto | 21 +++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 server/src/service/infrastructure/protos/health.proto diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index 92d269808..fa718e787 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,6 +1,8 @@ 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 path from "path"; +import { fileURLToPath } from "url"; import CacheableLookup from "cacheable-lookup"; const SERVICE_NAME = "NetworkService"; @@ -42,6 +44,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; @@ -56,6 +59,8 @@ class NetworkService implements INetworkService { private Docker: any; private net: any; private settingsService: any; + private grpc: any; + private protoLoader: any; private buildStatusResponse = ({ monitor, @@ -123,6 +128,8 @@ class NetworkService implements INetworkService { Docker, net, settingsService, + grpc, + protoLoader, }: { axios: any; got: Got; @@ -135,6 +142,8 @@ class NetworkService implements INetworkService { Docker: any; net: any; settingsService: any; + grpc: any; + protoLoader: any; }) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; @@ -143,6 +152,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; @@ -155,6 +165,8 @@ class NetworkService implements INetworkService { this.Docker = Docker; this.net = net; this.settingsService = settingsService; + this.grpc = grpc; + this.protoLoader = protoLoader; const cacheable = new CacheableLookup(); @@ -202,6 +214,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); } @@ -624,6 +638,139 @@ class NetworkService implements INetworkService { throw error; } } + private async requestGrpc(monitor: Monitor): Promise { + try { + const { url, port, ignoreTlsErrors } = monitor; + const grpcServiceName = monitor.grpcServiceName || ""; + + if (!url) { + throw new Error("Monitor host is required"); + } + if (!port) { + throw new Error("Monitor port is required"); + } + + 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(() => { + return new Promise((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 Error(err.details || err.message); + (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 = { + 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 { return { monitorId: "unknown", diff --git a/server/src/service/infrastructure/protos/health.proto b/server/src/service/infrastructure/protos/health.proto new file mode 100644 index 000000000..791354e64 --- /dev/null +++ b/server/src/service/infrastructure/protos/health.proto @@ -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); +} From c95db26437c4e839dde005e44ae19dce4b484597 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Thu, 19 Feb 2026 12:50:25 +0530 Subject: [PATCH 4/9] Feat: Update backend Joi validation --- server/src/config/services.ts | 4 ++++ server/src/validation/joi.ts | 12 +++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index ecb49ab40..7b19f2608 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -41,6 +41,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 { @@ -148,6 +150,8 @@ export const initializeServices = async ({ Docker, net, settingsService, + grpc, + protoLoader, }); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); diff --git a/server/src/validation/joi.ts b/server/src/validation/joi.ts index 912767587..227cbbf71 100755 --- a/server/src/validation/joi.ts +++ b/server/src/validation/joi.ts @@ -120,8 +120,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), }); @@ -136,8 +136,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(), @@ -170,6 +170,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(), }); @@ -201,6 +202,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(), }) @@ -301,7 +303,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"), From c06f81ef7663eddb5596a0c910a1976b4bb2d275 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Thu, 19 Feb 2026 12:52:32 +0530 Subject: [PATCH 5/9] Feat: Add gRPC to client validation --- client/src/Types/Monitor.ts | 2 ++ client/src/Validation/monitor.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 636e632e2..d51cdbfa7 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -9,6 +9,7 @@ export const MonitorTypes = [ "docker", "port", "game", + "grpc", "unknown", ] as const; export type MonitorType = (typeof MonitorTypes)[number]; @@ -58,6 +59,7 @@ export interface Monitor { tempAlertCounter: number; selectedDisks: string[]; gameId?: string; + grpcServiceName?: string; group: string | null; recentChecks: CheckSnapshot[]; createdAt: string; diff --git a/client/src/Validation/monitor.ts b/client/src/Validation/monitor.ts index 68a15fbec..f3968c8e1 100644 --- a/client/src/Validation/monitor.ts +++ b/client/src/Validation/monitor.ts @@ -66,6 +66,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"), @@ -103,6 +115,7 @@ export const monitorSchema = z.discriminatedUnion("type", [ portSchema, dockerSchema, gameSchema, + grpcSchema, pagespeedSchema, hardwareSchema, ]); @@ -116,6 +129,7 @@ export { portSchema, dockerSchema, gameSchema, + grpcSchema, pagespeedSchema, hardwareSchema, }; From 13aa600852a3d09ba09650a5335797927d734fcf Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Thu, 19 Feb 2026 12:56:48 +0530 Subject: [PATCH 6/9] Feat: Add gRPC to CreateMonitor UI --- client/src/Hooks/useMonitorForm.ts | 10 ++++ client/src/Pages/CreateMonitor/index.tsx | 60 +++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/client/src/Hooks/useMonitorForm.ts b/client/src/Hooks/useMonitorForm.ts index 633fcb5c9..0606f4363 100644 --- a/client/src/Hooks/useMonitorForm.ts +++ b/client/src/Hooks/useMonitorForm.ts @@ -70,6 +70,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, diff --git a/client/src/Pages/CreateMonitor/index.tsx b/client/src/Pages/CreateMonitor/index.tsx index 5012c1154..560b0fc27 100644 --- a/client/src/Pages/CreateMonitor/index.tsx +++ b/client/src/Pages/CreateMonitor/index.tsx @@ -41,6 +41,8 @@ interface GeneralSettingsConfig { showPort: boolean; showGameSelect: boolean; showSecret: boolean; + showGrpcServiceName: boolean; + showIgnoreTls: boolean; } const getGeneralSettingsConfig = ( @@ -56,6 +58,8 @@ const getGeneralSettingsConfig = ( showPort: false, showGameSelect: false, showSecret: false, + showGrpcServiceName: false, + showIgnoreTls: false, }, ping: { urlLabel: t("pages.createMonitor.form.general.option.host.label"), @@ -65,6 +69,8 @@ const getGeneralSettingsConfig = ( showPort: false, showGameSelect: false, showSecret: false, + showGrpcServiceName: false, + showIgnoreTls: false, }, docker: { urlLabel: t("pages.createMonitor.form.general.option.container.label"), @@ -74,6 +80,8 @@ const getGeneralSettingsConfig = ( showPort: false, showGameSelect: false, showSecret: false, + showGrpcServiceName: false, + showIgnoreTls: false, }, port: { urlLabel: t("pages.createMonitor.form.general.option.url.label"), @@ -83,6 +91,8 @@ const getGeneralSettingsConfig = ( showPort: true, showGameSelect: false, showSecret: false, + showGrpcServiceName: false, + showIgnoreTls: false, }, game: { urlLabel: t("pages.createMonitor.form.general.option.url.label"), @@ -92,6 +102,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"), @@ -101,6 +124,8 @@ const getGeneralSettingsConfig = ( showPort: false, showGameSelect: false, showSecret: false, + showGrpcServiceName: false, + showIgnoreTls: false, }, hardware: { urlLabel: t("pages.createMonitor.form.general.option.url.label"), @@ -110,6 +135,8 @@ const getGeneralSettingsConfig = ( showPort: false, showGameSelect: false, showSecret: true, + showGrpcServiceName: false, + showIgnoreTls: false, }, }; return configs[type] || configs.http; @@ -288,6 +315,13 @@ const CreateMonitorPage = () => { "pages.createMonitor.form.type.optionGameDescription" )} /> + )} @@ -376,6 +410,30 @@ const CreateMonitorPage = () => { /> )} + {/* gRPC Service Name field - only for grpc type */} + {generalSettingsConfig.showGrpcServiceName && ( + ( + + )} + /> + )} + {/* Secret field - only for hardware type */} {generalSettingsConfig.showSecret && ( { } /> - {watchedType === "http" && ( + {(watchedType === "http" || watchedType === "grpc") && ( Date: Thu, 19 Feb 2026 13:02:38 +0530 Subject: [PATCH 7/9] Feat: Add gRPC to filters --- client/src/Components/monitors/ControlsFilter.tsx | 12 ++++++++++-- client/src/Pages/Uptime/Monitors/index.tsx | 2 +- client/src/Utils/MonitorUtils.ts | 1 + client/src/locales/en.json | 7 +++++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/client/src/Components/monitors/ControlsFilter.tsx b/client/src/Components/monitors/ControlsFilter.tsx index e37408819..705435e6a 100644 --- a/client/src/Components/monitors/ControlsFilter.tsx +++ b/client/src/Components/monitors/ControlsFilter.tsx @@ -6,7 +6,15 @@ import useMediaQuery from "@mui/material/useMediaQuery"; import type { MonitorType } from "@/Types/Monitor"; import { Typography, useTheme } from "@mui/material"; -const types = ["http", "ping", "port", "docker", "game"]; +const types = ["http", "ping", "port", "docker", "game", "grpc"]; +const typeDisplayNames: Record = { + http: "HTTP", + ping: "Ping", + port: "Port", + docker: "Docker", + game: "Game", + grpc: "gRPC", +}; const statuses = ["up", "down"]; const states = ["active", "paused"]; @@ -50,7 +58,7 @@ export const ControlsFilter = ({ key={type} value={type} > - {type} + {typeDisplayNames[type] ?? type} ))} diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 5c419738b..4251e5edf 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -68,7 +68,7 @@ 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(() => { diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts index a19316c62..ad744ba6d 100644 --- a/client/src/Utils/MonitorUtils.ts +++ b/client/src/Utils/MonitorUtils.ts @@ -8,6 +8,7 @@ export const getMonitorPath = (type: MonitorType): string => { port: "uptime", ping: "uptime", game: "uptime", + grpc: "uptime", unknown: "uptime", docker: "uptime", hardware: "infrastructure", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index bb740cd13..6d45fadeb 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -481,6 +481,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", @@ -491,6 +495,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." } }, @@ -525,6 +530,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", From 22b33d789cad0b5355c64fdee39bb0e5df3a7344 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Thu, 19 Feb 2026 13:16:12 +0530 Subject: [PATCH 8/9] Fix: Add grpc in checkData --- client/src/Pages/Uptime/Monitors/index.tsx | 4 +++- server/src/service/business/monitorService.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 4251e5edf..e660a55f1 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -68,7 +68,9 @@ const UptimeMonitorsPage = () => { // Default to all types when none selected const effectiveTypes = - selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port", "game", "grpc"]; + selectedTypes.length > 0 + ? selectedTypes + : ["http", "ping", "docker", "port", "game", "grpc"]; // Build URL for monitors with checks const monitorsWithChecksUrl = useMemo(() => { diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 2005a4a5d..cafc65a93 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -218,7 +218,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 }); } From ea086d6956b45cd42c0beaaca984ee86d990441c Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Mon, 23 Feb 2026 10:19:42 +0530 Subject: [PATCH 9/9] Fix: PR comments --- .../src/repositories/monitors/MongoMonitorsRepository.ts | 2 ++ server/src/service/infrastructure/networkService.ts | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 4ae4394d4..5373e6c48 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -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)), createdAt: toDateString(doc.createdAt), @@ -407,6 +408,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)), createdAt: toDateString(doc.createdAt), diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index fa718e787..4b7cf938f 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,6 +1,7 @@ import { HTTPError, RequestError } from "got"; import type { Got, Response } from "got"; import type { Monitor, MonitorStatusResponse, GrpcStatusPayload } from "@/types/index.js"; +import { AppError } from "@/utils/AppError.js"; import path from "path"; import { fileURLToPath } from "url"; @@ -644,10 +645,10 @@ class NetworkService implements INetworkService { const grpcServiceName = monitor.grpcServiceName || ""; if (!url) { - throw new Error("Monitor host is required"); + throw new AppError({ message: "Monitor host is required", service: this.SERVICE_NAME, method: "requestGrpc" }); } if (!port) { - throw new Error("Monitor port is required"); + throw new AppError({ message: "Monitor port is required", service: this.SERVICE_NAME, method: "requestGrpc" }); } const target = `${url}:${port}`; @@ -700,7 +701,7 @@ class NetworkService implements INetworkService { serviceName: grpcServiceName, servingStatus: "UNKNOWN", }; - const grpcError = new Error(err.details || err.message); + 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);