mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-19 07:58:46 -05:00
hardware details
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
rootDir: ".",
|
||||
testEnvironment: "node",
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
transform: {
|
||||
"^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
},
|
||||
testMatch: ["<rootDir>/test/**/*.test.ts"],
|
||||
setupFilesAfterEnv: [],
|
||||
collectCoverageFrom: ["src/**/*.ts"],
|
||||
coveragePathIgnorePatterns: ["/node_modules/", "/test/"],
|
||||
clearMocks: true,
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
+3304
-359
File diff suppressed because it is too large
Load Diff
+10
-4
@@ -5,7 +5,7 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "c8 mocha",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand",
|
||||
"dev": "nodemon --exec tsx src/index.js",
|
||||
"start": "node --watch ./dist/index.js",
|
||||
"build": "tsc && tsc-alias",
|
||||
@@ -58,21 +58,27 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/dockerode": "^4.0.0",
|
||||
"@types/express": "5.0.3",
|
||||
"@types/gamedig": "^5.0.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jmespath": "^0.15.2",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/mjml": "^4.7.4",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/nodemailer": "7.0.1",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/ping": "0.4.4",
|
||||
"c8": "10.1.3",
|
||||
"chai": "5.2.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-mocha": "^10.5.0",
|
||||
"esm": "3.2.25",
|
||||
"globals": "^15.14.0",
|
||||
"mocha": "11.1.0",
|
||||
"jest": "^30.2.0",
|
||||
"nodemon": "^3.1.11",
|
||||
"prettier": "^3.3.3",
|
||||
"sinon": "19.0.2",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.20.5",
|
||||
"typescript": "5.9.2"
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import mongoose from "mongoose";
|
||||
import { MonitorModel } from "../dist/db/models/Monitor.js";
|
||||
import { CheckModel } from "../dist/db/models/Check.js";
|
||||
|
||||
const DEFAULT_MONITOR_ID = "000000000000000000000001";
|
||||
const DEFAULT_TEAM_ID = "0000000000000000000000aa";
|
||||
const DEFAULT_USER_ID = "0000000000000000000000bb";
|
||||
const DEFAULT_MONITOR_TYPE = "http";
|
||||
const DEFAULT_TOTAL = 1_000_000;
|
||||
const DEFAULT_BATCH_SIZE = 5_000;
|
||||
|
||||
const parseObjectId = (value, fallback) => {
|
||||
try {
|
||||
return new mongoose.Types.ObjectId(value || fallback);
|
||||
} catch (error) {
|
||||
console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`);
|
||||
return new mongoose.Types.ObjectId(fallback);
|
||||
}
|
||||
};
|
||||
|
||||
async function ensureMonitor({ monitorId, teamId, userId, type }) {
|
||||
const existing = await MonitorModel.findById(monitorId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
console.log(`Monitor ${monitorId.toString()} not found, creating it.`);
|
||||
const monitor = new MonitorModel({
|
||||
_id: monitorId,
|
||||
userId,
|
||||
teamId,
|
||||
name: `Seed Monitor ${monitorId.toString()}`,
|
||||
description: "Synthetic monitor for performance testing",
|
||||
statusWindow: [],
|
||||
statusWindowSize: 5,
|
||||
statusWindowThreshold: 60,
|
||||
type,
|
||||
ignoreTlsErrors: false,
|
||||
url: "https://example.com",
|
||||
isActive: true,
|
||||
interval: 60000,
|
||||
alertThreshold: 5,
|
||||
cpuAlertThreshold: 5,
|
||||
memoryAlertThreshold: 5,
|
||||
diskAlertThreshold: 5,
|
||||
tempAlertThreshold: 5,
|
||||
selectedDisks: [],
|
||||
});
|
||||
|
||||
await monitor.save();
|
||||
return monitor;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const mongoUri = process.env.MONGO_URI ?? "mongodb://localhost:27017/uptime_db";
|
||||
const monitorId = parseObjectId(process.env.MONITOR_ID ?? DEFAULT_MONITOR_ID, DEFAULT_MONITOR_ID);
|
||||
const teamId = parseObjectId("69648b0578209af45f9ffe30");
|
||||
const userId = parseObjectId("69648b0678209af45f9ffe32");
|
||||
const monitorType = process.env.MONITOR_TYPE ?? DEFAULT_MONITOR_TYPE;
|
||||
const total = Number(process.env.CHECK_TOTAL ?? DEFAULT_TOTAL);
|
||||
const batchSize = Number(process.env.CHECK_BATCH_SIZE ?? DEFAULT_BATCH_SIZE);
|
||||
|
||||
console.log(`Connecting to MongoDB at ${mongoUri}`);
|
||||
await mongoose.connect(mongoUri);
|
||||
|
||||
await ensureMonitor({ monitorId, teamId, userId, type: monitorType });
|
||||
|
||||
console.log(`Seeding ${total} checks for monitor ${monitorId.toString()} (team ${teamId.toString()}) in batches of ${batchSize}.`);
|
||||
|
||||
const docs = [];
|
||||
const startTime = Date.now();
|
||||
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
const baseTime = Date.now() - (total - i) * 1000;
|
||||
const createdAt = new Date(baseTime);
|
||||
docs.push({
|
||||
metadata: {
|
||||
monitorId,
|
||||
teamId,
|
||||
type: monitorType,
|
||||
},
|
||||
status: i % 50 !== 0,
|
||||
statusCode: i % 50 !== 0 ? 200 : 500,
|
||||
responseTime: Math.floor(Math.random() * 1000),
|
||||
message: i % 50 !== 0 ? "OK" : "Error",
|
||||
expiry: createdAt,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
timings: {
|
||||
start: baseTime,
|
||||
socket: baseTime,
|
||||
lookup: baseTime,
|
||||
connect: baseTime,
|
||||
secureConnect: baseTime,
|
||||
upload: baseTime,
|
||||
response: baseTime + 40,
|
||||
end: baseTime + 45,
|
||||
phases: {
|
||||
wait: 0,
|
||||
dns: 1,
|
||||
tcp: 2,
|
||||
tls: 4,
|
||||
request: 0,
|
||||
firstByte: 30,
|
||||
download: 5,
|
||||
total: 45,
|
||||
},
|
||||
},
|
||||
cpu: {
|
||||
physical_core: 8,
|
||||
logical_core: 16,
|
||||
frequency: 3600,
|
||||
temperature: [50 + Math.random() * 10],
|
||||
free_percent: 40,
|
||||
usage_percent: Math.random() * 100,
|
||||
},
|
||||
memory: {
|
||||
total_bytes: 32 * 1024 ** 3,
|
||||
available_bytes: 16 * 1024 ** 3,
|
||||
used_bytes: 16 * 1024 ** 3,
|
||||
usage_percent: Math.random() * 100,
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
device: "/dev/sda1",
|
||||
mountpoint: "/",
|
||||
read_speed_bytes: Math.random() * 10_000_000,
|
||||
write_speed_bytes: Math.random() * 10_000_000,
|
||||
total_bytes: 512 * 1024 ** 3,
|
||||
free_bytes: 128 * 1024 ** 3,
|
||||
usage_percent: Math.random() * 100,
|
||||
},
|
||||
],
|
||||
host: {
|
||||
os: "linux",
|
||||
platform: "ubuntu",
|
||||
kernel_version: "5.15.0",
|
||||
},
|
||||
net: [
|
||||
{
|
||||
name: "eth0",
|
||||
bytes_sent: Math.random() * 10_000_000,
|
||||
bytes_recv: Math.random() * 10_000_000,
|
||||
packets_sent: Math.random() * 1_000_000,
|
||||
packets_recv: Math.random() * 1_000_000,
|
||||
err_in: 0,
|
||||
err_out: 0,
|
||||
drop_in: 0,
|
||||
drop_out: 0,
|
||||
fifo_in: 0,
|
||||
fifo_out: 0,
|
||||
},
|
||||
],
|
||||
errors: i % 50 === 0 ? [{ metric: ["uptime"], err: "500" }] : [],
|
||||
});
|
||||
|
||||
if (docs.length === batchSize) {
|
||||
await CheckModel.insertMany(docs, { ordered: false });
|
||||
console.log(`Inserted ${i + 1} / ${total}`);
|
||||
docs.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (docs.length > 0) {
|
||||
await CheckModel.insertMany(docs, { ordered: false });
|
||||
}
|
||||
|
||||
await mongoose.disconnect();
|
||||
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
console.log(`Finished inserting ${total} checks in ${duration}s`);
|
||||
}
|
||||
|
||||
run().catch((error) => {
|
||||
console.error("Failed to seed checks", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -34,9 +34,8 @@ const { compile } = pkg;
|
||||
import mjml2html from "mjml";
|
||||
import jwt from "jsonwebtoken";
|
||||
import crypto from "crypto";
|
||||
import { games } from "gamedig";
|
||||
import { games, GameDig } from "gamedig";
|
||||
import jmespath from "jmespath";
|
||||
import { GameDig } from "gamedig";
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import { ObjectId } from "mongodb";
|
||||
@@ -47,7 +46,6 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js";
|
||||
import { ParseBoolean } from "../utils/utils.js";
|
||||
|
||||
// Models
|
||||
import { CheckModel } from "@/db/models/index.js";
|
||||
import Monitor from "../db/models/Monitor.js";
|
||||
import User from "../db/models/User.js";
|
||||
import InviteToken from "../db/models/InviteToken.js";
|
||||
@@ -72,11 +70,11 @@ import SettingsModule from "../db/modules/settingsModule.js";
|
||||
import IncidentModule from "../db/modules/incidentModule.js";
|
||||
|
||||
// repositories
|
||||
import { MongoMonitorsRepository, MongoChecksRepository } from "@/repositories/index.js";
|
||||
import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository } from "@/repositories/index.js";
|
||||
|
||||
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
|
||||
export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => {
|
||||
const serviceRegistry = new ServiceRegistry({ logger });
|
||||
ServiceRegistry.instance = serviceRegistry;
|
||||
(ServiceRegistry as any).instance = serviceRegistry;
|
||||
|
||||
const translationService = new TranslationService(logger);
|
||||
await translationService.initialize();
|
||||
@@ -84,7 +82,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
const stringService = new StringService(translationService);
|
||||
|
||||
// Create DB
|
||||
const checkModule = new CheckModule({ logger, CheckModel, Monitor, User });
|
||||
const checkModule = new CheckModule({ logger, Monitor, User });
|
||||
const inviteModule = new InviteModule({ InviteToken, crypto, stringService });
|
||||
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService });
|
||||
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
|
||||
@@ -125,6 +123,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
// Repositories
|
||||
const monitorsRepository = new MongoMonitorsRepository();
|
||||
const checksRepository = new MongoChecksRepository();
|
||||
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||
|
||||
const networkService = new NetworkService({
|
||||
axios,
|
||||
@@ -228,6 +227,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
|
||||
games,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
});
|
||||
|
||||
const services = {
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
createMonitorBodyValidation,
|
||||
editMonitorBodyValidation,
|
||||
pauseMonitorParamValidation,
|
||||
getMonitorStatsByIdParamValidation,
|
||||
getMonitorStatsByIdQueryValidation,
|
||||
getCertificateParamValidation,
|
||||
getHardwareDetailsByIdParamValidation,
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
@@ -90,39 +88,6 @@ class MonitorController {
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorStatsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
|
||||
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
|
||||
|
||||
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
|
||||
const monitorId = req?.params?.monitorId;
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new AppError({ message: "Team ID is required", status: 400 });
|
||||
}
|
||||
|
||||
const monitorStats = await this.monitorService.getMonitorStatsById({
|
||||
teamId,
|
||||
monitorId,
|
||||
limit,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: "Monitor stats retrieved successfully",
|
||||
data: monitorStats,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
|
||||
|
||||
@@ -11,12 +11,9 @@ class MongoDB {
|
||||
inviteModule,
|
||||
statusPageModule,
|
||||
userModule,
|
||||
hardwareCheckModule,
|
||||
maintenanceWindowModule,
|
||||
monitorModule,
|
||||
networkCheckModule,
|
||||
notificationModule,
|
||||
pageSpeedCheckModule,
|
||||
recoveryModule,
|
||||
settingsModule,
|
||||
incidentModule,
|
||||
@@ -26,15 +23,12 @@ class MongoDB {
|
||||
this.userModule = userModule;
|
||||
this.inviteModule = inviteModule;
|
||||
this.recoveryModule = recoveryModule;
|
||||
this.pageSpeedCheckModule = pageSpeedCheckModule;
|
||||
this.hardwareCheckModule = hardwareCheckModule;
|
||||
this.checkModule = checkModule;
|
||||
this.maintenanceWindowModule = maintenanceWindowModule;
|
||||
this.monitorModule = monitorModule;
|
||||
this.notificationModule = notificationModule;
|
||||
this.settingsModule = settingsModule;
|
||||
this.statusPageModule = statusPageModule;
|
||||
this.networkCheckModule = networkCheckModule;
|
||||
this.incidentModule = incidentModule;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const MonitorStatsSchema = new mongoose.Schema(
|
||||
{
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
avgResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalUpChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalDownChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
uptimePercentage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastCheckTimestamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
timeOfLastFailure: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const MonitorStats = mongoose.model("MonitorStats", MonitorStatsSchema);
|
||||
|
||||
export default MonitorStats;
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
import { Schema, model, type Types } from "mongoose";
|
||||
import type { MonitorStats as MonitorStatsEntity } from "@/types/monitorStats.js";
|
||||
|
||||
type MonitorStatsDocumentBase = Omit<MonitorStatsEntity, "id" | "monitorId" | "createdAt" | "updatedAt"> & {
|
||||
monitorId: Types.ObjectId;
|
||||
};
|
||||
|
||||
interface MonitorStatsDocument extends MonitorStatsDocumentBase {
|
||||
_id: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MonitorStatsSchema = new Schema<MonitorStatsDocument>(
|
||||
{
|
||||
monitorId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
avgResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalUpChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalDownChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
uptimePercentage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastCheckTimestamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
timeOfLastFailure: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const MonitorStatsModel = model<MonitorStatsDocument>("MonitorStats", MonitorStatsSchema);
|
||||
|
||||
export type { MonitorStatsDocument };
|
||||
export { MonitorStatsModel };
|
||||
export default MonitorStatsModel;
|
||||
@@ -3,3 +3,6 @@ export { default as MonitorModel } from "@/db/models/Monitor.js";
|
||||
|
||||
export * from "@/db/models/Check.js";
|
||||
export { default as CheckModel } from "@/db/models/Check.js";
|
||||
|
||||
export * from "@/db/models/MonitorStats.js";
|
||||
export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js";
|
||||
@@ -253,48 +253,6 @@ class MonitorModule {
|
||||
}
|
||||
};
|
||||
|
||||
getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => {
|
||||
try {
|
||||
// Get monitor, if we can't find it, abort with error
|
||||
const monitor = await this.Monitor.findById(monitorId);
|
||||
if (monitor === null || monitor === undefined) {
|
||||
throw new Error(this.stringService.getDbFindMonitorById(monitorId));
|
||||
}
|
||||
|
||||
// Get query params
|
||||
const sort = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
// Get Checks for monitor in date range requested
|
||||
const dates = this.getDateRange(dateRange);
|
||||
const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort);
|
||||
|
||||
// Build monitor stats
|
||||
const monitorStats = {
|
||||
...monitor.toObject(),
|
||||
uptimeDuration: this.calculateUptimeDuration(checksAll),
|
||||
lastChecked: this.getLastChecked(checksAll),
|
||||
latestResponseTime: this.getLatestResponseTime(checksAll),
|
||||
periodIncidents: this.getIncidents(checksForDateRange),
|
||||
periodTotalChecks: checksForDateRange.length,
|
||||
checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize),
|
||||
};
|
||||
|
||||
if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") {
|
||||
// HTTP/PING Specific stats
|
||||
monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange);
|
||||
monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange);
|
||||
const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange);
|
||||
monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats);
|
||||
}
|
||||
|
||||
return monitorStats;
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getMonitorStatsById";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async ({ monitorId, dateRange }) => {
|
||||
try {
|
||||
const monitor = await this.Monitor.findById(monitorId);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Check, MonitorType } from "@/types/index.js";
|
||||
import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
|
||||
|
||||
export interface IChecksRepository {
|
||||
@@ -6,12 +7,53 @@ export interface IChecksRepository {
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
): Promise<{
|
||||
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
|
||||
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
}>;
|
||||
dateString: string,
|
||||
options?: { type?: MonitorType }
|
||||
): Promise<
|
||||
| {
|
||||
monitorType: "uptime";
|
||||
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
|
||||
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
}
|
||||
| {
|
||||
monitorType: "hardware";
|
||||
aggregateData: {
|
||||
latestCheck: Check | null;
|
||||
totalChecks: number;
|
||||
};
|
||||
upChecks: {
|
||||
totalChecks: number;
|
||||
};
|
||||
checks: Array<{
|
||||
_id: string;
|
||||
avgCpuUsage: number;
|
||||
avgMemoryUsage: number;
|
||||
avgTemperature: number[];
|
||||
disks: Array<{
|
||||
name: string;
|
||||
readSpeed: number;
|
||||
writeSpeed: number;
|
||||
totalBytes: number;
|
||||
freeBytes: number;
|
||||
usagePercent: number;
|
||||
}>;
|
||||
net: Array<{
|
||||
name: string;
|
||||
bytesSentPerSecond: number;
|
||||
deltaBytesRecv: number;
|
||||
deltaPacketsSent: number;
|
||||
deltaPacketsRecv: number;
|
||||
deltaErrIn: number;
|
||||
deltaErrOut: number;
|
||||
deltaDropIn: number;
|
||||
deltaDropOut: number;
|
||||
deltaFifoIn: number;
|
||||
deltaFifoOut: number;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ import type {
|
||||
} from "@/types/index.js";
|
||||
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import {
|
||||
getAggregateData as getHardwareAggregateData,
|
||||
getHardwareStats,
|
||||
getUpChecks as getHardwareUpChecks,
|
||||
} from "@/db/modules/monitorModuleQueries.js";
|
||||
|
||||
export type LatestChecksMap = Record<string, Check[]>;
|
||||
|
||||
@@ -210,20 +215,17 @@ class MongoChecksRepistory implements IChecksRepository {
|
||||
}, {});
|
||||
};
|
||||
|
||||
findDateRangeChecksByMonitor = async (
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
): Promise<{
|
||||
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
|
||||
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
|
||||
uptimePercentage: number;
|
||||
avgResponseTime: number;
|
||||
}> => {
|
||||
findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: string }) => {
|
||||
const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
|
||||
if (options?.type === "hardware") {
|
||||
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
||||
}
|
||||
return this.findUptimeDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
|
||||
};
|
||||
|
||||
private findUptimeDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => {
|
||||
const matchStage = {
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
"metadata.monitorId": monitorObjectId,
|
||||
updatedAt: { $gte: startDate, $lte: endDate },
|
||||
};
|
||||
const [result] = await CheckModel.aggregate([
|
||||
@@ -296,12 +298,13 @@ class MongoChecksRepistory implements IChecksRepository {
|
||||
],
|
||||
},
|
||||
},
|
||||
]).exec();
|
||||
]);
|
||||
|
||||
const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0;
|
||||
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
|
||||
|
||||
return {
|
||||
monitorType: "uptime" as const,
|
||||
groupedChecks: result?.groupedChecks ?? [],
|
||||
groupedUpChecks: result?.groupedUpChecks ?? [],
|
||||
groupedDownChecks: result?.groupedDownChecks ?? [],
|
||||
@@ -309,6 +312,60 @@ class MongoChecksRepistory implements IChecksRepository {
|
||||
avgResponseTime,
|
||||
};
|
||||
};
|
||||
|
||||
private findHardwareDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => {
|
||||
const monitorId = monitorObjectId.toHexString();
|
||||
const dates = { start: startDate, end: endDate };
|
||||
const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([
|
||||
getHardwareAggregateData(monitorId, dates),
|
||||
getHardwareUpChecks(monitorId, dates),
|
||||
getHardwareStats(monitorId, dates, dateString),
|
||||
]);
|
||||
|
||||
const aggregateData = {
|
||||
latestCheck: aggregateDataDoc?.latestCheck ? this.toEntity(aggregateDataDoc.latestCheck as CheckDocument) : null,
|
||||
totalChecks: aggregateDataDoc?.totalChecks ?? 0,
|
||||
};
|
||||
|
||||
const upChecks = {
|
||||
totalChecks: upChecksDoc?.totalChecks ?? 0,
|
||||
};
|
||||
|
||||
const checks = (hardwareMetrics ?? []).map((metric) => ({
|
||||
_id: metric._id,
|
||||
avgCpuUsage: metric.avgCpuUsage ?? 0,
|
||||
avgMemoryUsage: metric.avgMemoryUsage ?? 0,
|
||||
avgTemperature: metric.avgTemperature ?? [],
|
||||
disks: (metric.disks ?? []).map((disk: { [key: string]: number | string | undefined }) => ({
|
||||
name: disk?.name ?? "",
|
||||
readSpeed: disk?.readSpeed ?? 0,
|
||||
writeSpeed: disk?.writeSpeed ?? 0,
|
||||
totalBytes: disk?.totalBytes ?? 0,
|
||||
freeBytes: disk?.freeBytes ?? 0,
|
||||
usagePercent: disk?.usagePercent ?? 0,
|
||||
})),
|
||||
net: (metric.net ?? []).map((iface: { [key: string]: number | string | undefined }) => ({
|
||||
name: iface?.name ?? "",
|
||||
bytesSentPerSecond: iface?.bytesSentPerSecond ?? 0,
|
||||
deltaBytesRecv: iface?.deltaBytesRecv ?? 0,
|
||||
deltaPacketsSent: iface?.deltaPacketsSent ?? 0,
|
||||
deltaPacketsRecv: iface?.deltaPacketsRecv ?? 0,
|
||||
deltaErrIn: iface?.deltaErrIn ?? 0,
|
||||
deltaErrOut: iface?.deltaErrOut ?? 0,
|
||||
deltaDropIn: iface?.deltaDropIn ?? 0,
|
||||
deltaDropOut: iface?.deltaDropOut ?? 0,
|
||||
deltaFifoIn: iface?.deltaFifoIn ?? 0,
|
||||
deltaFifoOut: iface?.deltaFifoOut ?? 0,
|
||||
})),
|
||||
}));
|
||||
|
||||
return {
|
||||
monitorType: "hardware" as const,
|
||||
aggregateData,
|
||||
upChecks,
|
||||
checks,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoChecksRepistory;
|
||||
|
||||
@@ -3,3 +3,6 @@ export { default as MongoMonitorsRepository } from "@/repositories/monitors/Mong
|
||||
|
||||
export * from "@/repositories/checks/IChecksRepository.js";
|
||||
export { default as MongoChecksRepository } from "@/repositories/checks/MongoChecksRepistory.js";
|
||||
|
||||
export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js";
|
||||
export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js";
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { MonitorStats } from "@/types/index.js";
|
||||
export interface IMonitorStatsRepository {
|
||||
// create
|
||||
// single fetch
|
||||
findByMonitorId(monitorId: string): Promise<MonitorStats>;
|
||||
// update
|
||||
// delete
|
||||
// other
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { type MonitorStatsDocument, MonitorStatsModel } from "@/db/models/index.js";
|
||||
import type { MonitorStats } from "@/types/index.js";
|
||||
import { IMonitorStatsRepository } from "@/repositories/index.js";
|
||||
import mongoose from "mongoose";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
class MongoMonitorStatsRepository implements IMonitorStatsRepository {
|
||||
private toEntity = (doc: MonitorStatsDocument): MonitorStats => {
|
||||
const toStringId = (value: unknown): string => {
|
||||
if (value instanceof mongoose.Types.ObjectId) {
|
||||
return value.toString();
|
||||
}
|
||||
return value?.toString() ?? "";
|
||||
};
|
||||
|
||||
const toDateString = (value: Date | string): string => {
|
||||
return value instanceof Date ? value.toISOString() : value;
|
||||
};
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
monitorId: toStringId(doc.monitorId),
|
||||
avgResponseTime: doc.avgResponseTime,
|
||||
totalChecks: doc.totalChecks,
|
||||
totalUpChecks: doc.totalUpChecks,
|
||||
totalDownChecks: doc.totalDownChecks,
|
||||
uptimePercentage: doc.uptimePercentage,
|
||||
lastCheckTimestamp: doc.lastCheckTimestamp,
|
||||
lastResponseTime: doc.lastResponseTime,
|
||||
timeOfLastFailure: doc.timeOfLastFailure,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
findByMonitorId = async (monitorId: string): Promise<MonitorStats> => {
|
||||
const monitorStats = await MonitorStatsModel.findOne({ monitorId: new mongoose.Types.ObjectId(monitorId) });
|
||||
if (!monitorStats) {
|
||||
throw new AppError({ message: "Monitor stats not found", status: 404 });
|
||||
}
|
||||
return this.toEntity(monitorStats);
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoMonitorStatsRepository;
|
||||
@@ -30,7 +30,6 @@ class MonitorRoutes {
|
||||
|
||||
// General monitor routes
|
||||
this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor);
|
||||
this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById);
|
||||
|
||||
// Util routes
|
||||
this.router.get("/certificate/:monitorId", (req, res, next) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js";
|
||||
import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js";
|
||||
import { type Monitor } from "@/types/index.js";
|
||||
import type { MonitorType } from "@/types/monitor.js";
|
||||
import type { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||
import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository } from "@/repositories/index.js";
|
||||
import fs from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
@@ -23,17 +23,8 @@ export interface IMonitorService {
|
||||
|
||||
// read
|
||||
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<any>;
|
||||
getMonitorStatsById(args: {
|
||||
teamId: string;
|
||||
monitorId: string;
|
||||
limit?: number;
|
||||
sortOrder?: 1 | -1;
|
||||
dateRange?: string;
|
||||
numToDisplay?: number;
|
||||
normalize?: boolean;
|
||||
}): Promise<any>;
|
||||
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
|
||||
getMonitorById(args: { teamId: string; monitorId: string }): Promise<any>;
|
||||
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
||||
getMonitorsByTeamId(args: {
|
||||
teamId: string;
|
||||
limit?: number;
|
||||
@@ -86,7 +77,7 @@ export class MonitorService implements IMonitorService {
|
||||
private games: any;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
private checksRepository: IChecksRepository;
|
||||
private fs: any;
|
||||
private monitorStatsRepository: IMonitorStatsRepository;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
@@ -99,6 +90,7 @@ export class MonitorService implements IMonitorService {
|
||||
games,
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
}: {
|
||||
db: any;
|
||||
jobQueue: any;
|
||||
@@ -110,6 +102,7 @@ export class MonitorService implements IMonitorService {
|
||||
games: any;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
checksRepository: IChecksRepository;
|
||||
monitorStatsRepository: IMonitorStatsRepository;
|
||||
}) {
|
||||
this.db = db;
|
||||
this.jobQueue = jobQueue;
|
||||
@@ -121,6 +114,7 @@ export class MonitorService implements IMonitorService {
|
||||
this.games = games;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
this.checksRepository = checksRepository;
|
||||
this.monitorStatsRepository = monitorStatsRepository;
|
||||
}
|
||||
|
||||
get serviceName(): string {
|
||||
@@ -269,10 +263,14 @@ export class MonitorService implements IMonitorService {
|
||||
}
|
||||
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
||||
const { start, end } = this.getDateRange(rangeKey);
|
||||
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey));
|
||||
const monitorStats = await this.db.monitorModule.getMonitorStatsById({
|
||||
monitorId,
|
||||
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
||||
type: monitor.type,
|
||||
});
|
||||
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
|
||||
|
||||
if (checksData.monitorType !== "uptime") {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
||||
}
|
||||
|
||||
return {
|
||||
monitorData: {
|
||||
@@ -287,47 +285,41 @@ export class MonitorService implements IMonitorService {
|
||||
};
|
||||
};
|
||||
|
||||
getMonitorStatsById = async ({
|
||||
teamId,
|
||||
monitorId,
|
||||
limit,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
}: {
|
||||
teamId: string;
|
||||
monitorId: string;
|
||||
limit?: number;
|
||||
sortOrder?: 1 | -1;
|
||||
dateRange?: string;
|
||||
numToDisplay?: number;
|
||||
normalize?: boolean;
|
||||
}): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitorStats = await this.db.monitorModule.getMonitorStatsById({
|
||||
monitorId,
|
||||
limit,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
numToDisplay,
|
||||
normalize,
|
||||
});
|
||||
|
||||
return monitorStats;
|
||||
};
|
||||
|
||||
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.getHardwareDetailsById({ monitorId, dateRange });
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
if (!monitor) {
|
||||
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
||||
}
|
||||
if (monitor.type !== "hardware") {
|
||||
throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 });
|
||||
}
|
||||
|
||||
return monitor;
|
||||
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
||||
const { start, end } = this.getDateRange(rangeKey);
|
||||
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
||||
type: monitor.type,
|
||||
});
|
||||
|
||||
if (checksData.monitorType !== "hardware") {
|
||||
throw new AppError({ message: "Unable to load hardware stats for this monitor", status: 500 });
|
||||
}
|
||||
|
||||
const stats = {
|
||||
aggregateData: checksData.aggregateData,
|
||||
upChecks: checksData.upChecks,
|
||||
checks: checksData.checks,
|
||||
};
|
||||
|
||||
return {
|
||||
...monitor,
|
||||
stats,
|
||||
};
|
||||
};
|
||||
|
||||
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
|
||||
await this.verifyTeamAccess({ teamId, monitorId });
|
||||
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
|
||||
|
||||
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,10 +7,14 @@ class StatusService {
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* db: any
|
||||
* logger: any
|
||||
* buffer: import("./bufferService.js").BufferService
|
||||
* incidentService: import("../business/incidentService.js").IncidentService
|
||||
* monitorsRepository: any
|
||||
* }}
|
||||
*/ constructor({ db, logger, buffer, incidentService, monitorsRepository }) {
|
||||
*/
|
||||
constructor({ db, logger, buffer, incidentService, monitorsRepository }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.buffer = buffer;
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "@/types/check.js";
|
||||
export * from "@/types/monitor.js";
|
||||
export * from "@/types/monitorStats.js";
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface MonitorStats {
|
||||
id: string;
|
||||
monitorId: string;
|
||||
avgResponseTime: number;
|
||||
totalChecks: number;
|
||||
totalUpChecks: number;
|
||||
totalDownChecks: number;
|
||||
uptimePercentage: number;
|
||||
lastCheckTimestamp: number;
|
||||
lastResponseTime: number;
|
||||
timeOfLastFailure?: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -134,18 +134,6 @@ const getMonitorsByTeamIdQueryValidation = joi.object({
|
||||
order: joi.string().valid("asc", "desc"),
|
||||
});
|
||||
|
||||
const getMonitorStatsByIdParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
const getMonitorStatsByIdQueryValidation = joi.object({
|
||||
status: joi.string(),
|
||||
limit: joi.number(),
|
||||
sortOrder: joi.string().valid("asc", "desc"),
|
||||
dateRange: joi.string().valid("hour", "day", "week", "month", "all"),
|
||||
numToDisplay: joi.number(),
|
||||
normalize: joi.boolean(),
|
||||
});
|
||||
|
||||
const getCertificateParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
@@ -725,8 +713,6 @@ export {
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByTeamIdParamValidation,
|
||||
getMonitorsByTeamIdQueryValidation,
|
||||
getMonitorStatsByIdParamValidation,
|
||||
getMonitorStatsByIdQueryValidation,
|
||||
getHardwareDetailsByIdParamValidation,
|
||||
getHardwareDetailsByIdQueryValidation,
|
||||
getCertificateParamValidation,
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import { MonitorService } from "../src/service/business/monitorService.ts";
|
||||
import type { IMonitorsRepository, IChecksRepository } from "../src/repositories/index.ts";
|
||||
|
||||
const createMonitorsRepositoryMock = () =>
|
||||
({
|
||||
findMonitorCountByTeamIdAndType: jest.fn(),
|
||||
findByTeamId: jest.fn(),
|
||||
findById: jest.fn(),
|
||||
create: jest.fn(),
|
||||
createBulkMonitors: jest.fn(),
|
||||
deleteByTeamId: jest.fn(),
|
||||
}) as unknown as IMonitorsRepository;
|
||||
|
||||
const createChecksRepositoryMock = () =>
|
||||
({
|
||||
findLatestChecksByMonitorIds: jest.fn(),
|
||||
findDateRangeChecksByMonitor: jest.fn(),
|
||||
}) as unknown as IChecksRepository;
|
||||
|
||||
const createService = ({
|
||||
monitorsRepository = createMonitorsRepositoryMock(),
|
||||
checksRepository = createChecksRepositoryMock(),
|
||||
monitorStatsRepository = { findByMonitorId: jest.fn() },
|
||||
monitorModuleOverrides = {},
|
||||
}: {
|
||||
monitorsRepository?: IMonitorsRepository;
|
||||
checksRepository?: IChecksRepository;
|
||||
monitorStatsRepository?: { findByMonitorId: jest.Mock };
|
||||
monitorModuleOverrides?: Record<string, unknown>;
|
||||
} = {}) => {
|
||||
const monitorModule = {
|
||||
getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: () => true } }),
|
||||
getMonitorStatsById: jest.fn().mockResolvedValue({ latest: {} }),
|
||||
getMonitorsByTeamId: jest.fn().mockResolvedValue([]),
|
||||
getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [], summary: {} }),
|
||||
...monitorModuleOverrides,
|
||||
};
|
||||
|
||||
return new MonitorService({
|
||||
db: {
|
||||
monitorModule,
|
||||
statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() },
|
||||
checkModule: { deleteChecks: jest.fn() },
|
||||
pageSpeedCheckModule: { deletePageSpeedChecksByMonitorId: jest.fn() },
|
||||
notificationsModule: { deleteNotificationsByMonitorId: jest.fn() },
|
||||
},
|
||||
jobQueue: {
|
||||
addJob: jest.fn(),
|
||||
updateJob: jest.fn(),
|
||||
resumeJob: jest.fn(),
|
||||
pauseJob: jest.fn(),
|
||||
deleteJob: jest.fn(),
|
||||
},
|
||||
stringService: {},
|
||||
emailService: { buildEmail: jest.fn(), sendEmail: jest.fn() },
|
||||
papaparse: { parse: jest.fn(), unparse: jest.fn() },
|
||||
logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn() },
|
||||
errorService: {
|
||||
createAuthorizationError: jest.fn(() => new Error("unauthorized")),
|
||||
createServerError: jest.fn(() => new Error("server")),
|
||||
createBadRequestError: jest.fn(() => new Error("bad request")),
|
||||
createNotFoundError: jest.fn(() => new Error("not found")),
|
||||
},
|
||||
games: [],
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
monitorStatsRepository,
|
||||
});
|
||||
};
|
||||
|
||||
describe("MonitorService", () => {
|
||||
describe("getMonitorsWithChecksByTeamId", () => {
|
||||
it("returns monitors enriched with normalized checks", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
(monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(2);
|
||||
(monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([
|
||||
{ id: "m1", name: "Monitor 1", interval: 60000 },
|
||||
{ id: "m2", name: "Monitor 2", interval: 60000 },
|
||||
]);
|
||||
|
||||
const checksRepository = createChecksRepositoryMock();
|
||||
(checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({
|
||||
m1: [
|
||||
{ responseTime: 10, status: true, message: "OK" },
|
||||
{ responseTime: 20, status: true, message: "OK" },
|
||||
],
|
||||
m2: [{ responseTime: 50, status: true, message: "OK" }],
|
||||
});
|
||||
|
||||
const service = createService({ monitorsRepository, checksRepository });
|
||||
const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" });
|
||||
|
||||
expect(result).toMatchObject({ count: 2 });
|
||||
expect(result.monitors).toHaveLength(2);
|
||||
expect(result.monitors[0]).toHaveProperty("checks");
|
||||
expect(result.monitors[0].checks.length).toBeGreaterThan(0);
|
||||
expect(result.monitors[0].checks[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
responseTime: expect.any(Number),
|
||||
status: expect.any(Boolean),
|
||||
message: expect.any(String),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMonitorsByTeamId", () => {
|
||||
it("returns monitors array from db module", async () => {
|
||||
const monitorsPayload = [
|
||||
{ id: "m1", name: "Monitor 1" },
|
||||
{ id: "m2", name: "Monitor 2" },
|
||||
];
|
||||
const monitorModuleOverrides = {
|
||||
getMonitorsByTeamId: jest.fn().mockResolvedValue(monitorsPayload),
|
||||
};
|
||||
const service = createService({ monitorModuleOverrides });
|
||||
const result = await service.getMonitorsByTeamId({ teamId: "team" } as any);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toHaveProperty("id", "m1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMonitorsAndSummaryByTeamId", () => {
|
||||
it("returns monitors with summary block", async () => {
|
||||
const monitorModuleOverrides = {
|
||||
getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } }),
|
||||
};
|
||||
const service = createService({ monitorModuleOverrides });
|
||||
const result = await service.getMonitorsAndSummaryByTeamId({ teamId: "team" });
|
||||
expect(result).toEqual({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } });
|
||||
});
|
||||
});
|
||||
|
||||
describe("getUptimeDetailsById", () => {
|
||||
it("returns monitorData and monitorStats with expected shape", async () => {
|
||||
const TEAM_ID = "team";
|
||||
const monitor = {
|
||||
id: "monitor-1",
|
||||
teamId: TEAM_ID,
|
||||
name: "Hardware monitor",
|
||||
interval: 60000,
|
||||
statusWindow: [],
|
||||
statusWindowSize: 5,
|
||||
statusWindowThreshold: 60,
|
||||
type: "http",
|
||||
ignoreTlsErrors: false,
|
||||
url: "https://example.com",
|
||||
isActive: true,
|
||||
alertThreshold: 5,
|
||||
cpuAlertThreshold: 5,
|
||||
memoryAlertThreshold: 5,
|
||||
diskAlertThreshold: 5,
|
||||
tempAlertThreshold: 5,
|
||||
selectedDisks: [],
|
||||
notifications: [],
|
||||
group: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
|
||||
const checksRepository = createChecksRepositoryMock();
|
||||
(checksRepository.findDateRangeChecksByMonitor as jest.Mock).mockResolvedValue({
|
||||
monitorType: "uptime",
|
||||
groupedChecks: [{ _id: "2024-01-01", avgResponseTime: 100, totalChecks: 2 }],
|
||||
groupedUpChecks: [{ _id: "2024-01-01", totalChecks: 2, avgResponseTime: 90 }],
|
||||
groupedDownChecks: [{ _id: "2024-01-01", totalChecks: 0, avgResponseTime: 0 }],
|
||||
uptimePercentage: 0.99,
|
||||
avgResponseTime: 95,
|
||||
});
|
||||
|
||||
const monitorStatsRepository = {
|
||||
findByMonitorId: jest.fn().mockResolvedValue({
|
||||
id: "stats-1",
|
||||
monitorId: monitor.id,
|
||||
avgResponseTime: 90,
|
||||
totalChecks: 10,
|
||||
totalUpChecks: 9,
|
||||
totalDownChecks: 1,
|
||||
uptimePercentage: 0.9,
|
||||
lastCheckTimestamp: 123456789,
|
||||
lastResponseTime: 80,
|
||||
timeOfLastFailure: 123456700,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}),
|
||||
} as any;
|
||||
const monitorModuleOverrides = {
|
||||
getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: (value: string) => value === TEAM_ID } }),
|
||||
};
|
||||
|
||||
const service = createService({ monitorsRepository, checksRepository, monitorModuleOverrides, monitorStatsRepository });
|
||||
const result = await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: "monitor-1", dateRange: "recent" });
|
||||
|
||||
expect(result).toHaveProperty("monitorData");
|
||||
expect(result.monitorData.monitor).toMatchObject({ id: monitor.id, name: monitor.name });
|
||||
expect(result.monitorData.groupedChecks[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
_id: expect.any(String),
|
||||
avgResponseTime: expect.any(Number),
|
||||
totalChecks: expect.any(Number),
|
||||
})
|
||||
);
|
||||
expect(result.monitorStats).toEqual(
|
||||
expect.objectContaining({
|
||||
monitorId: monitor.id,
|
||||
avgResponseTime: 90,
|
||||
totalChecks: 10,
|
||||
totalUpChecks: 9,
|
||||
totalDownChecks: 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"types": ["jest"],
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src", "test"]
|
||||
}
|
||||
@@ -12,7 +12,8 @@
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"skipLibCheck": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["tests", "dist", "node_modules"]
|
||||
|
||||
Reference in New Issue
Block a user