diff --git a/server/package-lock.json b/server/package-lock.json index eaf771a61..d254da407 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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", diff --git a/server/package.json b/server/package.json index 5610d32a9..3547810ce 100755 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/config/services.js b/server/src/config/services.js index 28b7c9a86..0916752ab 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -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 = { diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts old mode 100755 new mode 100644 index b13aa9a16..b4a31f8f7 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -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 & { + 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( + { + 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( + { + 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( { 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( { 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( { 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( { 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( { 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( { 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( { - 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( + { + 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( { 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( { 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("Check", CheckSchema); + +export type { CheckDocument, CheckMetadataDocument }; +export { CheckModel }; +export default CheckModel; diff --git a/server/src/db/models/Monitor.js b/server/src/db/models/Monitor.ts old mode 100755 new mode 100644 similarity index 70% rename from server/src/db/models/Monitor.js rename to server/src/db/models/Monitor.ts index 901dc39d1..f6f9d4371 --- a/server/src/db/models/Monitor.js +++ b/server/src/db/models/Monitor.ts @@ -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( + { + usage_cpu: { type: Number }, + usage_memory: { type: Number }, + usage_disk: { type: Number }, + usage_temperature: { type: Number }, + }, + { _id: false } +); + +const MonitorSchema = new Schema( { 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 | 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("Monitor", MonitorSchema); + +export type { MonitorDocument }; +export { MonitorModel }; +export default MonitorModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts new file mode 100644 index 000000000..473940874 --- /dev/null +++ b/server/src/db/models/index.ts @@ -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"; diff --git a/server/src/db/modules/checkModule.js b/server/src/db/modules/checkModule.js index dfca02bed..871761290 100755 --- a/server/src/db/modules/checkModule.js +++ b/server/src/db/modules/checkModule.js @@ -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; diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 39168f0b2..316caa2ea 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -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]; diff --git a/server/src/db/modules/monitorModuleQueries.js b/server/src/db/modules/monitorModuleQueries.js index 32aed1529..e6c6b4ec6 100755 --- a/server/src/db/modules/monitorModuleQueries.js +++ b/server/src/db/modules/monitorModuleQueries.js @@ -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: { diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index e69de29bb..df69cf18a 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -0,0 +1,10 @@ +import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; + +export interface IChecksRepository { + // create + // single fetch + // collection fetch + findLatestChecksByMonitorIds(monitorIds: string[]): Promise; + // update + // delete +} diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index e69de29bb..d04118da0 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -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; + +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 => { + 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((acc, cm) => { + acc[cm._id.toString()] = cm.latestChecks.map((c: CheckDocument) => this.toEntity(c)); + return acc; + }, {}); + }; +} + +export default MongoChecksRepistory; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts new file mode 100644 index 000000000..c2bae6939 --- /dev/null +++ b/server/src/repositories/index.ts @@ -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"; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 458ee5bad..865d7f8e4 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -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; + findByTeamId(teamId: string, config: TeamQueryConfig): Promise; // update // delete + + // counts + findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index b4bae06c8..dd85f9a3d 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -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 => { + const documents = await MonitorModel.find().exec(); + return this.mapDocuments(documents); + }; + + findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { + const { page = 0, rowsPerPage = 25, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; + + const query: Record = { + 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 => { + const { type } = config ?? {}; + + const query: FilterQuery = { + 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; diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.ts similarity index 96% rename from server/src/routes/v1/monitorRoute.js rename to server/src/routes/v1/monitorRoute.ts index 86463b6bb..e68e0019f 100755 --- a/server/src/routes/v1/monitorRoute.js +++ b/server/src/routes/v1/monitorRoute.ts @@ -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(); diff --git a/server/src/service/business/monitorService.js b/server/src/service/business/monitorService.js index 4eae0176f..f457e5c7a 100644 --- a/server/src/service/business/monitorService.js +++ b/server/src/service/business/monitorService.js @@ -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 }) => { diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 3c1682818..843c356ed 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -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); diff --git a/server/src/types/check.ts b/server/src/types/check.ts index f2ccc0b77..2120d5de1 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -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; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 6a386c2e4..4c8dc7528 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -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; }