Merge pull request #3141 from bluewave-labs/feat/job-queue-ts
feat/job queue ts
This commit is contained in:
21 files changed
+1052
-559
No files matched your search
@@ -87,8 +87,8 @@ const Incidents2 = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const lookup = monitors?.reduce((acc, monitor) => {
|
||||
acc[monitor._id] = {
|
||||
_id: monitor._id,
|
||||
acc[monitor.id] = {
|
||||
_id: monitor.id,
|
||||
name: monitor.name,
|
||||
type: monitor.type,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,11 @@ const config: Config = {
|
||||
"^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^@/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"],
|
||||
|
||||
@@ -155,7 +155,7 @@ export const initializeServices = async ({
|
||||
|
||||
// Repositories
|
||||
const monitorsRepository = new MongoMonitorsRepository();
|
||||
const checksRepository = new MongoChecksRepository();
|
||||
const checksRepository = new MongoChecksRepository(logger);
|
||||
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||
const statusPagesRepository = new MongoStatusPagesRepository();
|
||||
|
||||
@@ -183,7 +183,17 @@ export const initializeServices = async ({
|
||||
stringService,
|
||||
});
|
||||
|
||||
const bufferService = new BufferService({ db, logger, envSettings, incidentService });
|
||||
const checkService = new CheckService({
|
||||
db,
|
||||
settingsService,
|
||||
stringService,
|
||||
errorService,
|
||||
monitorsRepository,
|
||||
logger,
|
||||
checksRepository,
|
||||
});
|
||||
|
||||
const bufferService = new BufferService({ db, logger, envSettings, incidentService, checkService });
|
||||
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository });
|
||||
|
||||
@@ -208,11 +218,11 @@ export const initializeServices = async ({
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
checkService,
|
||||
buffer: bufferService,
|
||||
});
|
||||
|
||||
const superSimpleQueue = await SuperSimpleQueue.create({
|
||||
envSettings,
|
||||
db,
|
||||
logger,
|
||||
helper: superSimpleQueueHelper,
|
||||
monitorsRepository,
|
||||
@@ -231,13 +241,7 @@ export const initializeServices = async ({
|
||||
jobQueue: superSimpleQueue,
|
||||
monitorsRepository,
|
||||
});
|
||||
const checkService = new CheckService({
|
||||
db,
|
||||
settingsService,
|
||||
stringService,
|
||||
errorService,
|
||||
monitorsRepository,
|
||||
});
|
||||
|
||||
const diagnosticService = new DiagnosticService();
|
||||
const inviteService = new InviteService({
|
||||
db,
|
||||
|
||||
@@ -11,8 +11,7 @@ import type {
|
||||
CheckMemoryInfo,
|
||||
CheckMetadata,
|
||||
CheckNetworkInterfaceInfo,
|
||||
CheckTimings,
|
||||
CheckTimingPhases,
|
||||
GotTimings,
|
||||
ILighthouseAudit,
|
||||
} from "@/types/check.js";
|
||||
|
||||
@@ -35,7 +34,7 @@ interface CheckDocument extends CheckDocumentBase {
|
||||
_id: Types.ObjectId;
|
||||
}
|
||||
|
||||
const timingPhasesSchema = new Schema<CheckTimingPhases>(
|
||||
const timingPhasesSchema = new Schema<GotTimings["phases"]>(
|
||||
{
|
||||
wait: { type: Number, default: 0 },
|
||||
dns: { type: Number, default: 0 },
|
||||
@@ -49,7 +48,7 @@ const timingPhasesSchema = new Schema<CheckTimingPhases>(
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const timingsSchema = new Schema<CheckTimings>(
|
||||
const timingsSchema = new Schema<GotTimings>(
|
||||
{
|
||||
start: { type: Number, default: 0 },
|
||||
socket: { type: Number, default: 0 },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Check, CheckAudits, MonitorType } from "@/types/index.js";
|
||||
import type { Check, MonitorType } from "@/types/index.js";
|
||||
import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
|
||||
|
||||
export interface PageSpeedChecksResult {
|
||||
@@ -55,6 +55,8 @@ export interface UptimeChecksResult {
|
||||
|
||||
export interface IChecksRepository {
|
||||
// create
|
||||
createChecks(checks: Check[]): Promise<Check[]>;
|
||||
|
||||
// single fetch
|
||||
// collection fetch
|
||||
findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
|
||||
|
||||
@@ -10,18 +10,27 @@ import type {
|
||||
CheckMemoryInfo,
|
||||
CheckMetadata,
|
||||
CheckNetworkInterfaceInfo,
|
||||
CheckTimings,
|
||||
GotTimings,
|
||||
MonitorType,
|
||||
} from "@/types/index.js";
|
||||
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
export type LatestChecksMap = Record<string, Check[]>;
|
||||
type DateRange = { start: Date; end: Date };
|
||||
type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number };
|
||||
type HardwareUpChecks = { totalChecks: number };
|
||||
|
||||
class MongoChecksRepository implements IChecksRepository {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: any;
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private toEntity = (doc: CheckDocument): Check => {
|
||||
const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => {
|
||||
if (!value) {
|
||||
@@ -44,7 +53,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
return toDateString(value);
|
||||
};
|
||||
|
||||
const mapTimings = (timings?: CheckTimings): CheckTimings => {
|
||||
const mapTimings = (timings?: GotTimings): GotTimings => {
|
||||
const phases = timings?.phases ?? {
|
||||
wait: 0,
|
||||
dns: 0,
|
||||
@@ -133,11 +142,11 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
cls: audits.cls ?? 0,
|
||||
si: audits.si ?? 0,
|
||||
fcp: audits.fcp ?? 0,
|
||||
lcp: audits.lcp ?? 0,
|
||||
tbt: audits.tbt ?? 0,
|
||||
cls: audits.cls,
|
||||
si: audits.si,
|
||||
fcp: audits.fcp,
|
||||
lcp: audits.lcp,
|
||||
tbt: audits.tbt,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -176,6 +185,10 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
};
|
||||
};
|
||||
|
||||
createChecks = async (checks: Check[]) => {
|
||||
return await CheckModel.insertMany(checks);
|
||||
};
|
||||
|
||||
findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap> => {
|
||||
if (monitorIds.length === 0) {
|
||||
return {};
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||
import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js";
|
||||
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
|
||||
|
||||
const SERVICE_NAME = "checkService";
|
||||
|
||||
@@ -10,31 +12,114 @@ class CheckService {
|
||||
private stringService: any;
|
||||
private errorService: any;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
|
||||
private checksRepository: IChecksRepository;
|
||||
private logger: any;
|
||||
constructor({
|
||||
db,
|
||||
settingsService,
|
||||
stringService,
|
||||
errorService,
|
||||
monitorsRepository,
|
||||
logger,
|
||||
checksRepository,
|
||||
}: {
|
||||
db: any;
|
||||
settingsService: any;
|
||||
stringService: any;
|
||||
errorService: any;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
logger: any;
|
||||
checksRepository: IChecksRepository;
|
||||
}) {
|
||||
this.db = db;
|
||||
this.settingsService = settingsService;
|
||||
this.stringService = stringService;
|
||||
this.errorService = errorService;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.logger = logger;
|
||||
this.checksRepository = checksRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return CheckService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
createChecks = async (checks: Check[]) => {
|
||||
return this.checksRepository.createChecks(checks);
|
||||
};
|
||||
|
||||
buildCheck = (statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>) => {
|
||||
const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse;
|
||||
|
||||
const check: Partial<Check> = {
|
||||
metadata: {
|
||||
monitorId,
|
||||
teamId,
|
||||
type,
|
||||
},
|
||||
status,
|
||||
statusCode: code,
|
||||
responseTime: responseTime || 0,
|
||||
timings: timings,
|
||||
message,
|
||||
};
|
||||
|
||||
if (type === "pagespeed") {
|
||||
const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined;
|
||||
if (!pageSpeedPayload) {
|
||||
this.logger.warn({
|
||||
message: "Failed to build check",
|
||||
service: SERVICE_NAME,
|
||||
method: "buildCheck",
|
||||
details: "empty payload",
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
const categories = pageSpeedPayload.lighthouseResult?.categories ?? {};
|
||||
const audits = pageSpeedPayload.lighthouseResult?.audits ?? {};
|
||||
const mapAudit = (audit: any) => {
|
||||
if (!audit || typeof audit !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: audit.id,
|
||||
title: audit.title,
|
||||
score: typeof audit.score === "number" ? audit.score : (audit.score ?? null),
|
||||
displayValue: audit.displayValue,
|
||||
numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined,
|
||||
numericUnit: audit.numericUnit,
|
||||
};
|
||||
};
|
||||
check.accessibility = (categories?.accessibility?.score || 0) * 100;
|
||||
check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100;
|
||||
check.seo = (categories?.seo?.score || 0) * 100;
|
||||
check.performance = (categories?.performance?.score || 0) * 100;
|
||||
check.audits = {
|
||||
cls: mapAudit(audits?.["cumulative-layout-shift"]),
|
||||
si: mapAudit(audits?.["speed-index"]),
|
||||
fcp: mapAudit(audits?.["first-contentful-paint"]),
|
||||
lcp: mapAudit(audits?.["largest-contentful-paint"]),
|
||||
tbt: mapAudit(audits?.["total-blocking-time"]),
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "hardware") {
|
||||
const hardwarePayload = payload as HardwareStatusPayload | undefined;
|
||||
const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {};
|
||||
const errorsSource = Array.isArray(hardwarePayload?.errors)
|
||||
? hardwarePayload?.errors
|
||||
: (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors;
|
||||
check.cpu = cpu;
|
||||
check.memory = memory;
|
||||
check.disk = disk;
|
||||
check.host = host;
|
||||
check.errors = errorsSource;
|
||||
check.capture = hardwarePayload?.capture;
|
||||
check.net = net;
|
||||
}
|
||||
return check;
|
||||
};
|
||||
|
||||
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => {
|
||||
if (!monitorId) {
|
||||
throw this.errorService.createBadRequestError("No monitor ID in request");
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "@/service/business/monitorService.js";
|
||||
export * from "@/service/infrastructure/networkService.js";
|
||||
@@ -1,177 +0,0 @@
|
||||
import Scheduler from "super-simple-scheduler";
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
|
||||
class SuperSimpleQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ envSettings, db, logger, helper, monitorsRepository }) {
|
||||
this.envSettings = envSettings;
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.helper = helper;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return SuperSimpleQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create({ envSettings, db, logger, helper, monitorsRepository }) {
|
||||
const instance = new SuperSimpleQueue({ envSettings, db, logger, helper, monitorsRepository });
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
try {
|
||||
this.scheduler = new Scheduler({
|
||||
// storeType: "mongo",
|
||||
// storeType: "redis",
|
||||
logLevel: "debug",
|
||||
debug: true,
|
||||
// dbUri: this.envSettings.dbConnectionString,
|
||||
});
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||
const monitors = await this.monitorsRepository.findAll();
|
||||
for (const monitor of monitors) {
|
||||
const randomOffset = Math.floor(Math.random() * 100);
|
||||
setTimeout(() => {
|
||||
this.addJob(monitor.id, monitor);
|
||||
}, randomOffset);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: "Failed to initialize SuperSimpleQueue",
|
||||
service: SERVICE_NAME,
|
||||
method: "init",
|
||||
details: error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
addJob = async (monitorId, monitor) => {
|
||||
this.scheduler.addJob({
|
||||
id: monitorId,
|
||||
template: "monitor-job",
|
||||
repeat: monitor.interval,
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
};
|
||||
|
||||
deleteJob = async (monitor) => {
|
||||
this.scheduler.removeJob(monitor.id);
|
||||
};
|
||||
|
||||
pauseJob = async (monitor) => {
|
||||
const result = this.scheduler.pauseJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Paused monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "pauseJob",
|
||||
});
|
||||
};
|
||||
|
||||
resumeJob = async (monitor) => {
|
||||
const result = this.scheduler.resumeJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Resumed monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "resumeJob",
|
||||
});
|
||||
};
|
||||
|
||||
updateJob = async (monitor) => {
|
||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
this.scheduler.stop();
|
||||
};
|
||||
|
||||
getMetrics = async () => {
|
||||
const jobs = await this.scheduler.getJobs();
|
||||
const metrics = jobs.reduce(
|
||||
(acc, job) => {
|
||||
acc.totalRuns += job.runCount || 0;
|
||||
acc.totalFailures += job.failCount || 0;
|
||||
acc.jobs++;
|
||||
if (job.failCount > 0 && job.lastFailedAt >= job.lsatRunAt) {
|
||||
acc.failingJobs++;
|
||||
}
|
||||
|
||||
if (job.lockedAt) {
|
||||
acc.activeJobs++;
|
||||
}
|
||||
|
||||
if (job.failCount > 0) {
|
||||
acc.jobsWithFailures.push({
|
||||
monitorId: job.id,
|
||||
monitorUrl: job?.data?.url || null,
|
||||
monitorType: job?.data?.type || null,
|
||||
failedAt: job.lastFailedAt,
|
||||
failCount: job.failCount,
|
||||
failReason: job.lastFailReason,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
jobs: 0,
|
||||
activeJobs: 0,
|
||||
failingJobs: 0,
|
||||
jobsWithFailures: [],
|
||||
totalRuns: 0,
|
||||
totalFailures: 0,
|
||||
}
|
||||
);
|
||||
return metrics;
|
||||
};
|
||||
|
||||
getJobs = async () => {
|
||||
const jobs = await this.scheduler.getJobs();
|
||||
return jobs.map((job) => {
|
||||
return {
|
||||
monitorId: job.id,
|
||||
monitorUrl: job?.data?.url || null,
|
||||
monitorType: job?.data?.type || null,
|
||||
monitorInterval: job?.data?.interval || null,
|
||||
active: job.active,
|
||||
lockedAt: job.lockedAt,
|
||||
runCount: job.runCount || 0,
|
||||
failCount: job.failCount || 0,
|
||||
failReason: job.lastFailReason,
|
||||
lastRunAt: job.lastRunAt,
|
||||
lastFinishedAt: job.lastFinishedAt,
|
||||
lastRunTook: job.lockedAt ? null : job.lastFinishedAt - job.lastRunAt,
|
||||
lastFailedAt: job.lastFailedAt,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
flushQueues = async () => {
|
||||
const stopRes = this.scheduler.stop();
|
||||
const flushRes = this.scheduler.flushJobs();
|
||||
const initRes = await this.init();
|
||||
return {
|
||||
success: stopRes && flushRes && initRes,
|
||||
};
|
||||
};
|
||||
|
||||
obliterate = async () => {
|
||||
console.log("obliterate not implemented");
|
||||
};
|
||||
}
|
||||
|
||||
export default SuperSimpleQueue;
|
||||
@@ -0,0 +1,254 @@
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import Scheduler from "super-simple-scheduler";
|
||||
const SERVICE_NAME = "JobQueue";
|
||||
|
||||
type QueueJobFailure = {
|
||||
monitorId: string | number;
|
||||
monitorUrl: string | null;
|
||||
monitorType: string | null;
|
||||
failedAt: number | null;
|
||||
failCount: number;
|
||||
failReason: string | null;
|
||||
};
|
||||
|
||||
type QueueMetrics = {
|
||||
jobs: number;
|
||||
activeJobs: number;
|
||||
failingJobs: number;
|
||||
jobsWithFailures: QueueJobFailure[];
|
||||
totalRuns: number;
|
||||
totalFailures: number;
|
||||
};
|
||||
|
||||
type QueueJobSummary = {
|
||||
monitorId: string | number;
|
||||
monitorUrl: string | null;
|
||||
monitorType: string | null;
|
||||
monitorInterval: number | null;
|
||||
active: boolean;
|
||||
lockedAt: number | null;
|
||||
runCount: number;
|
||||
failCount: number;
|
||||
failReason: string | null;
|
||||
lastRunAt: number | null;
|
||||
lastFinishedAt: number | null;
|
||||
lastRunTook: number | null;
|
||||
lastFailedAt: number | null;
|
||||
};
|
||||
|
||||
export interface ISuperSimpleQueue {
|
||||
readonly serviceName: string;
|
||||
init(): Promise<boolean>;
|
||||
addJob(monitorId: string, monitor: any): Promise<void>;
|
||||
deleteJob(monitor: any): Promise<void>;
|
||||
pauseJob(monitor: any): Promise<void>;
|
||||
resumeJob(monitor: any): Promise<void>;
|
||||
updateJob(monitor: any): Promise<void>;
|
||||
shutdown(): Promise<void>;
|
||||
getMetrics(): Promise<QueueMetrics>;
|
||||
getJobs(): Promise<QueueJobSummary[]>;
|
||||
flushQueues(): Promise<{ success: boolean }>;
|
||||
obliterate(): Promise<void>;
|
||||
}
|
||||
|
||||
class SuperSimpleQueue implements ISuperSimpleQueue {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private logger: any;
|
||||
private helper: any;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private readonly scheduler: Scheduler;
|
||||
|
||||
constructor({
|
||||
logger,
|
||||
helper,
|
||||
monitorsRepository,
|
||||
scheduler,
|
||||
}: {
|
||||
logger: any;
|
||||
helper: any;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
scheduler: Scheduler;
|
||||
}) {
|
||||
this.logger = logger;
|
||||
this.helper = helper;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return SuperSimpleQueue.SERVICE_NAME;
|
||||
}
|
||||
|
||||
static async create({ logger, helper, monitorsRepository }: { logger: any; helper: any; monitorsRepository: IMonitorsRepository }) {
|
||||
const scheduler = new Scheduler({
|
||||
// storeType: "mongo",
|
||||
// storeType: "redis",
|
||||
logLevel: "debug",
|
||||
// dbUri: envSettings.dbConnectionString,
|
||||
});
|
||||
const instance = new SuperSimpleQueue({ logger, helper, monitorsRepository, scheduler });
|
||||
await instance.init();
|
||||
return instance;
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
try {
|
||||
this.scheduler.start();
|
||||
|
||||
this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob());
|
||||
const monitors = await this.monitorsRepository.findAll();
|
||||
if (!monitors) {
|
||||
return true;
|
||||
}
|
||||
for (const monitor of monitors) {
|
||||
const randomOffset = Math.floor(Math.random() * 100);
|
||||
setTimeout(() => {
|
||||
this.addJob(monitor.id, monitor);
|
||||
}, randomOffset);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error({
|
||||
message: "Failed to initialize SuperSimpleQueue",
|
||||
service: SERVICE_NAME,
|
||||
method: "init",
|
||||
details: error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
addJob = async (monitorId: string, monitor: any) => {
|
||||
this.scheduler.addJob({
|
||||
id: monitorId,
|
||||
template: "monitor-job",
|
||||
repeat: monitor.interval,
|
||||
active: monitor.isActive,
|
||||
data: monitor,
|
||||
});
|
||||
};
|
||||
|
||||
deleteJob = async (monitor: any) => {
|
||||
this.scheduler.removeJob(monitor.id);
|
||||
};
|
||||
|
||||
pauseJob = async (monitor: any) => {
|
||||
const result = await this.scheduler.pauseJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Paused monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "pauseJob",
|
||||
});
|
||||
};
|
||||
|
||||
resumeJob = async (monitor: any) => {
|
||||
const result = await this.scheduler.resumeJob(monitor.id);
|
||||
if (result === false) {
|
||||
throw new Error("Failed to resume monitor");
|
||||
}
|
||||
this.logger.debug({
|
||||
message: `Resumed monitor ${monitor.id}`,
|
||||
service: SERVICE_NAME,
|
||||
method: "resumeJob",
|
||||
});
|
||||
};
|
||||
|
||||
updateJob = async (monitor: any) => {
|
||||
this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor });
|
||||
};
|
||||
|
||||
shutdown = async () => {
|
||||
this.scheduler.stop();
|
||||
};
|
||||
|
||||
getMetrics = async () => {
|
||||
const jobs = await this.scheduler.getJobs();
|
||||
const metrics = jobs.reduce(
|
||||
(acc, job) => {
|
||||
const runCount = job.runCount ?? 0;
|
||||
const failCount = job.failCount ?? 0;
|
||||
const lastFailedAt = job.lastFailedAt ?? 0;
|
||||
const lastRunAt = job.lastRunAt ?? 0;
|
||||
acc.totalRuns += runCount;
|
||||
acc.totalFailures += failCount;
|
||||
acc.jobs++;
|
||||
if (failCount > 0 && lastFailedAt >= lastRunAt) {
|
||||
acc.failingJobs++;
|
||||
}
|
||||
|
||||
if (job.lockedAt) {
|
||||
acc.activeJobs++;
|
||||
}
|
||||
|
||||
if (failCount > 0) {
|
||||
acc.jobsWithFailures.push({
|
||||
monitorId: job.id,
|
||||
monitorUrl: job?.data?.url || null,
|
||||
monitorType: job?.data?.type || null,
|
||||
failedAt: job.lastFailedAt ?? null,
|
||||
failCount,
|
||||
failReason: job.lastFailReason ?? null,
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
jobs: 0,
|
||||
activeJobs: 0,
|
||||
failingJobs: 0,
|
||||
jobsWithFailures: [] as Array<{
|
||||
monitorId: string | number;
|
||||
monitorUrl: any;
|
||||
monitorType: any;
|
||||
failedAt: number | null;
|
||||
failCount: number;
|
||||
failReason: string | null;
|
||||
}>,
|
||||
totalRuns: 0,
|
||||
totalFailures: 0,
|
||||
}
|
||||
);
|
||||
return metrics;
|
||||
};
|
||||
|
||||
getJobs = async () => {
|
||||
const jobs = await this.scheduler.getJobs();
|
||||
return jobs.map((job) => {
|
||||
return {
|
||||
monitorId: job.id,
|
||||
monitorUrl: job?.data?.url || null,
|
||||
monitorType: job?.data?.type || null,
|
||||
monitorInterval: job?.data?.interval || null,
|
||||
active: job.active,
|
||||
lockedAt: job.lockedAt ?? null,
|
||||
runCount: job.runCount ?? 0,
|
||||
failCount: job.failCount ?? 0,
|
||||
failReason: job.lastFailReason ?? null,
|
||||
lastRunAt: job.lastRunAt ?? null,
|
||||
lastFinishedAt: job.lastFinishedAt ?? null,
|
||||
lastRunTook: job.lockedAt ? null : (job.lastFinishedAt ?? 0) - (job.lastRunAt ?? 0),
|
||||
lastFailedAt: job.lastFailedAt ?? null,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
flushQueues = async () => {
|
||||
const stopRes = await this.scheduler.stop();
|
||||
const flushRes = await this.scheduler.flushJobs();
|
||||
const initRes = await this.init();
|
||||
return {
|
||||
success: Boolean(stopRes && flushRes && initRes),
|
||||
};
|
||||
};
|
||||
|
||||
obliterate = async () => {
|
||||
console.log("obliterate not implemented");
|
||||
};
|
||||
}
|
||||
|
||||
export default SuperSimpleQueue;
|
||||
+55
-27
@@ -1,23 +1,42 @@
|
||||
const SERVICE_NAME = "JobQueueHelper";
|
||||
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { INetworkService } from "@/service/index.js";
|
||||
class SuperSimpleQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* db: import("../database.js").Database,
|
||||
* logger: import("../logger.js").Logger,
|
||||
* networkService: import("../networkService.js").NetworkService,
|
||||
* statusService: import("../statusService.js").StatusService,
|
||||
* notificationService: import("../notificationService.js").NotificationService
|
||||
* }}
|
||||
*/
|
||||
constructor({ db, logger, networkService, statusService, notificationService }) {
|
||||
private db: any;
|
||||
private logger: any;
|
||||
private networkService: INetworkService;
|
||||
private statusService: any;
|
||||
private notificationService: any;
|
||||
private checkService: any;
|
||||
private buffer: any;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
logger,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
checkService,
|
||||
buffer,
|
||||
}: {
|
||||
db: any;
|
||||
logger: any;
|
||||
networkService: INetworkService;
|
||||
statusService: any;
|
||||
notificationService: any;
|
||||
checkService: any;
|
||||
buffer: any;
|
||||
}) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
this.notificationService = notificationService;
|
||||
this.checkService = checkService;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -25,39 +44,48 @@ class SuperSimpleQueueHelper {
|
||||
}
|
||||
|
||||
getMonitorJob = () => {
|
||||
return async (monitor) => {
|
||||
return async (monitor: Monitor) => {
|
||||
try {
|
||||
const monitorId = monitor.id;
|
||||
const teamId = monitor.teamId;
|
||||
if (!monitorId) {
|
||||
throw new Error("No monitor id");
|
||||
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" });
|
||||
}
|
||||
|
||||
// Step 1. Check for maintenacne window, if found, skip the check
|
||||
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
|
||||
if (maintenanceWindowActive) {
|
||||
this.logger.info({
|
||||
this.logger.debug({
|
||||
message: `Monitor ${monitorId} is in maintenance window`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const networkResponse = await this.networkService.requestStatus(monitor);
|
||||
|
||||
if (!networkResponse) {
|
||||
// Step 2. Request monitor status
|
||||
const status = await this.networkService.requestStatus(monitor);
|
||||
if (!status) {
|
||||
throw new Error("No network response");
|
||||
}
|
||||
|
||||
const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse);
|
||||
// Step 3. Build check
|
||||
const check = await this.checkService.buildCheck(status);
|
||||
|
||||
// Step 4 Add check to buffer
|
||||
this.buffer.addToBuffer({ check });
|
||||
|
||||
// Step 4. Update monitor status
|
||||
const statusChangeResult = await this.statusService.updateMonitorStatus(status, check);
|
||||
|
||||
this.notificationService
|
||||
.handleNotifications({
|
||||
...networkResponse,
|
||||
monitor: updatedMonitor,
|
||||
prevStatus,
|
||||
statusChanged,
|
||||
...status,
|
||||
monitor: statusChangeResult.monitor,
|
||||
prevStatus: statusChangeResult.prevStatus,
|
||||
statusChanged: statusChangeResult.statusChanged,
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch((error: any) => {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: SERVICE_NAME,
|
||||
@@ -66,7 +94,7 @@ class SuperSimpleQueueHelper {
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: error.service || SERVICE_NAME,
|
||||
@@ -78,13 +106,13 @@ class SuperSimpleQueueHelper {
|
||||
};
|
||||
};
|
||||
|
||||
async isInMaintenanceWindow(monitorId, teamId) {
|
||||
async isInMaintenanceWindow(monitorId: string, teamId: string) {
|
||||
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({
|
||||
monitorId: monitorId.toString(),
|
||||
teamId: teamId.toString(),
|
||||
monitorId: monitorId,
|
||||
teamId: teamId,
|
||||
});
|
||||
// Check for active maintenance window:
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
|
||||
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => {
|
||||
if (window.active) {
|
||||
const start = new Date(window.start);
|
||||
const end = new Date(window.end);
|
||||
+44
-18
@@ -1,15 +1,38 @@
|
||||
import { config } from "@/config/index.js";
|
||||
import type { Check } from "@/types/index.js";
|
||||
|
||||
const SERVICE_NAME = "BufferService";
|
||||
|
||||
class BufferService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
constructor({ db, logger, envSettings, incidentService }) {
|
||||
console.log(envSettings);
|
||||
private BUFFER_TIMEOUT: number;
|
||||
private db: any;
|
||||
private logger: any;
|
||||
private incidentService: any;
|
||||
private SERVICE_NAME: string;
|
||||
private buffer: any[];
|
||||
private incidentBuffer: any[];
|
||||
private bufferTimer: NodeJS.Timeout | null = null;
|
||||
private checksService: any;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
logger,
|
||||
envSettings,
|
||||
incidentService,
|
||||
checkService,
|
||||
}: {
|
||||
db: any;
|
||||
logger: any;
|
||||
envSettings: any;
|
||||
incidentService: any;
|
||||
checkService: any;
|
||||
}) {
|
||||
this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.incidentService = incidentService;
|
||||
this.checksService = checkService;
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.buffer = [];
|
||||
this.incidentBuffer = [];
|
||||
@@ -25,10 +48,10 @@ class BufferService {
|
||||
return BufferService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
addToBuffer({ check }) {
|
||||
addToBuffer({ check }: { check: Check }) {
|
||||
try {
|
||||
this.buffer.push(check);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
@@ -38,7 +61,7 @@ class BufferService {
|
||||
}
|
||||
}
|
||||
|
||||
addIncidentToBuffer({ monitor, check, action = "create" }) {
|
||||
addIncidentToBuffer({ monitor, check, action = "create" }: { monitor: any; check: Check; action?: string }) {
|
||||
try {
|
||||
if (!monitor || !check) {
|
||||
this.logger.warn({
|
||||
@@ -50,7 +73,7 @@ class BufferService {
|
||||
}
|
||||
|
||||
this.incidentBuffer.push({ monitor, check, action });
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
@@ -60,20 +83,20 @@ class BufferService {
|
||||
}
|
||||
}
|
||||
|
||||
removeCheckFromBuffer(checkToRemove) {
|
||||
removeCheckFromBuffer(checkToRemove: Check) {
|
||||
try {
|
||||
if (!checkToRemove) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const index = this.buffer.findIndex((check) => {
|
||||
if (checkToRemove._id && check._id) {
|
||||
return check._id.toString() === checkToRemove._id.toString();
|
||||
if (checkToRemove.id && check.id) {
|
||||
return check.id.toString() === checkToRemove.id.toString();
|
||||
}
|
||||
return (
|
||||
check.monitorId?.toString() === checkToRemove.monitorId?.toString() &&
|
||||
check.teamId?.toString() === checkToRemove.teamId?.toString() &&
|
||||
check.type === checkToRemove.type &&
|
||||
check.monitorId?.toString() === checkToRemove.metadata.monitorId &&
|
||||
check.teamId?.toString() === checkToRemove.metadata.teamId &&
|
||||
check.type === checkToRemove.metadata.type &&
|
||||
check.status === checkToRemove.status &&
|
||||
check.statusCode === checkToRemove.statusCode &&
|
||||
check.responseTime === checkToRemove.responseTime &&
|
||||
@@ -87,7 +110,7 @@ class BufferService {
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
@@ -99,10 +122,13 @@ class BufferService {
|
||||
}
|
||||
|
||||
scheduleNextFlush() {
|
||||
if (this.bufferTimer) {
|
||||
clearTimeout(this.bufferTimer);
|
||||
}
|
||||
this.bufferTimer = setTimeout(async () => {
|
||||
try {
|
||||
await this.flushBuffer();
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error in flush cycle: ${error.message}`,
|
||||
service: this.SERVICE_NAME,
|
||||
@@ -118,9 +144,9 @@ class BufferService {
|
||||
async flushBuffer() {
|
||||
try {
|
||||
if (this.buffer.length > 0) {
|
||||
await this.db.checkModule.createChecks(this.buffer);
|
||||
await this.checksService.createChecks(this.buffer);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
@@ -133,7 +159,7 @@ class BufferService {
|
||||
if (this.incidentBuffer.length > 0 && this.incidentService) {
|
||||
await this.flushIncidentBuffer();
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
@@ -154,7 +180,7 @@ class BufferService {
|
||||
try {
|
||||
const itemsToProcess = [...this.incidentBuffer];
|
||||
await this.incidentService.processIncidentsFromBuffer(itemsToProcess);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
this.logger.error({
|
||||
message: `Error flushing incident buffer: ${error.message}`,
|
||||
service: this.SERVICE_NAME,
|
||||
+248
-81
@@ -1,10 +1,145 @@
|
||||
import { HTTPError, RequestError } from "got";
|
||||
import type { Got, Response } from "got";
|
||||
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
|
||||
|
||||
import CacheableLookup from "cacheable-lookup";
|
||||
const SERVICE_NAME = "NetworkService";
|
||||
|
||||
class NetworkService {
|
||||
type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
|
||||
|
||||
interface BuildStatusResponseArgs<T> {
|
||||
monitor: Monitor;
|
||||
response?: Response<T> | null;
|
||||
error?: Error | RequestError | HTTPError | null;
|
||||
payload?: T | null;
|
||||
jsonPath?: string;
|
||||
matchMethod?: MonitorStatusResponse["matchMethod"];
|
||||
expectedValue?: string;
|
||||
extracted?: unknown;
|
||||
overrides?: MonitorStatusResponseOverrides<T>;
|
||||
}
|
||||
|
||||
export interface INetworkService {
|
||||
readonly serviceName: string;
|
||||
requestStatus(monitor: Monitor): Promise<MonitorStatusResponse>;
|
||||
requestWebhook(type: string, url: string, body: any): Promise<{ type: string; status: boolean; code: number; message: string; payload?: unknown }>;
|
||||
requestPagerDuty(args: { message: string; routingKey: string; monitorUrl: string }): Promise<boolean>;
|
||||
requestMatrix(args: { homeserverUrl: string; accessToken: string; roomId: string; message: string }): Promise<{
|
||||
status: boolean;
|
||||
code: number;
|
||||
message: string;
|
||||
payload?: unknown;
|
||||
}>;
|
||||
}
|
||||
|
||||
class NetworkService implements INetworkService {
|
||||
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;
|
||||
|
||||
private buildStatusResponse = <T>({
|
||||
monitor,
|
||||
response,
|
||||
error,
|
||||
payload,
|
||||
jsonPath,
|
||||
matchMethod,
|
||||
expectedValue,
|
||||
extracted,
|
||||
overrides,
|
||||
}: BuildStatusResponseArgs<T>): MonitorStatusResponse<T> => {
|
||||
if (error) {
|
||||
const statusResponse: MonitorStatusResponse<T> = {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: false,
|
||||
code: this.NETWORK_ERROR,
|
||||
message: error.message ?? "Network error",
|
||||
responseTime: 0,
|
||||
timings: undefined,
|
||||
jsonPath,
|
||||
matchMethod,
|
||||
expectedValue,
|
||||
extracted,
|
||||
payload,
|
||||
};
|
||||
if (error instanceof HTTPError || error instanceof RequestError) {
|
||||
statusResponse.code = error?.response?.statusCode ?? this.NETWORK_ERROR;
|
||||
statusResponse.message = error.message;
|
||||
statusResponse.responseTime = error.timings?.phases?.total ?? 0;
|
||||
statusResponse.timings = error.timings;
|
||||
}
|
||||
return { ...statusResponse, ...(overrides ?? {}) };
|
||||
}
|
||||
|
||||
return {
|
||||
monitorId: monitor.id,
|
||||
teamId: monitor.teamId,
|
||||
type: monitor.type,
|
||||
status: response?.ok ?? false,
|
||||
code: response?.statusCode ?? this.NETWORK_ERROR,
|
||||
message: response?.statusMessage ?? "",
|
||||
responseTime: response?.timings?.phases?.total ?? 0,
|
||||
timings: response?.timings,
|
||||
payload: payload ?? response?.body,
|
||||
jsonPath,
|
||||
matchMethod,
|
||||
expectedValue,
|
||||
extracted,
|
||||
...(overrides ?? {}),
|
||||
};
|
||||
};
|
||||
|
||||
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 +152,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;
|
||||
@@ -38,13 +173,17 @@ class NetworkService {
|
||||
});
|
||||
}
|
||||
|
||||
get serviceName(): string {
|
||||
return NetworkService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
async timeRequest(operation) {
|
||||
private async timeRequest<T>(operation: () => Promise<T>): Promise<{ response: T | null; responseTime: number; error: unknown }> {
|
||||
const start = process.hrtime.bigint();
|
||||
try {
|
||||
const response = await operation();
|
||||
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
|
||||
return { response, responseTime: elapsedMs };
|
||||
return { response, responseTime: elapsedMs, error: null };
|
||||
} catch (error) {
|
||||
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
|
||||
return { response: null, responseTime: elapsedMs, error };
|
||||
@@ -52,7 +191,7 @@ class NetworkService {
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
async requestStatus(monitor) {
|
||||
async requestStatus(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
const type = monitor?.type || "unknown";
|
||||
switch (type) {
|
||||
case this.TYPE_PING:
|
||||
@@ -70,11 +209,11 @@ class NetworkService {
|
||||
case this.TYPE_GAME:
|
||||
return await this.requestGame(monitor);
|
||||
default:
|
||||
return await this.handleUnsupportedType(type);
|
||||
return this.handleUnsupportedType(type);
|
||||
}
|
||||
}
|
||||
|
||||
async requestPing(monitor) {
|
||||
private async requestPing(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
try {
|
||||
if (!monitor?.url) {
|
||||
throw new Error("Monitor URL is required");
|
||||
@@ -94,44 +233,49 @@ class NetworkService {
|
||||
throw new Error("Ping failed - no result returned");
|
||||
}
|
||||
|
||||
const pingResponse = {
|
||||
monitorId: monitor.id,
|
||||
type: "ping",
|
||||
status: response.alive,
|
||||
code: 200,
|
||||
responseTime: response.time,
|
||||
message: "Success",
|
||||
const pingResponse = this.buildStatusResponse({
|
||||
monitor,
|
||||
payload: response,
|
||||
};
|
||||
overrides: {
|
||||
status: (response as { alive?: boolean })?.alive ?? false,
|
||||
code: 200,
|
||||
message: "Success",
|
||||
responseTime: (response as { time?: number })?.time ?? 0,
|
||||
payload: response,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
pingResponse.status = false;
|
||||
pingResponse.code = 200;
|
||||
pingResponse.code = this.PING_ERROR;
|
||||
pingResponse.message = "Ping failed";
|
||||
return pingResponse;
|
||||
}
|
||||
|
||||
return pingResponse;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestPing";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async requestHttp(monitor) {
|
||||
private async requestHttp(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
|
||||
const httpResponse = {
|
||||
monitorId: id,
|
||||
teamId: teamId,
|
||||
type,
|
||||
};
|
||||
const httpResponse = this.buildStatusResponse({
|
||||
monitor,
|
||||
overrides: {
|
||||
status: false,
|
||||
code: this.NETWORK_ERROR,
|
||||
message: "Request not executed",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
if (!url) {
|
||||
throw new Error("Monitor URL is required");
|
||||
}
|
||||
const config = {
|
||||
const config: Record<string, any> = {
|
||||
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
||||
};
|
||||
|
||||
@@ -145,7 +289,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")) {
|
||||
@@ -158,12 +302,14 @@ class NetworkService {
|
||||
payload = response.body;
|
||||
}
|
||||
|
||||
httpResponse.code = response.statusCode;
|
||||
httpResponse.status = response.ok;
|
||||
httpResponse.message = response.statusMessage;
|
||||
httpResponse.responseTime = response.timings.phases.total || 0;
|
||||
httpResponse.payload = payload;
|
||||
httpResponse.timings = response.timings || {};
|
||||
Object.assign(httpResponse, {
|
||||
code: response.statusCode,
|
||||
status: response.ok,
|
||||
message: response.statusMessage ?? "",
|
||||
responseTime: response.timings.phases.total || 0,
|
||||
payload,
|
||||
timings: response.timings,
|
||||
});
|
||||
|
||||
if (!expectedValue && !jsonPath) {
|
||||
return httpResponse;
|
||||
@@ -231,7 +377,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 +393,7 @@ class NetworkService {
|
||||
}
|
||||
}
|
||||
|
||||
async requestPageSpeed(monitor) {
|
||||
private async requestPageSpeed(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
try {
|
||||
const url = monitor.url;
|
||||
if (!url) {
|
||||
@@ -269,24 +415,24 @@ class NetworkService {
|
||||
...monitor,
|
||||
url: pageSpeedUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestPageSpeed";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async requestHardware(monitor) {
|
||||
private async requestHardware(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
try {
|
||||
return await this.requestHttp(monitor);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestHardware";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async requestDocker(monitor) {
|
||||
private async requestDocker(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
try {
|
||||
if (!monitor.url) {
|
||||
throw new Error("Monitor URL is required");
|
||||
@@ -297,10 +443,14 @@ class NetworkService {
|
||||
handleError: true, // Enable error handling
|
||||
});
|
||||
|
||||
const dockerResponse = {
|
||||
monitorId: monitor.id,
|
||||
type: monitor.type,
|
||||
};
|
||||
const dockerResponse = this.buildStatusResponse({
|
||||
monitor,
|
||||
overrides: {
|
||||
status: false,
|
||||
code: this.NETWORK_ERROR,
|
||||
message: "No response",
|
||||
},
|
||||
});
|
||||
|
||||
const containers = await docker.listContainers({ all: true });
|
||||
|
||||
@@ -309,18 +459,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;
|
||||
@@ -353,14 +503,15 @@ class NetworkService {
|
||||
method: "requestDocker",
|
||||
details: { url: monitor.url },
|
||||
});
|
||||
dockerResponse.status = 404;
|
||||
dockerResponse.status = false;
|
||||
dockerResponse.message = `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`;
|
||||
return dockerResponse;
|
||||
}
|
||||
|
||||
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 +526,14 @@ class NetworkService {
|
||||
}
|
||||
|
||||
return dockerResponse;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "requestDocker";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async requestPort(monitor) {
|
||||
private async requestPort(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
try {
|
||||
const { url, port } = monitor;
|
||||
const { response, responseTime, error } = await this.timeRequest(async () => {
|
||||
@@ -405,21 +556,22 @@ class NetworkService {
|
||||
reject(new Error("Connection timeout"));
|
||||
});
|
||||
|
||||
socket.on("error", (err) => {
|
||||
socket.on("error", (err: any) => {
|
||||
socket.destroy();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const portResponse = {
|
||||
code: 200,
|
||||
status: response.success,
|
||||
message: this.stringService.portSuccess,
|
||||
monitorId: monitor.id,
|
||||
type: monitor.type,
|
||||
responseTime: responseTime,
|
||||
};
|
||||
const portResponse = this.buildStatusResponse({
|
||||
monitor,
|
||||
overrides: {
|
||||
code: 200,
|
||||
status: (response as { success?: boolean })?.success ?? false,
|
||||
message: this.stringService.portSuccess,
|
||||
responseTime,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
portResponse.code = this.NETWORK_ERROR;
|
||||
@@ -429,30 +581,31 @@ class NetworkService {
|
||||
}
|
||||
|
||||
return portResponse;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
error.service = this.SERVICE_NAME;
|
||||
error.method = "requestTCP";
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async requestGame(monitor) {
|
||||
private async requestGame(monitor: Monitor): Promise<MonitorStatusResponse> {
|
||||
try {
|
||||
const { url, port, gameId } = monitor;
|
||||
|
||||
const gameResponse = {
|
||||
code: 200,
|
||||
status: true,
|
||||
message: "Success",
|
||||
monitorId: monitor.id,
|
||||
type: "game",
|
||||
};
|
||||
const gameResponse = this.buildStatusResponse({
|
||||
monitor,
|
||||
overrides: {
|
||||
code: 200,
|
||||
status: true,
|
||||
message: "Success",
|
||||
},
|
||||
});
|
||||
|
||||
const state = await this.GameDig.query({
|
||||
type: gameId,
|
||||
host: url,
|
||||
port: port,
|
||||
}).catch((error) => {
|
||||
}).catch((error: any) => {
|
||||
this.logger.warn({
|
||||
message: error.message,
|
||||
service: this.SERVICE_NAME,
|
||||
@@ -471,21 +624,25 @@ 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}`);
|
||||
err.service = this.SERVICE_NAME;
|
||||
err.method = "getStatus";
|
||||
throw err;
|
||||
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
|
||||
return {
|
||||
monitorId: "unknown",
|
||||
teamId: "unknown",
|
||||
type: "unknown",
|
||||
status: false,
|
||||
code: this.NETWORK_ERROR,
|
||||
message: `Unsupported type: ${type}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 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 +657,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 +674,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 +689,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 +697,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 +727,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,
|
||||
+59
-168
@@ -1,11 +1,38 @@
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import MonitorStats from "../../db/models/MonitorStats.js";
|
||||
import { CheckModel } from "@/db/models/index.js";
|
||||
import type {
|
||||
CheckErrorInfo,
|
||||
Monitor,
|
||||
MonitorStatusResponse,
|
||||
StatusChangeResult,
|
||||
Check,
|
||||
HardwareStatusPayload,
|
||||
PageSpeedStatusPayload,
|
||||
} 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 +44,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 +105,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 +116,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 +132,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 +147,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 +158,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,27 +168,17 @@ 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) => {
|
||||
const check = this.buildCheck(networkResponse);
|
||||
await this.insertCheck(check);
|
||||
|
||||
updateMonitorStatus = async (
|
||||
statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>,
|
||||
check: Check
|
||||
): Promise<StatusChangeResult> => {
|
||||
try {
|
||||
const { monitorId, teamId, status, code } = networkResponse;
|
||||
const { monitorId, teamId, status, code } = statusResponse;
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
|
||||
// Update running stats
|
||||
this.updateRunningStats({ monitor, networkResponse });
|
||||
this.updateRunningStats({ monitor, networkResponse: statusResponse });
|
||||
|
||||
// If the status window size has changed, empty
|
||||
while (monitor.statusWindow.length > monitor.statusWindowSize) {
|
||||
@@ -221,7 +228,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 +249,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 +265,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 +279,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,147 +300,31 @@ 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) => {
|
||||
const {
|
||||
monitorId,
|
||||
teamId,
|
||||
type,
|
||||
status,
|
||||
responseTime,
|
||||
code,
|
||||
message,
|
||||
payload,
|
||||
first_byte_took,
|
||||
body_read_took,
|
||||
dns_took,
|
||||
conn_took,
|
||||
connect_took,
|
||||
tls_took,
|
||||
timings,
|
||||
} = networkResponse;
|
||||
|
||||
const check = {
|
||||
metadata: {
|
||||
monitorId,
|
||||
teamId,
|
||||
type,
|
||||
},
|
||||
status,
|
||||
statusCode: code,
|
||||
responseTime,
|
||||
timings: timings || {},
|
||||
message,
|
||||
first_byte_took,
|
||||
body_read_took,
|
||||
dns_took,
|
||||
conn_took,
|
||||
connect_took,
|
||||
tls_took,
|
||||
};
|
||||
|
||||
if (type === "pagespeed") {
|
||||
if (typeof payload === "undefined") {
|
||||
this.logger.warn({
|
||||
message: "Failed to build check",
|
||||
service: this.SERVICE_NAME,
|
||||
method: "buildCheck",
|
||||
details: "empty payload",
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
const categories = payload?.lighthouseResult?.categories ?? {};
|
||||
const audits = payload?.lighthouseResult?.audits ?? {};
|
||||
const mapAudit = (audit) => {
|
||||
if (!audit || typeof audit !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
id: audit.id,
|
||||
title: audit.title,
|
||||
score: typeof audit.score === "number" ? audit.score : (audit.score ?? null),
|
||||
displayValue: audit.displayValue,
|
||||
numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined,
|
||||
numericUnit: audit.numericUnit,
|
||||
};
|
||||
};
|
||||
check.accessibility = (categories?.accessibility?.score || 0) * 100;
|
||||
check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100;
|
||||
check.seo = (categories?.seo?.score || 0) * 100;
|
||||
check.performance = (categories?.performance?.score || 0) * 100;
|
||||
check.audits = {
|
||||
cls: mapAudit(audits?.["cumulative-layout-shift"]),
|
||||
si: mapAudit(audits?.["speed-index"]),
|
||||
fcp: mapAudit(audits?.["first-contentful-paint"]),
|
||||
lcp: mapAudit(audits?.["largest-contentful-paint"]),
|
||||
tbt: mapAudit(audits?.["total-blocking-time"]),
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "hardware") {
|
||||
const { cpu, memory, disk, host, net } = payload?.data ?? {};
|
||||
const { errors } = payload?.errors ?? [];
|
||||
check.cpu = cpu ?? {};
|
||||
check.memory = memory ?? {};
|
||||
check.disk = disk ?? {};
|
||||
check.host = host ?? {};
|
||||
check.errors = errors ?? [];
|
||||
check.capture = payload?.capture ?? {};
|
||||
check.net = net ?? {};
|
||||
}
|
||||
return check;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a check into the database based on 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 {Promise<void>} A promise that resolves when the check is inserted.
|
||||
*/
|
||||
insertCheck = async (check) => {
|
||||
insertCheck = async (check: Check) => {
|
||||
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}`,
|
||||
details: error.details || `Error inserting check for monitor: ${check?.metadata.monitorId}`,
|
||||
stack: error.stack,
|
||||
});
|
||||
}
|
||||
+37
-58
@@ -1,4 +1,6 @@
|
||||
import type { MonitorType } from "@/types/index.js";
|
||||
import type { Response } from "got";
|
||||
export type GotTimings = Response["timings"];
|
||||
|
||||
export interface CheckMetadata {
|
||||
monitorId: string;
|
||||
@@ -6,64 +8,41 @@ export interface CheckMetadata {
|
||||
type: MonitorType;
|
||||
}
|
||||
|
||||
export interface CheckTimingPhases {
|
||||
wait: number;
|
||||
dns: number;
|
||||
tcp: number;
|
||||
tls: number;
|
||||
request: number;
|
||||
firstByte: number;
|
||||
download: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface CheckTimings {
|
||||
start: number;
|
||||
socket: number;
|
||||
lookup: number;
|
||||
connect: number;
|
||||
secureConnect: number;
|
||||
upload: number;
|
||||
response: number;
|
||||
end: number;
|
||||
phases: CheckTimingPhases;
|
||||
}
|
||||
|
||||
export interface CheckCpuInfo {
|
||||
physical_core: number;
|
||||
logical_core: number;
|
||||
frequency: number;
|
||||
temperature: number[];
|
||||
free_percent: number;
|
||||
usage_percent: number;
|
||||
physical_core?: number;
|
||||
logical_core?: number;
|
||||
frequency?: number;
|
||||
temperature?: number[];
|
||||
free_percent?: number;
|
||||
usage_percent?: number;
|
||||
}
|
||||
|
||||
export interface CheckMemoryInfo {
|
||||
total_bytes: number;
|
||||
available_bytes: number;
|
||||
used_bytes: number;
|
||||
usage_percent: number;
|
||||
total_bytes?: number;
|
||||
available_bytes?: number;
|
||||
used_bytes?: number;
|
||||
usage_percent?: number;
|
||||
}
|
||||
|
||||
export interface CheckHostInfo {
|
||||
os: string;
|
||||
platform: string;
|
||||
kernel_version: string;
|
||||
os?: string;
|
||||
platform?: string;
|
||||
kernel_version?: string;
|
||||
}
|
||||
|
||||
export interface CheckCaptureInfo {
|
||||
version: string;
|
||||
mode: string;
|
||||
version?: string;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export interface CheckDiskInfo {
|
||||
device: string;
|
||||
mountpoint: string;
|
||||
read_speed_bytes: number;
|
||||
write_speed_bytes: number;
|
||||
total_bytes: number;
|
||||
free_bytes: number;
|
||||
usage_percent: number;
|
||||
device?: string;
|
||||
mountpoint?: string;
|
||||
read_speed_bytes?: number;
|
||||
write_speed_bytes?: number;
|
||||
total_bytes?: number;
|
||||
free_bytes?: number;
|
||||
usage_percent?: number;
|
||||
}
|
||||
|
||||
export interface CheckErrorInfo {
|
||||
@@ -86,11 +65,11 @@ export interface CheckNetworkInterfaceInfo {
|
||||
}
|
||||
|
||||
export interface CheckAudits {
|
||||
cls: ILighthouseAudit;
|
||||
si: ILighthouseAudit;
|
||||
fcp: ILighthouseAudit;
|
||||
lcp: ILighthouseAudit;
|
||||
tbt: ILighthouseAudit;
|
||||
cls?: ILighthouseAudit;
|
||||
si?: ILighthouseAudit;
|
||||
fcp?: ILighthouseAudit;
|
||||
lcp?: ILighthouseAudit;
|
||||
tbt?: ILighthouseAudit;
|
||||
}
|
||||
|
||||
export interface ILighthouseAudit {
|
||||
@@ -107,19 +86,19 @@ export interface Check {
|
||||
metadata: CheckMetadata;
|
||||
status: boolean;
|
||||
responseTime: number;
|
||||
timings: CheckTimings;
|
||||
timings?: GotTimings;
|
||||
statusCode: number;
|
||||
message: string;
|
||||
ack: boolean;
|
||||
ackAt?: string | null;
|
||||
expiry: string;
|
||||
cpu: CheckCpuInfo;
|
||||
memory: CheckMemoryInfo;
|
||||
disk: CheckDiskInfo[];
|
||||
host: CheckHostInfo;
|
||||
errors: CheckErrorInfo[];
|
||||
capture: CheckCaptureInfo;
|
||||
net: CheckNetworkInterfaceInfo[];
|
||||
cpu?: CheckCpuInfo;
|
||||
memory?: CheckMemoryInfo;
|
||||
disk?: CheckDiskInfo[];
|
||||
host?: CheckHostInfo;
|
||||
errors?: CheckErrorInfo[];
|
||||
capture?: CheckCaptureInfo;
|
||||
net?: CheckNetworkInterfaceInfo[];
|
||||
accessibility?: number;
|
||||
bestPractices?: number;
|
||||
seo?: number;
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "@/types/check.js";
|
||||
export * from "@/types/monitor.js";
|
||||
export * from "@/types/monitorStats.js";
|
||||
export * from "@/types/statusPage.js";
|
||||
export * from "@/types/network.js";
|
||||
@@ -1,4 +1,4 @@
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"] as const;
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export interface MonitorThresholds {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import type {
|
||||
CheckCaptureInfo,
|
||||
CheckCpuInfo,
|
||||
CheckDiskInfo,
|
||||
CheckErrorInfo,
|
||||
CheckHostInfo,
|
||||
CheckMemoryInfo,
|
||||
CheckNetworkInterfaceInfo,
|
||||
GotTimings,
|
||||
ILighthouseAudit,
|
||||
Monitor,
|
||||
MonitorMatchMethod,
|
||||
MonitorType,
|
||||
} from "@/types/index.js";
|
||||
|
||||
export interface MonitorStatusResponse<T = unknown> {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
type: MonitorType;
|
||||
status: boolean;
|
||||
code: number;
|
||||
message: string;
|
||||
responseTime?: number;
|
||||
payload?: T | null;
|
||||
timings?: GotTimings;
|
||||
first_byte_took?: number;
|
||||
body_read_took?: number;
|
||||
dns_took?: number;
|
||||
conn_took?: number;
|
||||
connect_took?: number;
|
||||
tls_took?: number;
|
||||
jsonPath?: string;
|
||||
matchMethod?: MonitorMatchMethod;
|
||||
expectedValue?: string;
|
||||
extracted?: unknown;
|
||||
}
|
||||
|
||||
export interface PingStatusPayload {
|
||||
host: string;
|
||||
numeric_host?: string;
|
||||
alive: boolean;
|
||||
time: number;
|
||||
times?: number[];
|
||||
output?: string;
|
||||
min?: string;
|
||||
max?: string;
|
||||
avg?: string;
|
||||
stddev?: string;
|
||||
packetLoss?: string;
|
||||
}
|
||||
|
||||
export type HttpStatusPayload = unknown;
|
||||
|
||||
export interface PageSpeedCategoryScore {
|
||||
score?: number | null;
|
||||
}
|
||||
|
||||
export interface PageSpeedStatusPayload {
|
||||
lighthouseResult?: {
|
||||
categories?: {
|
||||
accessibility?: PageSpeedCategoryScore;
|
||||
"best-practices"?: PageSpeedCategoryScore;
|
||||
performance?: PageSpeedCategoryScore;
|
||||
seo?: PageSpeedCategoryScore;
|
||||
[key: string]: PageSpeedCategoryScore | undefined;
|
||||
};
|
||||
audits?: Record<string, ILighthouseAudit | undefined>;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface HardwareStatusMetrics {
|
||||
cpu?: CheckCpuInfo;
|
||||
memory?: CheckMemoryInfo;
|
||||
disk?: CheckDiskInfo[];
|
||||
host?: CheckHostInfo;
|
||||
net?: CheckNetworkInterfaceInfo[];
|
||||
}
|
||||
|
||||
export interface HardwareStatusPayload {
|
||||
data?: HardwareStatusMetrics;
|
||||
errors?: CheckErrorInfo[] | { errors?: CheckErrorInfo[] };
|
||||
capture?: CheckCaptureInfo;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type DockerStatusPayload = Record<string, unknown>;
|
||||
|
||||
export interface PortStatusPayload {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export type GameStatusPayload = Record<string, unknown>;
|
||||
|
||||
export interface MonitorPayloadMap {
|
||||
ping: PingStatusPayload;
|
||||
http: HttpStatusPayload;
|
||||
pagespeed: PageSpeedStatusPayload;
|
||||
hardware: HardwareStatusPayload;
|
||||
docker: DockerStatusPayload;
|
||||
port: PortStatusPayload;
|
||||
game: GameStatusPayload;
|
||||
default: unknown;
|
||||
}
|
||||
|
||||
export type StatusChangeResult = {
|
||||
monitor: Monitor;
|
||||
statusChanged: boolean;
|
||||
prevStatus: boolean | undefined;
|
||||
code: number;
|
||||
timestamp: number;
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
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(), debug: jest.fn() });
|
||||
|
||||
const createHelper = (overrides?: Partial<ConstructorParameters<typeof SuperSimpleQueueHelper>[0]>) => {
|
||||
const maintenanceWindowModule = {
|
||||
getMaintenanceWindowsByMonitorId: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
const statusServiceMock = {
|
||||
updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: { id: "m1" }, statusChanged: true, prevStatus: false }),
|
||||
};
|
||||
const helper = new SuperSimpleQueueHelper({
|
||||
db: { maintenanceWindowModule },
|
||||
logger: createLogger(),
|
||||
networkService: { requestStatus: jest.fn() },
|
||||
statusService: statusServiceMock,
|
||||
notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) },
|
||||
...overrides,
|
||||
});
|
||||
return { helper, maintenanceWindowModule };
|
||||
};
|
||||
|
||||
describe("SuperSimpleQueueHelper", () => {
|
||||
describe("getMonitorJob", () => {
|
||||
it("skips execution when monitor is in maintenance window", async () => {
|
||||
const { helper } = createHelper();
|
||||
const spy = jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(true);
|
||||
const job = helper.getMonitorJob();
|
||||
await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor);
|
||||
expect(helper["networkService"].requestStatus).not.toHaveBeenCalled();
|
||||
expect(helper["logger"].debug).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") })
|
||||
);
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("processes monitor status and notifications when active", async () => {
|
||||
const networkResponse = { monitor: { id: "m1" }, status: true };
|
||||
const updatedMonitor = { id: "m1", status: true };
|
||||
const { helper } = createHelper({
|
||||
networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) },
|
||||
statusService: {
|
||||
updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }),
|
||||
},
|
||||
notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) },
|
||||
});
|
||||
jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(false);
|
||||
const job = helper.getMonitorJob();
|
||||
const monitor = { id: "m1", teamId: "team" } as Monitor;
|
||||
await job(monitor);
|
||||
expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor);
|
||||
expect(helper["statusService"].updateMonitorStatus).toHaveBeenCalledWith(networkResponse);
|
||||
expect(helper["notificationService"].handleNotifications).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ monitor: updatedMonitor, statusChanged: true, prevStatus: false })
|
||||
);
|
||||
});
|
||||
|
||||
it("throws when monitor id is missing", async () => {
|
||||
const { helper } = createHelper();
|
||||
const job = helper.getMonitorJob();
|
||||
await expect(job({} as Monitor)).rejects.toThrow("No monitor id");
|
||||
expect(helper["logger"].warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isInMaintenanceWindow", () => {
|
||||
it("returns true when an active window spans now", async () => {
|
||||
const now = new Date();
|
||||
const { helper, maintenanceWindowModule } = createHelper();
|
||||
maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([
|
||||
{
|
||||
active: true,
|
||||
start: new Date(now.getTime() - 1000).toISOString(),
|
||||
end: new Date(now.getTime() + 1000).toISOString(),
|
||||
repeat: 0,
|
||||
},
|
||||
]);
|
||||
await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when repeat interval advances window into current time", async () => {
|
||||
const now = Date.now();
|
||||
const { helper, maintenanceWindowModule } = createHelper();
|
||||
maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([
|
||||
{
|
||||
active: true,
|
||||
start: new Date(now - 7200000).toISOString(),
|
||||
end: new Date(now - 6600000).toISOString(),
|
||||
repeat: 3600000,
|
||||
},
|
||||
]);
|
||||
await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when no active windows exist", async () => {
|
||||
const { helper } = createHelper();
|
||||
await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,8 @@
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["jest"],
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"] // allows "@/db" -> "./src/db"
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
|
||||
Reference in new issue
Block a user