refactoring

This commit is contained in:
Alex Holliday
2026-01-12 22:58:46 +00:00
parent e090d3cfd3
commit 7a6b0d5da4
19 changed files with 699 additions and 116 deletions
+11
View File
@@ -48,6 +48,7 @@
"@eslint/js": "^9.17.0",
"@types/express": "5.0.3",
"@types/jsonwebtoken": "9.0.10",
"@types/multer": "^2.0.0",
"@types/nodemailer": "7.0.1",
"@types/ping": "0.4.4",
"c8": "10.1.3",
@@ -3301,6 +3302,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/multer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz",
"integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/node": {
"version": "24.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
+1
View File
@@ -60,6 +60,7 @@
"@eslint/js": "^9.17.0",
"@types/express": "5.0.3",
"@types/jsonwebtoken": "9.0.10",
"@types/multer": "^2.0.0",
"@types/nodemailer": "7.0.1",
"@types/ping": "0.4.4",
"c8": "10.1.3",
+11 -3
View File
@@ -47,7 +47,7 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js";
import { ParseBoolean } from "../utils/utils.js";
// Models
import Check from "../db/models/Check.js";
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";
@@ -71,6 +71,9 @@ import RecoveryModule from "../db/modules/recoveryModule.js";
import SettingsModule from "../db/modules/settingsModule.js";
import IncidentModule from "../db/modules/incidentModule.js";
// repositories
import { MongoMonitorsRepository, MongoChecksRepository } from "@/repositories/index.js";
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
const serviceRegistry = new ServiceRegistry({ logger });
ServiceRegistry.instance = serviceRegistry;
@@ -81,7 +84,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
const stringService = new StringService(translationService);
// Create DB
const checkModule = new CheckModule({ logger, Check, Monitor, User });
const checkModule = new CheckModule({ logger, CheckModel, 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 });
@@ -89,7 +92,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
const monitorModule = new MonitorModule({
Monitor,
MonitorStats,
Check,
stringService,
fs,
path,
@@ -120,6 +122,10 @@ export const initializeServices = async ({ logger, envSettings, settingsService
await db.connect();
// Repositories
const monitorsRepository = new MongoMonitorsRepository();
const checksRepository = new MongoChecksRepository();
const networkService = new NetworkService({
axios,
got,
@@ -220,6 +226,8 @@ export const initializeServices = async ({ logger, envSettings, settingsService
logger,
errorService,
games,
monitorsRepository,
checksRepository,
});
const services = {
Executable → Regular
+108 -32
View File
@@ -1,7 +1,78 @@
import mongoose from "mongoose";
import { MonitorTypes } from "@/types/monitor.js";
import { Schema, model, Types } from "mongoose";
import {
MonitorTypes,
type MonitorType,
} from "@/types/monitor.js";
import type {
Check,
CheckAudits,
CheckCaptureInfo,
CheckCpuInfo,
CheckDiskInfo,
CheckErrorInfo,
CheckHostInfo,
CheckMemoryInfo,
CheckMetadata,
CheckNetworkInterfaceInfo,
CheckTimings,
CheckTimingPhases,
} from "@/types/check.js";
const cpuSchema = new mongoose.Schema(
type CheckMetadataDocument = Omit<CheckMetadata, "monitorId" | "teamId"> & {
monitorId: Types.ObjectId;
teamId: Types.ObjectId;
type: MonitorType;
};
type CheckDocumentBase = Omit<
Check,
"id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt"
> & {
metadata: CheckMetadataDocument;
expiry: Date;
ackAt?: Date | null;
createdAt: Date;
updatedAt: Date;
__v: number;
};
interface CheckDocument extends CheckDocumentBase {
_id: Types.ObjectId;
}
const timingPhasesSchema = new Schema<CheckTimingPhases>(
{
wait: { type: Number, default: 0 },
dns: { type: Number, default: 0 },
tcp: { type: Number, default: 0 },
tls: { type: Number, default: 0 },
request: { type: Number, default: 0 },
firstByte: { type: Number, default: 0 },
download: { type: Number, default: 0 },
total: { type: Number, default: 0 },
},
{ _id: false }
);
const timingsSchema = new Schema<CheckTimings>(
{
start: { type: Number, default: 0 },
socket: { type: Number, default: 0 },
lookup: { type: Number, default: 0 },
connect: { type: Number, default: 0 },
secureConnect: { type: Number, default: 0 },
upload: { type: Number, default: 0 },
response: { type: Number, default: 0 },
end: { type: Number, default: 0 },
phases: {
type: timingPhasesSchema,
default: () => ({}),
},
},
{ _id: false }
);
const cpuSchema = new Schema<CheckCpuInfo>(
{
physical_core: { type: Number, default: 0 },
logical_core: { type: Number, default: 0 },
@@ -13,7 +84,7 @@ const cpuSchema = new mongoose.Schema(
{ _id: false }
);
const memorySchema = new mongoose.Schema(
const memorySchema = new Schema<CheckMemoryInfo>(
{
total_bytes: { type: Number, default: 0 },
available_bytes: { type: Number, default: 0 },
@@ -23,7 +94,7 @@ const memorySchema = new mongoose.Schema(
{ _id: false }
);
const diskSchema = new mongoose.Schema(
const diskSchema = new Schema<CheckDiskInfo>(
{
device: { type: String, default: "" },
mountpoint: { type: String, default: "" },
@@ -36,7 +107,7 @@ const diskSchema = new mongoose.Schema(
{ _id: false }
);
const hostSchema = new mongoose.Schema(
const hostSchema = new Schema<CheckHostInfo>(
{
os: { type: String, default: "" },
platform: { type: String, default: "" },
@@ -45,7 +116,7 @@ const hostSchema = new mongoose.Schema(
{ _id: false }
);
const errorSchema = new mongoose.Schema(
const errorSchema = new Schema<CheckErrorInfo>(
{
metric: { type: [String], default: [] },
err: { type: String, default: "" },
@@ -53,7 +124,7 @@ const errorSchema = new mongoose.Schema(
{ _id: false }
);
const captureSchema = new mongoose.Schema(
const captureSchema = new Schema<CheckCaptureInfo>(
{
version: { type: String, default: "" },
mode: { type: String, default: "" },
@@ -61,9 +132,9 @@ const captureSchema = new mongoose.Schema(
{ _id: false }
);
const networkInterfaceSchema = new mongoose.Schema(
const networkInterfaceSchema = new Schema<CheckNetworkInterfaceInfo>(
{
name: { type: String },
name: { type: String, default: "" },
bytes_sent: { type: Number, default: 0 },
bytes_recv: { type: Number, default: 0 },
packets_sent: { type: Number, default: 0 },
@@ -78,17 +149,30 @@ const networkInterfaceSchema = new mongoose.Schema(
{ _id: false }
);
const metadataSchema = new mongoose.Schema(
const auditsSchema = new Schema<CheckAudits>(
{
cls: { type: Number, default: 0 },
si: { type: Number, default: 0 },
fcp: { type: Number, default: 0 },
lcp: { type: Number, default: 0 },
tbt: { type: Number, default: 0 },
},
{ _id: false }
);
const metadataSchema = new Schema<CheckMetadataDocument>(
{
monitorId: {
type: mongoose.Schema.Types.ObjectId,
type: Schema.Types.ObjectId,
ref: "Monitor",
required: true,
immutable: true,
index: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
type: Schema.Types.ObjectId,
ref: "Team",
required: true,
immutable: true,
index: true,
},
@@ -102,7 +186,7 @@ const metadataSchema = new mongoose.Schema(
{ _id: false }
);
const CheckSchema = new mongoose.Schema(
const CheckSchema = new Schema<CheckDocument>(
{
metadata: {
type: metadataSchema,
@@ -112,40 +196,32 @@ const CheckSchema = new mongoose.Schema(
type: Boolean,
index: true,
},
responseTime: {
type: Number,
},
timings: {
type: Object,
default: {},
type: timingsSchema,
default: undefined,
},
statusCode: {
type: Number,
index: true,
},
message: {
type: String,
},
expiry: {
type: Date,
default: Date.now,
},
ack: {
type: Boolean,
default: false,
},
ackAt: {
type: Date,
default: undefined,
},
// Hardware fields
cpu: {
type: cpuSchema,
default: () => ({}),
@@ -162,23 +238,18 @@ const CheckSchema = new mongoose.Schema(
type: hostSchema,
default: () => ({}),
},
errors: {
type: [errorSchema],
default: () => [],
},
capture: {
type: captureSchema,
default: () => ({}),
},
net: {
type: [networkInterfaceSchema],
default: () => [],
},
// PageSpeed fields
accessibility: {
type: Number,
},
@@ -192,7 +263,8 @@ const CheckSchema = new mongoose.Schema(
type: Number,
},
audits: {
type: Object,
type: auditsSchema,
default: undefined,
},
},
{
@@ -211,4 +283,8 @@ CheckSchema.index({ "metadata.monitorId": 1, updatedAt: 1 });
CheckSchema.index({ "metadata.monitorId": 1, updatedAt: -1 });
CheckSchema.index({ "metadata.teamId": 1, updatedAt: -1 });
export default mongoose.model("Check", CheckSchema);
const CheckModel = model<CheckDocument>("Check", CheckSchema);
export type { CheckDocument, CheckMetadataDocument };
export { CheckModel };
export default CheckModel;
+48 -24
View File
@@ -1,18 +1,49 @@
import mongoose from "mongoose";
import { Schema, model, Types, type UpdateQuery } from "mongoose";
import type { Monitor, MonitorMatchMethod, MonitorThresholds } from "@/types/monitor.js";
import { MonitorTypes } from "@/types/monitor.js";
import Check from "./Check.js";
import MonitorStats from "./MonitorStats.js";
import StatusPage from "./StatusPage.js";
const MonitorSchema = mongoose.Schema(
type MonitorDocumentBase = Omit<
Monitor,
"id" | "userId" | "teamId" | "notifications" | "selectedDisks" | "statusWindow" | "createdAt" | "updatedAt"
> & {
statusWindow: boolean[];
notifications: Types.ObjectId[];
selectedDisks: string[];
matchMethod?: MonitorMatchMethod;
thresholds?: MonitorThresholds;
};
interface MonitorDocument extends MonitorDocumentBase {
_id: Types.ObjectId;
userId: Types.ObjectId;
teamId: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const thresholdsSchema = new Schema<MonitorThresholds>(
{
usage_cpu: { type: Number },
usage_memory: { type: Number },
usage_disk: { type: Number },
usage_temperature: { type: Number },
},
{ _id: false }
);
const MonitorSchema = new Schema<MonitorDocument>(
{
userId: {
type: mongoose.Schema.Types.ObjectId,
type: Schema.Types.ObjectId,
ref: "User",
immutable: true,
required: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
type: Schema.Types.ObjectId,
ref: "Team",
immutable: true,
required: true,
@@ -43,7 +74,7 @@ const MonitorSchema = mongoose.Schema(
type: {
type: String,
required: true,
enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"],
enum: MonitorTypes,
},
ignoreTlsErrors: {
type: Boolean,
@@ -71,7 +102,6 @@ const MonitorSchema = mongoose.Schema(
default: true,
},
interval: {
// in milliseconds
type: Number,
default: 60000,
},
@@ -81,7 +111,7 @@ const MonitorSchema = mongoose.Schema(
},
notifications: [
{
type: mongoose.Schema.Types.ObjectId,
type: Schema.Types.ObjectId,
ref: "Notification",
},
],
@@ -89,13 +119,7 @@ const MonitorSchema = mongoose.Schema(
type: String,
},
thresholds: {
type: {
usage_cpu: { type: Number },
usage_memory: { type: Number },
usage_disk: { type: Number },
usage_temperature: { type: Number },
},
_id: false,
type: thresholdsSchema,
},
alertThreshold: {
type: Number,
@@ -137,7 +161,7 @@ const MonitorSchema = mongoose.Schema(
trim: true,
maxLength: 50,
default: null,
set: function (value) {
set(value: string | null) {
return value && value.trim() ? value.trim() : null;
},
},
@@ -148,7 +172,6 @@ const MonitorSchema = mongoose.Schema(
);
MonitorSchema.pre("findOneAndDelete", async function (next) {
// Delete checks and stats
try {
const doc = await this.model.findOne(this.getFilter());
@@ -157,20 +180,17 @@ MonitorSchema.pre("findOneAndDelete", async function (next) {
}
await Check.deleteMany({ monitorId: doc._id });
// Deal with status pages
await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } });
await MonitorStats.deleteMany({ monitorId: doc?._id.toString() });
next();
} catch (error) {
next(error);
next(error as Error);
}
});
MonitorSchema.pre("deleteMany", async function (next) {
const filter = this.getFilter();
const monitors = await this.model.find(filter).select(["_id", "type"]).lean();
const monitors = (await this.model.find(filter).select(["_id", "type"]).lean()) as { _id: Types.ObjectId }[];
for (const monitor of monitors) {
await Check.deleteMany({ monitorId: monitor._id });
@@ -197,8 +217,8 @@ MonitorSchema.pre("save", function (next) {
});
MonitorSchema.pre("findOneAndUpdate", function (next) {
const update = this.getUpdate();
if (update.alertThreshold) {
const update = this.getUpdate() as UpdateQuery<MonitorDocument> | null;
if (update && !Array.isArray(update) && update.alertThreshold !== undefined) {
update.cpuAlertThreshold = update.alertThreshold;
update.memoryAlertThreshold = update.alertThreshold;
update.diskAlertThreshold = update.alertThreshold;
@@ -209,4 +229,8 @@ MonitorSchema.pre("findOneAndUpdate", function (next) {
MonitorSchema.index({ teamId: 1, type: 1 });
export default mongoose.model("Monitor", MonitorSchema);
const MonitorModel = model<MonitorDocument>("Monitor", MonitorSchema);
export type { MonitorDocument };
export { MonitorModel };
export default MonitorModel;
+5
View File
@@ -0,0 +1,5 @@
export * from "@/db/models/Monitor.js";
export { default as MonitorModel } from "@/db/models/Monitor.js";
export * from "@/db/models/Check.js";
export { default as CheckModel } from "@/db/models/Check.js";
+25 -18
View File
@@ -1,4 +1,6 @@
import { ObjectId } from "mongodb";
import mongoose from "mongoose";
import { CheckModel } from "@/db/models/index.js";
import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js";
const SERVICE_NAME = "checkModule";
@@ -12,18 +14,15 @@ const dateRangeLookup = {
};
class CheckModule {
constructor({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }) {
constructor({ logger, Monitor, User }) {
this.logger = logger;
this.Check = Check;
this.HardwareCheck = HardwareCheck;
this.PageSpeedCheck = PageSpeedCheck;
this.Monitor = Monitor;
this.User = User;
}
createChecks = async (checks) => {
try {
await this.Check.insertMany(checks, { ordered: false });
await CheckModel.insertMany(checks, { ordered: false });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createCheck";
@@ -41,7 +40,7 @@ class CheckModule {
// Match
const matchStage = {
monitorId: new ObjectId(monitorId),
"metadata.monitorId": new ObjectId(monitorId),
...(typeof status !== "undefined" && { status }),
...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
@@ -79,7 +78,7 @@ class CheckModule {
skip = page * rowsPerPage;
}
const checks = await this.Check.aggregate([
const checks = await CheckModel.aggregate([
{ $match: matchStage },
{ $sort: { createdAt: sortOrder } },
{
@@ -115,7 +114,7 @@ class CheckModule {
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
const matchStage = {
teamId: new ObjectId(teamId),
"metadata.teamId": new ObjectId(teamId),
status: false,
...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
@@ -170,7 +169,7 @@ class CheckModule {
},
];
const checks = await this.Check.aggregate(aggregatePipeline);
const checks = await CheckModel.aggregate(aggregatePipeline);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
@@ -181,7 +180,11 @@ class CheckModule {
ackCheck = async (checkId, teamId, ack) => {
try {
const updatedCheck = await this.Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true });
const updatedCheck = await CheckModel.findOneAndUpdate(
{ _id: checkId, "metadata.teamId": teamId },
{ $set: { ack, ackAt: new Date() } },
{ new: true }
);
if (!updatedCheck) {
throw new Error("Check not found");
@@ -197,7 +200,11 @@ class CheckModule {
ackAllChecks = async (monitorId, teamId, ack, path) => {
try {
const updatedChecks = await this.Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } });
const filter =
path === "monitor"
? { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) }
: { "metadata.teamId": new mongoose.Types.ObjectId(teamId) };
const updatedChecks = await CheckModel.updateMany(filter, { $set: { ack, ackAt: new Date() } });
return updatedChecks.modifiedCount;
} catch (error) {
error.service = SERVICE_NAME;
@@ -209,9 +216,9 @@ class CheckModule {
getChecksSummaryByTeamId = async ({ teamId }) => {
try {
const matchStage = {
teamId: new ObjectId(teamId),
"metadata.teamId": new ObjectId(teamId),
};
const checks = await this.Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage }));
const checks = await CheckModel.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage }));
return checks[0].summary;
} catch (error) {
error.service = SERVICE_NAME;
@@ -221,7 +228,7 @@ class CheckModule {
};
deleteChecks = async (monitorId) => {
try {
const result = await this.Check.deleteMany({ monitorId });
const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
@@ -237,7 +244,7 @@ class CheckModule {
const monitorIds = teamMonitors.map((monitor) => monitor._id);
// Delete all checks for these monitors in one operation
const deleteResult = await this.Check.deleteMany({ monitorId: { $in: monitorIds } });
const deleteResult = await CheckModel.deleteMany({ "metadata.monitorId": { $in: monitorIds } });
return deleteResult.deletedCount;
} catch (error) {
@@ -249,7 +256,7 @@ class CheckModule {
updateChecksTTL = async (teamId, ttl) => {
try {
await this.Check.collection.dropIndex("expiry_1");
await CheckModel.collection.dropIndex("expiry_1");
} catch (error) {
this.logger.error({
message: error.message,
@@ -260,9 +267,9 @@ class CheckModule {
}
try {
await this.Check.collection.createIndex(
await CheckModel.collection.createIndex(
{ expiry: 1 },
{ expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary
{ expireAfterSeconds: ttl, partialFilterExpression: { "metadata.mode": { $exists: true } } }
);
} catch (error) {
error.service = SERVICE_NAME;
+14 -9
View File
@@ -8,13 +8,14 @@ import {
buildFilteredMonitorsByTeamIdPipeline,
} from "./monitorModuleQueries.js";
import { CheckModel } from "@/db/models/index.js";
const SERVICE_NAME = "monitorModule";
class MonitorModule {
constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
constructor({ Monitor, MonitorStats, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
this.Monitor = Monitor;
this.MonitorStats = MonitorStats;
this.Check = Check;
this.stringService = stringService;
this.fs = fs;
this.path = path;
@@ -123,15 +124,18 @@ class MonitorModule {
//Helper
getMonitorChecks = async (monitorId, dateRange, sortOrder) => {
const objectId = new this.ObjectId(monitorId);
const indexSpec = {
monitorId: 1,
updatedAt: sortOrder, // This will be 1 or -1
"metadata.monitorId": 1,
updatedAt: sortOrder,
};
const matchBase = { "metadata.monitorId": objectId };
const [checksAll, checksForDateRange] = await Promise.all([
this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
this.Check.find({
monitorId,
CheckModel.find(matchBase).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
CheckModel.find({
...matchBase,
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
})
.hint(indexSpec)
@@ -238,7 +242,8 @@ class MonitorModule {
const dateString = formatLookup[dateRange];
const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString));
console.log("WTTTTFFF");
const results = await CheckModel.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString));
const monitorData = results[0];
@@ -313,7 +318,7 @@ class MonitorModule {
};
const dateString = formatLookup[dateRange];
const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
const hardwareStats = await CheckModel.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
const stats = hardwareStats[0];
+13 -8
View File
@@ -4,7 +4,7 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
return [
{
$match: {
monitorId: new ObjectId(monitorId),
"metadata.monitorId": new ObjectId(monitorId),
updatedAt: { $gte: dates.start, $lte: dates.end },
},
},
@@ -172,8 +172,8 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
return [
{
$match: {
monitorId: monitor._id,
type: "hardware",
"metadata.monitorId": monitor._id,
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
@@ -233,11 +233,16 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
},
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ["$monitorId", monitor._id] }, { $gte: ["$createdAt", dates.start] }, { $lte: ["$createdAt", dates.end] }],
},
},
$match: {
$expr: {
$and: [
{ $eq: ["$metadata.monitorId", monitor._id] },
{ $eq: ["$metadata.type", "hardware"] },
{ $gte: ["$createdAt", dates.start] },
{ $lte: ["$createdAt", dates.end] },
],
},
},
},
{
$group: {
@@ -0,0 +1,10 @@
import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
export interface IChecksRepository {
// create
// single fetch
// collection fetch
findLatestChecksByMonitorIds(monitorIds: string[]): Promise<LatestChecksMap>;
// update
// delete
}
@@ -0,0 +1,203 @@
import { IChecksRepository } from "@/repositories/index.js";
import type {
Check,
CheckAudits,
CheckCaptureInfo,
CheckCpuInfo,
CheckDiskInfo,
CheckErrorInfo,
CheckHostInfo,
CheckMemoryInfo,
CheckMetadata,
CheckNetworkInterfaceInfo,
CheckTimings,
} from "@/types/index.js";
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
import mongoose from "mongoose";
export type LatestChecksMap = Record<string, Check[]>;
class MongoChecksRepistory implements IChecksRepository {
private toEntity = (doc: CheckDocument): Check => {
const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => {
if (!value) {
return "";
}
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
};
const toDateString = (value?: Date | string | null): string => {
if (!value) {
return new Date(0).toISOString();
}
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
};
const toOptionalDateString = (value?: Date | string | null): string | undefined => {
if (!value) {
return undefined;
}
return toDateString(value);
};
const mapTimings = (timings?: CheckTimings): CheckTimings => {
const phases = timings?.phases ?? {
wait: 0,
dns: 0,
tcp: 0,
tls: 0,
request: 0,
firstByte: 0,
download: 0,
total: 0,
};
return {
start: timings?.start ?? 0,
socket: timings?.socket ?? 0,
lookup: timings?.lookup ?? 0,
connect: timings?.connect ?? 0,
secureConnect: timings?.secureConnect ?? 0,
upload: timings?.upload ?? 0,
response: timings?.response ?? 0,
end: timings?.end ?? 0,
phases,
};
};
const mapCpu = (cpu?: CheckCpuInfo): CheckCpuInfo => ({
physical_core: cpu?.physical_core ?? 0,
logical_core: cpu?.logical_core ?? 0,
frequency: cpu?.frequency ?? 0,
temperature: cpu?.temperature ?? [],
free_percent: cpu?.free_percent ?? 0,
usage_percent: cpu?.usage_percent ?? 0,
});
const mapMemory = (memory?: CheckMemoryInfo): CheckMemoryInfo => ({
total_bytes: memory?.total_bytes ?? 0,
available_bytes: memory?.available_bytes ?? 0,
used_bytes: memory?.used_bytes ?? 0,
usage_percent: memory?.usage_percent ?? 0,
});
const mapHost = (host?: CheckHostInfo): CheckHostInfo => ({
os: host?.os ?? "",
platform: host?.platform ?? "",
kernel_version: host?.kernel_version ?? "",
});
const mapCapture = (capture?: CheckCaptureInfo): CheckCaptureInfo => ({
version: capture?.version ?? "",
mode: capture?.mode ?? "",
});
const mapDisks = (disks?: CheckDiskInfo[]): CheckDiskInfo[] =>
(disks ?? []).map((disk) => ({
device: disk?.device ?? "",
mountpoint: disk?.mountpoint ?? "",
read_speed_bytes: disk?.read_speed_bytes ?? 0,
write_speed_bytes: disk?.write_speed_bytes ?? 0,
total_bytes: disk?.total_bytes ?? 0,
free_bytes: disk?.free_bytes ?? 0,
usage_percent: disk?.usage_percent ?? 0,
}));
const mapErrors = (errors?: CheckErrorInfo[]): CheckErrorInfo[] =>
(errors ?? []).map((error) => ({
metric: error?.metric ?? [],
err: error?.err ?? "",
}));
const mapNet = (net?: CheckNetworkInterfaceInfo[]): CheckNetworkInterfaceInfo[] =>
(net ?? []).map((iface) => ({
name: iface?.name ?? "",
bytes_sent: iface?.bytes_sent ?? 0,
bytes_recv: iface?.bytes_recv ?? 0,
packets_sent: iface?.packets_sent ?? 0,
packets_recv: iface?.packets_recv ?? 0,
err_in: iface?.err_in ?? 0,
err_out: iface?.err_out ?? 0,
drop_in: iface?.drop_in ?? 0,
drop_out: iface?.drop_out ?? 0,
fifo_in: iface?.fifo_in ?? 0,
fifo_out: iface?.fifo_out ?? 0,
}));
const mapAudits = (audits?: CheckAudits): CheckAudits | undefined => {
if (!audits) {
return undefined;
}
return {
cls: audits.cls ?? 0,
si: audits.si ?? 0,
fcp: audits.fcp ?? 0,
lcp: audits.lcp ?? 0,
tbt: audits.tbt ?? 0,
};
};
const mapMetadata = (metadata: CheckDocument["metadata"]): CheckMetadata => ({
monitorId: toStringId(metadata.monitorId),
teamId: toStringId(metadata.teamId),
type: metadata.type,
});
return {
id: toStringId(doc._id),
metadata: mapMetadata(doc.metadata),
status: doc.status ?? false,
responseTime: doc.responseTime ?? 0,
timings: mapTimings(doc.timings),
statusCode: doc.statusCode ?? 0,
message: doc.message ?? "",
ack: doc.ack ?? false,
ackAt: toOptionalDateString(doc.ackAt),
expiry: toDateString(doc.expiry),
cpu: mapCpu(doc.cpu),
memory: mapMemory(doc.memory),
disk: mapDisks(doc.disk),
host: mapHost(doc.host),
errors: mapErrors(doc.errors),
capture: mapCapture(doc.capture),
net: mapNet(doc.net),
accessibility: doc.accessibility,
bestPractices: doc.bestPractices,
seo: doc.seo,
performance: doc.performance,
audits: mapAudits(doc.audits),
__v: doc.__v ?? 0,
createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt),
};
};
findLatestChecksByMonitorIds = async (monitorIds: string[]): Promise<LatestChecksMap> => {
const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
const checkMap = await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": { $in: mongoIds },
},
},
{ $sort: { createdAt: -1 } },
{
$group: {
_id: "$metadata.monitorId",
latestChecks: { $push: "$$ROOT" },
},
},
{
$project: {
latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] },
},
},
]);
return checkMap.reduce<LatestChecksMap>((acc, cm) => {
acc[cm._id.toString()] = cm.latestChecks.map((c: CheckDocument) => this.toEntity(c));
return acc;
}, {});
};
}
export default MongoChecksRepistory;
+5
View File
@@ -0,0 +1,5 @@
export * from "@/repositories/monitors/IMonitorsRepository.js";
export { default as MongoMonitorsRepository } from "@/repositories/monitors/MongoMonitorsRepository.js";
export * from "@/repositories/checks/IChecksRepository.js";
export { default as MongoChecksRepository } from "@/repositories/checks/MongoChecksRepistory.js";
@@ -1,9 +1,24 @@
import type { Monitor } from "@/types/index.js";
import { type MonitorType, type Monitor } from "@/types/index.js";
export interface TeamQueryConfig {
limit?: number;
type?: MonitorType | MonitorType[];
page?: number;
rowsPerPage?: number;
filter?: string;
field?: string;
order?: "asc" | "desc";
}
export interface IMonitorsRepository {
// create
// single fetch
// collection fetch
findAll(): Promise<Monitor[] | null>;
findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null>;
// update
// delete
// counts
findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise<number>;
}
@@ -1,5 +1,125 @@
import { MonitorModel } from "@/db/models/index.js";
import type { MonitorDocument } from "@/db/models/Monitor.js";
import type { Monitor, MonitorType } from "@/types/monitor.js";
import mongoose, { type FilterQuery } from "mongoose";
import type { IMonitorsRepository, TeamQueryConfig } from "./IMonitorsRepository.js";
class MongoMonitorsRepository implements IMonitorsRepository {
findAll = async () => {
return null;
findAll = async (): Promise<Monitor[] | null> => {
const documents = await MonitorModel.find().exec();
return this.mapDocuments(documents);
};
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
const { page = 0, rowsPerPage = 25, filter, field = "createdAt", order = "desc", type, limit } = config ?? {};
const query: Record<string, unknown> = {
teamId: new mongoose.Types.ObjectId(teamId),
};
if (type !== undefined) {
query.type = Array.isArray(type) ? { $in: type } : type;
}
if (filter !== undefined) {
switch (field) {
case "name":
query.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
break;
case "isActive":
query.isActive = filter === "true";
break;
case "status":
query.status = filter === "true";
break;
case "type":
query.type = filter;
break;
default:
break;
}
}
const sort = { [field]: order === "asc" ? 1 : -1 } as const;
const skip = Math.max(page, 0) * rowsPerPage;
const limitValue = limit ?? rowsPerPage;
const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limitValue).exec();
return this.mapDocuments(documents);
};
findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise<number> => {
const { type } = config ?? {};
const query: FilterQuery<MonitorDocument> = {
teamId: new mongoose.Types.ObjectId(teamId),
};
if (type !== undefined) {
query.type = Array.isArray(type) ? { $in: type } : type;
}
const count = await MonitorModel.countDocuments(query);
return count;
};
private mapDocuments = (documents: MonitorDocument[]): Monitor[] | null => {
if (!documents?.length) {
return null;
}
return documents.map((doc) => this.toEntity(doc));
};
private toEntity = (doc: MonitorDocument): Monitor => {
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;
};
const notificationIds = (doc.notifications ?? []).map((notification) => toStringId(notification));
return {
id: toStringId(doc._id),
userId: toStringId(doc.userId),
teamId: toStringId(doc.teamId),
name: doc.name,
description: doc.description ?? undefined,
status: doc.status ?? undefined,
statusWindow: doc.statusWindow ?? [],
statusWindowSize: doc.statusWindowSize,
statusWindowThreshold: doc.statusWindowThreshold,
type: doc.type,
ignoreTlsErrors: doc.ignoreTlsErrors,
jsonPath: doc.jsonPath ?? undefined,
expectedValue: doc.expectedValue ?? undefined,
matchMethod: doc.matchMethod ?? undefined,
url: doc.url,
port: doc.port ?? undefined,
isActive: doc.isActive,
interval: doc.interval,
uptimePercentage: doc.uptimePercentage ?? undefined,
notifications: notificationIds,
secret: doc.secret ?? undefined,
thresholds: doc.thresholds ?? undefined,
alertThreshold: doc.alertThreshold,
cpuAlertThreshold: doc.cpuAlertThreshold,
memoryAlertThreshold: doc.memoryAlertThreshold,
diskAlertThreshold: doc.diskAlertThreshold,
tempAlertThreshold: doc.tempAlertThreshold,
selectedDisks: doc.selectedDisks ?? [],
gameId: doc.gameId ?? undefined,
group: doc.group ?? null,
createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt),
};
};
}
export default MongoMonitorsRepository;
@@ -7,7 +7,9 @@ const upload = multer({
});
class MonitorRoutes {
constructor(monitorController) {
private router: Router;
private monitorController: any;
constructor(monitorController: any) {
this.router = Router();
this.monitorController = monitorController;
this.initRoutes();
+31 -5
View File
@@ -1,10 +1,23 @@
import { createMonitorsBodyValidation } from "@/validation/joi.js";
import { NormalizeData } from "@/utils/dataUtils.js";
const SERVICE_NAME = "MonitorService";
class MonitorService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService, games }) {
constructor({
db,
settingsService,
jobQueue,
stringService,
emailService,
papaparse,
logger,
errorService,
games,
monitorsRepository,
checksRepository,
}) {
this.db = db;
this.settingsService = settingsService;
this.jobQueue = jobQueue;
@@ -14,6 +27,8 @@ class MonitorService {
this.logger = logger;
this.errorService = errorService;
this.games = games;
this.monitorsRepository = monitorsRepository;
this.checksRepository = checksRepository;
}
get serviceName() {
@@ -205,6 +220,7 @@ class MonitorService {
};
getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => {
console.log("que");
const monitors = await this.db.monitorModule.getMonitorsByTeamId({
limit,
type,
@@ -228,7 +244,8 @@ class MonitorService {
};
getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => {
const result = await this.db.monitorModule.getMonitorsWithChecksByTeamId({
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
limit,
type,
page,
@@ -236,10 +253,19 @@ class MonitorService {
filter,
field,
order,
teamId,
explain,
});
return result;
const monitorIds = monitors?.map((m) => m.id) ?? [];
const checksMap = await this.checksRepository.findLatestChecksByMonitorIds(monitorIds);
const monitorsWithChecks = (monitors ?? []).map((monitor) => {
const checks = NormalizeData(checksMap[monitor.id] ?? [], 10, 100);
return {
...monitor,
checks,
};
});
return { count, monitors: monitorsWithChecks };
};
exportMonitorsToCSV = async ({ teamId }) => {
@@ -1,5 +1,5 @@
import MonitorStats from "../../db/models/MonitorStats.js";
import Check from "../../db/models/Check.js";
import { CheckModel } from "@/db/models/index.js";
const SERVICE_NAME = "StatusService";
class StatusService {
@@ -115,7 +115,7 @@ class StatusService {
if (!check._id) {
try {
const checkModel = new Check(check);
const checkModel = new CheckModel(check);
savedCheck = await checkModel.save();
this.buffer.removeCheckFromBuffer(check);
+50 -7
View File
@@ -1,5 +1,11 @@
import type { MonitorType } from "@/types/index.js";
export interface CheckMetadata {
monitorId: string;
teamId: string;
type: MonitorType;
}
export interface CheckTimingPhases {
wait: number;
dns: number;
@@ -50,29 +56,66 @@ export interface CheckCaptureInfo {
mode: string;
}
export interface CheckDiskInfo {}
export interface CheckDiskInfo {
device: string;
mountpoint: string;
read_speed_bytes: number;
write_speed_bytes: number;
total_bytes: number;
free_bytes: number;
usage_percent: number;
}
export interface CheckNetInfo {}
export interface CheckErrorInfo {
metric: string[];
err: string;
}
export interface CheckNetworkInterfaceInfo {
name: string;
bytes_sent: number;
bytes_recv: number;
packets_sent: number;
packets_recv: number;
err_in: number;
err_out: number;
drop_in: number;
drop_out: number;
fifo_in: number;
fifo_out: number;
}
export interface CheckAudits {
cls: number;
si: number;
fcp: number;
lcp: number;
tbt: number;
}
export interface Check {
id: string;
monitorId: string;
teamId: string;
type: MonitorType;
metadata: CheckMetadata;
status: boolean;
responseTime: number;
timings: CheckTimings;
statusCode: number;
message: string;
ack: boolean;
ackAt?: string | null;
expiry: string;
cpu: CheckCpuInfo;
memory: CheckMemoryInfo;
disk: CheckDiskInfo[];
host: CheckHostInfo;
errors: string[];
errors: CheckErrorInfo[];
capture: CheckCaptureInfo;
net: CheckNetInfo[];
net: CheckNetworkInterfaceInfo[];
accessibility?: number;
bestPractices?: number;
seo?: number;
performance?: number;
audits?: CheckAudits;
__v: number;
createdAt: string;
updatedAt: string;
+21 -4
View File
@@ -1,29 +1,46 @@
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"] as const;
export type MonitorType = (typeof MonitorTypes)[number];
export interface MonitorThresholds {
usage_cpu?: number;
usage_memory?: number;
usage_disk?: number;
usage_temperature?: number;
}
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
export interface Monitor {
id: string;
userId: string;
teamId: string;
name: string;
description: string;
description?: string;
status?: boolean;
statusWindow: boolean[];
statusWindowSize: number;
statusWindowThreshold: number;
type: MonitorType;
ignoreTlsErrors: boolean;
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
url: string;
port?: number;
isActive: boolean;
interval: number;
uptimePercentage?: number;
notifications: string[];
secret?: string;
thresholds?: MonitorThresholds;
alertThreshold: number;
selectedDisks: string[];
group: string | null;
cpuAlertThreshold: number;
memoryAlertThreshold: number;
diskAlertThreshold: number;
tempAlertThreshold: number;
selectedDisks: string[];
gameId?: string;
group: string | null;
createdAt: string;
updatedAt: string;
status: boolean;
}