related serverices to TS

This commit is contained in:
Alex Holliday committed 2026-01-15 22:33:25 +00:00
1 parent cf67c82f5e
commit ed80837810
6 files changed
+162 -114

No files matched your search

+6 -10
View File
@@ -1,14 +1,5 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { pathsToModuleNameMapper } from "ts-jest";
import type { Config } from "jest";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const tsconfigPath = path.resolve(__dirname, "./tsconfig.json");
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8"));
const config: Config = {
rootDir: ".",
testEnvironment: "node",
@@ -17,7 +8,12 @@ const config: Config = {
"^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }],
},
moduleNameMapper: {
...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, { prefix: "<rootDir>/" }),
"^@/validation/(.*)\\.js$": "<rootDir>/src/validation/$1.js",
"^@/utils/(AppError)\\.js$": "<rootDir>/src/utils/$1.ts",
"^@/utils/(.*)\\.js$": "<rootDir>/src/utils/$1.js",
"^@/(.*)\\.ts$": "<rootDir>/src/$1.ts",
"^@/(.*)\\.js$": "<rootDir>/src/$1.ts",
"^@/(.*)$": "<rootDir>/src/$1",
},
testMatch: ["<rootDir>/test/**/*.test.ts"],
setupFilesAfterEnv: [],
@@ -1,4 +1,5 @@
import { IMonitorsRepository } from "@/repositories/index.js";
import type { MonitorType } from "@/types/index.js";
const SERVICE_NAME = "checkService";
@@ -35,6 +36,8 @@ class CheckService {
return CheckService.SERVICE_NAME;
}
buildCheck = async (statusResponse: any, type: MonitorType) => {};
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => {
if (!monitorId) {
throw this.errorService.createBadRequestError("No monitor ID in request");
@@ -1,7 +1,6 @@
const SERVICE_NAME = "JobQueueHelper";
import type { Monitor } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { error } from "winston";
class SuperSimpleQueueHelper {
static SERVICE_NAME = SERVICE_NAME;
@@ -1,10 +1,63 @@
import { Got } from "got";
import type { Monitor } from "@/types/index.js";
import CacheableLookup from "cacheable-lookup";
const SERVICE_NAME = "NetworkService";
class NetworkService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) {
private TYPE_PING: string;
private TYPE_HTTP: string;
private TYPE_PAGESPEED: string;
private TYPE_HARDWARE: string;
private TYPE_DOCKER: string;
private TYPE_PORT: string;
private TYPE_GAME: string;
private SERVICE_NAME: string;
private NETWORK_ERROR: number;
private PING_ERROR: number;
private axios: any;
private got: Got;
private https: any;
private jmespath: any;
private GameDig: any;
private ping: any;
private logger: any;
private http: any;
private Docker: any;
private net: any;
private stringService: any;
private settingsService: any;
constructor({
axios,
got,
https,
jmespath,
GameDig,
ping,
logger,
http,
Docker,
net,
stringService,
settingsService,
}: {
axios: any;
got: Got;
https: any;
jmespath: any;
GameDig: any;
ping: any;
logger: any;
http: any;
Docker: any;
net: any;
stringService: any;
settingsService: any;
}) {
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed";
@@ -17,11 +70,11 @@ class NetworkService {
this.PING_ERROR = 5001;
this.axios = axios;
this.https = https;
this.http = http;
this.jmespath = jmespath;
this.GameDig = GameDig;
this.ping = ping;
this.logger = logger;
this.http = http;
this.Docker = Docker;
this.net = net;
this.stringService = stringService;
@@ -39,7 +92,7 @@ class NetworkService {
}
// Helper functions
async timeRequest(operation) {
async timeRequest(operation: any) {
const start = process.hrtime.bigint();
try {
const response = await operation();
@@ -52,7 +105,7 @@ class NetworkService {
}
// Main entry point
async requestStatus(monitor) {
async requestStatus(monitor: Monitor) {
const type = monitor?.type || "unknown";
switch (type) {
case this.TYPE_PING:
@@ -74,7 +127,7 @@ class NetworkService {
}
}
async requestPing(monitor) {
async requestPing(monitor: Monitor) {
try {
if (!monitor?.url) {
throw new Error("Monitor URL is required");
@@ -112,16 +165,16 @@ class NetworkService {
}
return pingResponse;
} catch (err) {
} catch (err: any) {
err.service = this.SERVICE_NAME;
err.method = "requestPing";
throw err;
}
}
async requestHttp(monitor) {
async requestHttp(monitor: Monitor) {
const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
const httpResponse = {
const httpResponse: Record<string, any> = {
monitorId: id,
teamId: teamId,
type,
@@ -131,7 +184,7 @@ class NetworkService {
if (!url) {
throw new Error("Monitor URL is required");
}
const config = {
const config: Record<string, any> = {
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
};
@@ -145,7 +198,7 @@ class NetworkService {
const response = await this.got(url, config);
let payload;
let payload: any;
const contentType = response.headers["content-type"];
if (contentType && contentType.includes("application/json")) {
@@ -231,7 +284,7 @@ class NetworkService {
}
}
return httpResponse;
} catch (err) {
} catch (err: any) {
if (err.name === "HTTPError" || err.name === "RequestError") {
httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR;
httpResponse.status = false;
@@ -247,7 +300,7 @@ class NetworkService {
}
}
async requestPageSpeed(monitor) {
async requestPageSpeed(monitor: Monitor) {
try {
const url = monitor.url;
if (!url) {
@@ -269,24 +322,24 @@ class NetworkService {
...monitor,
url: pageSpeedUrl,
});
} catch (err) {
} catch (err: any) {
err.service = this.SERVICE_NAME;
err.method = "requestPageSpeed";
throw err;
}
}
async requestHardware(monitor) {
async requestHardware(monitor: Monitor) {
try {
return await this.requestHttp(monitor);
} catch (err) {
} catch (err: any) {
err.service = this.SERVICE_NAME;
err.method = "requestHardware";
throw err;
}
}
async requestDocker(monitor) {
async requestDocker(monitor: Monitor) {
try {
if (!monitor.url) {
throw new Error("Monitor URL is required");
@@ -297,7 +350,7 @@ class NetworkService {
handleError: true, // Enable error handling
});
const dockerResponse = {
const dockerResponse: Record<string, any> = {
monitorId: monitor.id,
type: monitor.type,
};
@@ -309,18 +362,18 @@ class NetworkService {
// Priority-based matching to avoid ambiguity:
// 1. Exact full ID match (64-char)
let exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
let exactIdMatch = containers.find((c: any) => c.Id.toLowerCase() === normalizedInput);
// 2. Exact container name match (case-insensitive)
let exactNameMatch = containers.find((c) =>
c.Names.some((name) => {
let exactNameMatch = containers.find((c: any) =>
c.Names.some((name: string) => {
const cleanName = name.replace(/^\/+/, "").toLowerCase();
return cleanName === normalizedInput;
})
);
// 3. Partial ID match (fallback for backwards compatibility)
let partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
let partialIdMatch = containers.find((c: any) => c.Id.toLowerCase().startsWith(normalizedInput));
// Select container based on priority
let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
@@ -360,7 +413,9 @@ class NetworkService {
}
const container = docker.getContainer(targetContainer.Id);
const { response, responseTime, error } = await this.timeRequest(() => container.inspect());
const { response, responseTime, error }: { response?: any; responseTime?: number; error?: any } = await this.timeRequest(() =>
container.inspect()
);
dockerResponse.responseTime = responseTime;
dockerResponse.status = response?.State?.Status === "running" ? true : false;
@@ -375,14 +430,14 @@ class NetworkService {
}
return dockerResponse;
} catch (err) {
} catch (err: any) {
err.service = this.SERVICE_NAME;
err.method = "requestDocker";
throw err;
}
}
async requestPort(monitor) {
async requestPort(monitor: Monitor) {
try {
const { url, port } = monitor;
const { response, responseTime, error } = await this.timeRequest(async () => {
@@ -405,7 +460,7 @@ class NetworkService {
reject(new Error("Connection timeout"));
});
socket.on("error", (err) => {
socket.on("error", (err: any) => {
socket.destroy();
reject(err);
});
@@ -429,18 +484,18 @@ class NetworkService {
}
return portResponse;
} catch (error) {
} catch (error: any) {
error.service = this.SERVICE_NAME;
error.method = "requestTCP";
throw error;
}
}
async requestGame(monitor) {
async requestGame(monitor: Monitor) {
try {
const { url, port, gameId } = monitor;
const gameResponse = {
const gameResponse: Record<string, any> = {
code: 200,
status: true,
message: "Success",
@@ -452,7 +507,7 @@ class NetworkService {
type: gameId,
host: url,
port: port,
}).catch((error) => {
}).catch((error: any) => {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
@@ -471,21 +526,21 @@ class NetworkService {
gameResponse.responseTime = state.ping;
gameResponse.payload = state;
return gameResponse;
} catch (error) {
} catch (error: any) {
error.service = this.SERVICE_NAME;
error.method = "requestPing";
throw error;
}
}
async handleUnsupportedType(type) {
const err = new Error(`Unsupported type: ${type}`);
async handleUnsupportedType(type: string) {
const err: any = new Error(`Unsupported type: ${type}`);
err.service = this.SERVICE_NAME;
err.method = "getStatus";
throw err;
}
// Other network requests unrelated to monitoring:
async requestWebhook(type, url, body) {
async requestWebhook(type: string, url: string, body: any) {
try {
const response = await this.axios.post(url, body, {
headers: {
@@ -500,7 +555,7 @@ class NetworkService {
message: `Successfully sent ${type} notification`,
payload: response.data,
};
} catch (error) {
} catch (error: any) {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
@@ -517,7 +572,7 @@ class NetworkService {
}
}
async requestPagerDuty({ message, routingKey, monitorUrl }) {
async requestPagerDuty({ message, routingKey, monitorUrl }: { message: string; routingKey: string; monitorUrl: string }) {
try {
const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, {
routing_key: routingKey,
@@ -532,7 +587,7 @@ class NetworkService {
if (response?.data?.status !== "success") return false;
return true;
} catch (error) {
} catch (error: any) {
error.details = error.response?.data;
error.service = this.SERVICE_NAME;
error.method = "requestPagerDuty";
@@ -540,7 +595,17 @@ class NetworkService {
}
}
async requestMatrix({ homeserverUrl, accessToken, roomId, message }) {
async requestMatrix({
homeserverUrl,
accessToken,
roomId,
message,
}: {
homeserverUrl: string;
accessToken: string;
roomId: string;
message: string;
}) {
try {
const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`;
const body = {
@@ -560,7 +625,7 @@ class NetworkService {
code: response.status,
message: "Successfully sent Matrix notification",
};
} catch (error) {
} catch (error: any) {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
@@ -1,11 +1,30 @@
import { IMonitorsRepository } from "@/repositories/index.js";
import MonitorStats from "../../db/models/MonitorStats.js";
import { CheckModel } from "@/db/models/index.js";
import type { Monitor } from "@/types/index.js";
const SERVICE_NAME = "StatusService";
class StatusService {
static SERVICE_NAME = SERVICE_NAME;
private db: any;
private logger: any;
private buffer: any;
private incidentService: any;
private monitorsRepository: IMonitorsRepository;
constructor({ db, logger, buffer, incidentService, monitorsRepository }) {
constructor({
db,
logger,
buffer,
incidentService,
monitorsRepository,
}: {
db: any;
logger: any;
buffer: any;
incidentService: any;
monitorsRepository: IMonitorsRepository;
}) {
this.db = db;
this.logger = logger;
this.buffer = buffer;
@@ -17,7 +36,7 @@ class StatusService {
return StatusService.SERVICE_NAME;
}
async updateRunningStats({ monitor, networkResponse }) {
async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }) {
try {
const monitorId = monitor.id;
const { responseTime, status } = networkResponse;
@@ -78,9 +97,9 @@ class StatusService {
await stats.save();
return true;
} catch (error) {
} catch (error: any) {
this.logger.error({
service: this.SERVICE_NAME,
service: SERVICE_NAME,
message: error.message,
method: "updateRunningStats",
stack: error.stack,
@@ -89,23 +108,13 @@ class StatusService {
}
}
getStatusString = (status) => {
getStatusString = (status: boolean | undefined) => {
if (status === true) return "up";
if (status === false) return "down";
return "unknown";
};
/**
* Saves check if needed and adds to incident buffer
* Removes check from checks buffer if it was saved immediately
*
* @param {Object} check - The check object
* @param {Object} monitor - The monitor object
* @param {string} action - The incident action ("create" or "resolve")
* @param {string} errorContext - Context for error messages
* @returns {Promise<void>}
*/
handleIncidentForCheck = async (check, monitor, action, errorContext = "incident handling") => {
handleIncidentForCheck = async (check: any, monitor: Monitor, action: any, errorContext = "incident handling") => {
try {
let savedCheck = check;
@@ -115,9 +124,9 @@ class StatusService {
savedCheck = await checkModel.save();
this.buffer.removeCheckFromBuffer(check);
} catch (checkError) {
} catch (checkError: any) {
this.logger.error({
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "handleIncidentForCheck",
message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`,
monitorId: monitor.id,
@@ -130,9 +139,9 @@ class StatusService {
if (savedCheck && savedCheck._id) {
try {
this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action });
} catch (incidentError) {
} catch (incidentError: any) {
this.logger.error({
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "handleIncidentForCheck",
message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`,
monitorId: monitor.id,
@@ -141,9 +150,9 @@ class StatusService {
});
}
}
} catch (error) {
} catch (error: any) {
this.logger.error({
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "handleIncidentForCheck",
message: `Error in ${errorContext}: ${error.message}`,
monitorId: monitor?.id,
@@ -151,19 +160,8 @@ class StatusService {
});
}
};
/**
* Updates the status of a monitor based on the network response.
*
* @param {Object} networkResponse - The network response containing monitorId and status.
* @param {string} networkResponse.monitorId - The ID of the monitor.
* @param {string} networkResponse.status - The new status of the monitor.
* @returns {Promise<Object>} - A promise that resolves to an object containinfg the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred.
* @returns {Promise<Object>} returnObject - The object returned by the function.
* @returns {Object} returnObject.monitor - The monitor object.
* @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed.
* @returns {boolean} returnObject.prevStatus - The previous status of the monitor
*/
updateStatus = async (networkResponse) => {
updateStatus = async (networkResponse: any) => {
const check = this.buildCheck(networkResponse);
await this.insertCheck(check);
try {
@@ -221,7 +219,7 @@ class StatusService {
if (statusChanged) {
this.logger.info({
service: this.SERVICE_NAME,
service: SERVICE_NAME,
message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`,
prevStatus,
newStatus,
@@ -242,7 +240,7 @@ class StatusService {
if (lastManuallyResolvedIncident && lastManuallyResolvedIncident.endTime) {
try {
const checksAfterResolution = await Check.find({
const checksAfterResolution = await CheckModel.find({
monitorId: monitor.id,
createdAt: { $gt: lastManuallyResolvedIncident.endTime },
})
@@ -258,9 +256,9 @@ class StatusService {
} else {
calculatedFailureRate = 0;
}
} catch (checkQueryError) {
} catch (checkQueryError: any) {
this.logger.error({
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "updateStatus",
message: `Failed to query checks after manual resolution: ${checkQueryError.message}`,
monitorId: monitor.id,
@@ -272,9 +270,9 @@ class StatusService {
if (calculatedFailureRate >= monitor.statusWindowThreshold) {
await this.handleIncidentForCheck(check, monitor, "create", "threshold check without status change");
}
} catch (error) {
} catch (error: any) {
this.logger.error({
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "updateStatus",
message: `Error handling threshold check without status change: ${error.message}`,
monitorId: monitor.id,
@@ -293,27 +291,14 @@ class StatusService {
code,
timestamp: new Date().getTime(),
};
} catch (error) {
error.service = this.SERVICE_NAME;
} catch (error: any) {
error.service = SERVICE_NAME;
error.method = "updateStatus";
throw error;
}
};
/**
* Builds a check object from the network response.
*
* @param {Object} networkResponse - The network response object.
* @param {string} networkResponse.monitorId - The monitor ID.
* @param {string} networkResponse.type - The type of the response.
* @param {string} networkResponse.status - The status of the response.
* @param {number} networkResponse.responseTime - The response time.
* @param {number} networkResponse.code - The status code.
* @param {string} networkResponse.message - The message.
* @param {Object} networkResponse.payload - The payload of the response.
* @returns {Object} The check object.
*/
buildCheck = (networkResponse) => {
buildCheck = (networkResponse: any) => {
const {
monitorId,
teamId,
@@ -355,7 +340,7 @@ class StatusService {
if (typeof payload === "undefined") {
this.logger.warn({
message: "Failed to build check",
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "buildCheck",
details: "empty payload",
});
@@ -363,7 +348,7 @@ class StatusService {
}
const categories = payload?.lighthouseResult?.categories ?? {};
const audits = payload?.lighthouseResult?.audits ?? {};
const mapAudit = (audit) => {
const mapAudit = (audit: any) => {
if (!audit || typeof audit !== "object") {
return undefined;
}
@@ -416,22 +401,22 @@ class StatusService {
* @param {Object} networkResponse.payload - The payload of the response.
* @returns {Promise<void>} A promise that resolves when the check is inserted.
*/
insertCheck = async (check) => {
insertCheck = async (check: any) => {
try {
if (typeof check === "undefined") {
this.logger.warn({
message: "Failed to build check",
service: this.SERVICE_NAME,
service: SERVICE_NAME,
method: "insertCheck",
});
return false;
}
this.buffer.addToBuffer({ check });
return true;
} catch (error) {
} catch (error: any) {
this.logger.error({
message: error.message,
service: error.service || this.SERVICE_NAME,
service: error.service || SERVICE_NAME,
method: error.method || "insertCheck",
details: error.details || `Error inserting check for monitor: ${check?.monitorId}`,
stack: error.stack,
+2 -2
View File
@@ -2,7 +2,7 @@ import { describe, expect, it, jest } from "@jest/globals";
import SuperSimpleQueueHelper from "../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts";
import type { Monitor } from "../src/types/monitor.ts";
const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn() });
const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() });
const createHelper = (overrides?: Partial<ConstructorParameters<typeof SuperSimpleQueueHelper>[0]>) => {
const maintenanceWindowModule = {
@@ -27,7 +27,7 @@ describe("SuperSimpleQueueHelper", () => {
const job = helper.getMonitorJob();
await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor);
expect(helper["networkService"].requestStatus).not.toHaveBeenCalled();
expect(helper["logger"].info).toHaveBeenCalledWith(
expect(helper["logger"].debug).toHaveBeenCalledWith(
expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") })
);
spy.mockRestore();