Merge upstream/develop into fix/3120-data-leak

This commit is contained in:
Craig Lawson committed 2026-01-15 11:06:27 +00:00
commit f0a6fc4fdf
309 files changed
+8413 -4468

No files matched your search

@@ -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;
}
@@ -0,0 +1,10 @@
import Monitor from "@/db/models/Monitor.js";
async function migrateStatusWindowThreshold() {
const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } });
for (const monitor of monitors) {
monitor.statusWindowThreshold = monitor.statusWindowThreshold * 100;
await monitor.save();
}
}
export { migrateStatusWindowThreshold };
@@ -0,0 +1,95 @@
import mongoose from "mongoose";
const CHECKS_COLLECTION = "checks";
const BACKUP_COLLECTION = "checks_backup";
const BATCH_SIZE = 1000;
const getDb = () => {
const db = mongoose.connection.db;
if (!db) {
throw new Error("Database connection is not initialized");
}
return db;
};
const backupAndDropExistingCollection = async () => {
const db = getDb();
const collections = await db.listCollections({ name: CHECKS_COLLECTION }).toArray();
if (collections.length === 0) {
return false;
}
await db
.collection(CHECKS_COLLECTION)
.aggregate([{ $match: {} }, { $out: BACKUP_COLLECTION }])
.toArray();
await db.collection(CHECKS_COLLECTION).drop();
return true;
};
const createTimeSeriesCollection = async () => {
const db = getDb();
const existing = await db.listCollections({ name: CHECKS_COLLECTION }).toArray();
if (existing.length === 0) {
await db.createCollection(CHECKS_COLLECTION, {
timeseries: {
timeField: "createdAt",
metaField: "metadata",
granularity: "seconds",
},
});
}
await db
.collection(CHECKS_COLLECTION)
.createIndexes([
{ key: { updatedAt: 1 } },
{ key: { "metadata.monitorId": 1, updatedAt: 1 } },
{ key: { "metadata.monitorId": 1, updatedAt: -1 } },
{ key: { "metadata.teamId": 1, updatedAt: -1 } },
]);
};
const migrateBackupData = async () => {
const db = getDb();
const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray();
if (backupExists.length === 0) {
return;
}
const source = db.collection(BACKUP_COLLECTION);
const target = db.collection(CHECKS_COLLECTION);
const cursor = source.find();
const operations: any[] = [];
while (await cursor.hasNext()) {
const doc = await cursor.next();
if (!doc) {
continue;
}
const metadata = {
monitorId: doc.monitorId,
teamId: doc.teamId,
type: doc.type,
};
operations.push({ insertOne: { document: { ...doc, metadata } } });
if (operations.length >= BATCH_SIZE) {
await target.bulkWrite(operations);
operations.length = 0;
}
}
if (operations.length) {
await target.bulkWrite(operations);
}
};
export const convertChecksToTimeSeries = async () => {
const backedUp = await backupAndDropExistingCollection();
await createTimeSeriesCollection();
if (backedUp) {
await migrateBackupData();
}
};
+47
View File
@@ -0,0 +1,47 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import MigrationModel from "../models/Migration.js";
type MigrationEntry = {
name: string;
execute: () => Promise<void>;
};
const migrations: MigrationEntry[] = [
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
];
const runMigrations = async (logger?: { info: Function; error: Function }) => {
try {
logger?.info({ message: "Running migrations", service: "Migrations" });
for (const migration of migrations) {
const exists = await MigrationModel.findOne({ name: migration.name, status: "completed" });
if (exists) {
logger?.info({ message: `Skipping ${migration.name}`, service: "Migrations" });
continue;
}
try {
await migration.execute();
await MigrationModel.findOneAndUpdate(
{ name: migration.name },
{ status: "completed", completedAt: new Date(), error: undefined },
{ upsert: true }
);
logger?.info({ message: `Completed ${migration.name}`, service: "Migrations" });
} catch (error) {
const err = error as Error;
await MigrationModel.findOneAndUpdate({ name: migration.name }, { status: "failed", error: err?.message }, { upsert: true });
throw error;
}
}
logger?.info({ message: "Migrations completed", service: "Migrations" });
} catch (error) {
const err = error as Error;
logger?.error({ message: "Migration failed", service: "Migrations", details: err?.message, stack: err?.stack });
throw error;
}
};
export { runMigrations };
File renamed without changes.
+299
View File
@@ -0,0 +1,299 @@
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,
ILighthouseAudit,
} from "@/types/check.js";
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 },
frequency: { type: Number, default: 0 },
temperature: { type: [Number], default: [] },
free_percent: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
},
{ _id: false }
);
const memorySchema = new Schema<CheckMemoryInfo>(
{
total_bytes: { type: Number, default: 0 },
available_bytes: { type: Number, default: 0 },
used_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
},
{ _id: false }
);
const diskSchema = new Schema<CheckDiskInfo>(
{
device: { type: String, default: "" },
mountpoint: { type: String, default: "" },
read_speed_bytes: { type: Number, default: 0 },
write_speed_bytes: { type: Number, default: 0 },
total_bytes: { type: Number, default: 0 },
free_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
},
{ _id: false }
);
const hostSchema = new Schema<CheckHostInfo>(
{
os: { type: String, default: "" },
platform: { type: String, default: "" },
kernel_version: { type: String, default: "" },
},
{ _id: false }
);
const errorSchema = new Schema<CheckErrorInfo>(
{
metric: { type: [String], default: [] },
err: { type: String, default: "" },
},
{ _id: false }
);
const captureSchema = new Schema<CheckCaptureInfo>(
{
version: { type: String, default: "" },
mode: { type: String, default: "" },
},
{ _id: false }
);
const networkInterfaceSchema = new Schema<CheckNetworkInterfaceInfo>(
{
name: { type: String, default: "" },
bytes_sent: { type: Number, default: 0 },
bytes_recv: { type: Number, default: 0 },
packets_sent: { type: Number, default: 0 },
packets_recv: { type: Number, default: 0 },
err_in: { type: Number, default: 0 },
err_out: { type: Number, default: 0 },
drop_in: { type: Number, default: 0 },
drop_out: { type: Number, default: 0 },
fifo_in: { type: Number, default: 0 },
fifo_out: { type: Number, default: 0 },
},
{ _id: false }
);
const lighthouseAuditSchema = new Schema<ILighthouseAudit>(
{
id: { type: String },
title: { type: String },
score: { type: Number },
displayValue: { type: String },
numericValue: { type: Number },
numericUnit: { type: String },
},
{ _id: false }
);
const auditsSchema = new Schema<CheckAudits>(
{
cls: { type: lighthouseAuditSchema, default: undefined },
si: { type: lighthouseAuditSchema, default: undefined },
fcp: { type: lighthouseAuditSchema, default: undefined },
lcp: { type: lighthouseAuditSchema, default: undefined },
tbt: { type: lighthouseAuditSchema, default: undefined },
},
{ _id: false }
);
const metadataSchema = new Schema<CheckMetadataDocument>(
{
monitorId: {
type: Schema.Types.ObjectId,
ref: "Monitor",
required: true,
immutable: true,
index: true,
},
teamId: {
type: Schema.Types.ObjectId,
ref: "Team",
required: true,
immutable: true,
index: true,
},
type: {
type: String,
enum: MonitorTypes,
required: true,
index: true,
},
},
{ _id: false }
);
const CheckSchema = new Schema<CheckDocument>(
{
metadata: {
type: metadataSchema,
required: true,
},
status: {
type: Boolean,
index: true,
},
responseTime: {
type: Number,
},
timings: {
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,
},
cpu: {
type: cpuSchema,
default: () => ({}),
},
memory: {
type: memorySchema,
default: () => ({}),
},
disk: {
type: [diskSchema],
default: () => [],
},
host: {
type: hostSchema,
default: () => ({}),
},
errors: {
type: [errorSchema],
default: () => [],
},
capture: {
type: captureSchema,
default: () => ({}),
},
net: {
type: [networkInterfaceSchema],
default: () => [],
},
accessibility: {
type: Number,
},
bestPractices: {
type: Number,
},
seo: {
type: Number,
},
performance: {
type: Number,
},
audits: {
type: auditsSchema,
default: undefined,
},
},
{
timestamps: true,
strict: false,
timeseries: {
timeField: "createdAt",
metaField: "metadata",
granularity: "seconds",
},
}
);
CheckSchema.index({ updatedAt: 1 });
CheckSchema.index({ "metadata.monitorId": 1, updatedAt: 1 });
CheckSchema.index({ "metadata.monitorId": 1, updatedAt: -1 });
CheckSchema.index({ "metadata.monitorId": 1, createdAt: -1 });
CheckSchema.index({ "metadata.teamId": 1, updatedAt: -1 });
CheckSchema.index({ "metadata.teamId": 1, createdAt: -1 });
const CheckModel = model<CheckDocument>("Check", CheckSchema);
export type { CheckDocument, CheckMetadataDocument };
export { CheckModel };
export default CheckModel;
File renamed without changes.
File renamed without changes.
+13
View File
@@ -0,0 +1,13 @@
import mongoose from "mongoose";
const MigrationSchema = new mongoose.Schema(
{
name: { type: String, required: true, unique: true },
status: { type: String, enum: ["completed", "failed"], default: "completed" },
completedAt: { type: Date },
error: { type: String },
},
{ timestamps: true }
);
export default mongoose.model("Migration", MigrationSchema);
+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;
+63
View File
@@ -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;
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+8
View File
@@ -0,0 +1,8 @@
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";
export * from "@/db/models/MonitorStats.js";
export { default as MonitorStatsModel } from "@/db/models/MonitorStats.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, runValidators: true });
} 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;
@@ -1,20 +1,23 @@
import {
buildUptimeDetailsPipeline,
buildHardwareDetailsPipeline,
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
getHardwareStats,
getUpChecks,
getAggregateData,
} 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 +126,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)
@@ -188,17 +194,6 @@ class MonitorModule {
};
};
getAllMonitors = async () => {
try {
const monitors = await this.Monitor.find();
return monitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllMonitors";
throw error;
}
};
getMonitorById = async (monitorId) => {
try {
const monitor = await this.Monitor.findById(monitorId);
@@ -238,7 +233,7 @@ class MonitorModule {
const dateString = formatLookup[dateRange];
const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString));
const results = await CheckModel.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString));
const monitorData = results[0];
@@ -258,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);
@@ -313,9 +266,17 @@ class MonitorModule {
};
const dateString = formatLookup[dateRange];
const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
const [aggregateData, upChecksCount, metrics] = await Promise.all([
getAggregateData(monitorId, dates),
getUpChecks(monitorId, dates),
getHardwareStats(monitorId, dates, dateString),
]);
const stats = hardwareStats[0];
const stats = {
aggregateData: aggregateData,
upChecks: upChecksCount,
checks: metrics,
};
return {
...monitor.toObject(),
@@ -328,47 +289,33 @@ class MonitorModule {
}
};
getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => {
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build match stage
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage }));
const summary = summaryResult[0];
const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order }));
const filteredMonitors = await this.Monitor.aggregate(
buildFilteredMonitorsByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
);
const normalizedFilteredMonitors = filteredMonitors.map((monitor) => {
if (!monitor.checks) {
return monitor;
getMonitorsByTeamId = async ({ teamId, type, filter }) => {
try {
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
monitor.checks = this.NormalizeData(monitor.checks, 10, 100);
return monitor;
});
return { summary, monitors, filteredMonitors: normalizedFilteredMonitors };
if (filter !== undefined && filter !== null && filter !== "") {
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
}
const monitors = await this.Monitor.find(matchStage)
.sort({ name: 1 })
.select({
_id: 1,
name: 1,
type: 1,
url: 1,
status: 1,
isActive: 1,
teamId: 1,
})
.lean();
return monitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsByTeamId";
throw error;
}
};
getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => {
+740
View File
@@ -0,0 +1,740 @@
import { ObjectId } from "mongodb";
import { CheckModel } from "@/db/models/index.js";
const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
return [
{
$match: {
"metadata.monitorId": new ObjectId(monitorId),
updatedAt: { $gte: dates.start, $lte: dates.end },
},
},
{
$sort: {
updatedAt: 1,
},
},
{
$facet: {
// For the response time chart, should return checks for date window
// Grouped by: {day: hour}, {week: day}, {month: day}
uptimePercentage: [
{
$group: {
_id: null,
upChecks: {
$sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] },
},
totalChecks: { $sum: 1 },
},
},
{
$project: {
_id: 0,
percentage: {
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
},
},
},
],
groupedAvgResponseTime: [
{
$group: {
_id: null,
avgResponseTime: {
$avg: "$responseTime",
},
},
},
],
groupedChecks: [
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
avgResponseTime: {
$avg: "$responseTime",
},
totalChecks: {
$sum: 1,
},
},
},
{
$sort: {
_id: 1,
},
},
],
// Up checks grouped by: {day: hour}, {week: day}, {month: day}
groupedUpChecks: [
{
$match: {
status: true,
},
},
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
totalChecks: {
$sum: 1,
},
avgResponseTime: {
$avg: "$responseTime",
},
},
},
{
$sort: { _id: 1 },
},
],
// Down checks grouped by: {day: hour}, {week: day}, {month: day} for the date window
groupedDownChecks: [
{
$match: {
status: false,
},
},
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
totalChecks: {
$sum: 1,
},
avgResponseTime: {
$avg: "$responseTime",
},
},
},
{
$sort: { _id: 1 },
},
],
},
},
{
$lookup: {
from: "monitors",
let: { monitor_id: { $toObjectId: monitorId } },
pipeline: [
{
$match: {
$expr: { $eq: ["$_id", "$$monitor_id"] },
},
},
{
$project: {
_id: 1,
teamId: 1,
name: 1,
status: 1,
interval: 1,
type: 1,
url: 1,
isActive: 1,
notifications: 1,
},
},
],
as: "monitor",
},
},
{
$project: {
groupedAvgResponseTime: {
$arrayElemAt: ["$groupedAvgResponseTime.avgResponseTime", 0],
},
groupedChecks: "$groupedChecks",
groupedUpChecks: "$groupedUpChecks",
groupedDownChecks: "$groupedDownChecks",
groupedUptimePercentage: { $arrayElemAt: ["$uptimePercentage.percentage", 0] },
monitor: { $arrayElemAt: ["$monitor", 0] },
},
},
];
};
const buildMonitorStatsPipeline = (monitor) => {
return [
{
$match: {
monitorId: monitor._id,
},
},
{
$project: {
avgResponseTime: 1,
uptimePercentage: 1,
totalChecks: 1,
timeSinceLastCheck: {
$subtract: [Date.now(), "$lastCheckTimestamp"],
},
lastCheckTimestamp: 1,
uptBurnt: { $toString: "$uptBurnt" },
},
},
];
};
const buildMonitorSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
];
};
const buildMonitorsByTeamIdPipeline = ({ matchStage, field, order }) => {
const sort = { [field]: order === "asc" ? 1 : -1 };
return [
{ $match: matchStage },
{ $sort: sort },
{
$project: {
_id: 1,
name: 1,
type: 1,
port: 1,
},
},
];
};
const buildMonitorsAndSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
monitors: [
{ $sort: { name: 1 } },
{
$project: {
_id: 1,
name: 1,
type: 1,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
monitors: 1,
},
},
];
};
const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => {
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : [];
// Match name
if (typeof filter !== "undefined" && field === "name") {
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
}
// Match isActive
if (typeof filter !== "undefined" && field === "isActive") {
matchStage.isActive = filter === "true" ? true : false;
}
if (typeof filter !== "undefined" && field === "status") {
matchStage.status = filter === "true" ? true : false;
}
// Match type
if (typeof filter !== "undefined" && field === "type") {
matchStage.type = filter;
}
const monitorsPipeline = [
{ $sort: sort },
{ $skip: skip },
...limitStage,
{
$project: {
_id: 1,
name: 1,
description: 1,
type: 1,
url: 1,
isActive: 1,
createdAt: 1,
updatedAt: 1,
uptimePercentage: 1,
status: 1,
},
},
];
// Add checks
if (limit) {
const checksCollection = "checks";
monitorsPipeline.push({
$lookup: {
from: checksCollection,
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { updatedAt: -1 } },
{ $limit: limit },
{
$project: {
_id: 1,
status: 1,
responseTime: 1,
statusCode: 1,
createdAt: 1,
updatedAt: 1,
originalResponseTime: 1,
},
},
],
as: "checks",
},
});
}
const pipeline = [
{ $match: matchStage },
{
$facet: {
count: [{ $count: "monitorsCount" }],
monitors: monitorsPipeline,
},
},
{
$project: {
count: { $arrayElemAt: ["$count", 0] },
monitors: 1,
},
},
];
return pipeline;
};
const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => {
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : [];
if (typeof filter !== "undefined" && field === "name") {
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
}
if (typeof filter !== "undefined" && field === "status") {
matchStage.status = filter === "true";
}
const pipeline = [{ $match: matchStage }, { $sort: sort }, { $skip: skip }, ...limitStage];
// Add checks
if (limit) {
const checksCollection = "checks";
pipeline.push({
$lookup: {
from: checksCollection,
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
{ $limit: limit },
],
as: "checks",
},
});
}
return pipeline;
};
const buildGetMonitorsByTeamIdPipeline = (req) => {
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build the match stage
const matchStage = { teamId: new ObjectId(req.params.teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
monitors: [
{ $sort: sort },
{
$project: {
_id: 1,
name: 1,
},
},
],
filteredMonitors: [
...(filter !== undefined
? [
{
$match: {
$or: [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }],
},
},
]
: []),
{ $sort: sort },
{ $skip: skip },
...(rowsPerPage ? [{ $limit: rowsPerPage }] : []),
...(limit
? [
{
$lookup: {
from: "checks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "standardchecks",
},
},
]
: []),
{
$addFields: {
checks: {
$switch: {
branches: [
{
case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] },
then: "$standardchecks",
},
],
default: [],
},
},
},
},
{
$project: {
standardchecks: 0,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
filteredMonitors: 1,
monitors: 1,
},
},
];
};
export {
buildUptimeDetailsPipeline,
buildMonitorStatsPipeline,
buildGetMonitorsByTeamIdPipeline,
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
};
export const getAggregateData = async (monitorId, dates) => {
const result = await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": new ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{ $sort: { createdAt: -1 } },
{
$group: {
_id: null,
latestCheck: { $first: "$$ROOT" },
totalChecks: { $sum: 1 },
},
},
]);
return result[0] || { totalChecks: 0, latestCheck: null };
};
export const getUpChecks = async (monitorId, dates) => {
const count = await CheckModel.countDocuments({
"metadata.monitorId": new ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
status: true,
});
return { totalChecks: count };
};
export const getHardwareStats = async (monitorId, dates, dateString) => {
return await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": new ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{ $sort: { createdAt: 1 } },
{
$group: {
_id: { $dateToString: { format: dateString, date: "$createdAt" } },
avgCpuUsage: { $avg: "$cpu.usage_percent" },
avgMemoryUsage: { $avg: "$memory.usage_percent" },
avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } },
disks: { $push: "$disk" },
net: { $push: "$net" },
updatedAts: { $push: "$updatedAt" },
sampleDoc: { $first: "$$ROOT" },
},
},
{
$project: {
_id: 1,
avgCpuUsage: 1,
avgMemoryUsage: 1,
avgTemperature: {
$map: {
input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] },
as: "idx",
in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } },
},
},
disks: {
$map: {
input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] },
as: "dIdx",
in: {
name: { $concat: ["disk", { $toString: "$$dIdx" }] },
readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } },
writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } },
totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } },
freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } },
usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } },
},
},
},
net: {
$map: {
input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] },
as: "nIdx",
in: {
name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] },
bytesSentPerSecond: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaBytesRecv: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaPacketsSent: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaPacketsRecv: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaErrIn: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaErrOut: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaDropIn: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaDropOut: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
},
},
},
},
},
{ $sort: { _id: 1 } },
]);
};
@@ -153,7 +153,7 @@ class StatusPageModule {
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
$expr: { $eq: ["$metadata.monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
File renamed without changes.
File renamed without changes.
@@ -1,18 +0,0 @@
// import Monitor from "@/db/v1/models/Monitor.js";
async function migrateStatusWindowThreshold() {
try {
// const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } });
// for (const monitor of monitors) {
// monitor.statusWindowThreshold = monitor.statusWindowThreshold * 100;
// await monitor.save();
// console.log(`Migrated monitor ${monitor._id}: statusWindowThreshold set to ${monitor.statusWindowThreshold}`);
// }
// console.log("StatusWindowThreshold migration complete.");
return true;
} catch (err) {
console.error("Migration error:", err);
return false;
}
}
export { migrateStatusWindowThreshold };
-7
View File
@@ -1,7 +0,0 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
const runMigrations = async () => {
await migrateStatusWindowThreshold();
};
export { runMigrations };
-178
View File
@@ -1,178 +0,0 @@
import mongoose from "mongoose";
const cpuSchema = mongoose.Schema({
physical_core: { type: Number, default: 0 },
logical_core: { type: Number, default: 0 },
frequency: { type: Number, default: 0 },
temperature: { type: [Number], default: [] },
free_percent: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const memorySchema = mongoose.Schema({
total_bytes: { type: Number, default: 0 },
available_bytes: { type: Number, default: 0 },
used_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const diskSchema = mongoose.Schema({
device: { type: String, default: "" },
mountpoint: { type: String, default: "" },
read_speed_bytes: { type: Number, default: 0 },
write_speed_bytes: { type: Number, default: 0 },
total_bytes: { type: Number, default: 0 },
free_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const hostSchema = mongoose.Schema({
os: { type: String, default: "" },
platform: { type: String, default: "" },
kernel_version: { type: String, default: "" },
});
const errorSchema = mongoose.Schema({
metric: { type: [String], default: [] },
err: { type: String, default: "" },
});
const captureSchema = mongoose.Schema({
version: { type: String, default: "" },
mode: { type: String, default: "" },
});
const networkInterfaceSchema = mongoose.Schema({
name: { type: String },
bytes_sent: { type: Number, default: 0 },
bytes_recv: { type: Number, default: 0 },
packets_sent: { type: Number, default: 0 },
packets_recv: { type: Number, default: 0 },
err_in: { type: Number, default: 0 },
err_out: { type: Number, default: 0 },
drop_in: { type: Number, default: 0 },
drop_out: { type: Number, default: 0 },
fifo_in: { type: Number, default: 0 },
fifo_out: { type: Number, default: 0 },
});
const CheckSchema = new mongoose.Schema(
{
// Common fields
monitorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
immutable: true,
index: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Team",
immutable: true,
index: true,
},
type: {
type: String,
enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"],
required: true,
index: true,
},
status: {
type: Boolean,
index: true,
},
responseTime: {
type: Number,
},
timings: {
type: Object,
default: {},
},
statusCode: {
type: Number,
index: true,
},
message: {
type: String,
},
expiry: {
type: Date,
default: Date.now,
expires: 60 * 60 * 24 * 30, // 30 days
},
ack: {
type: Boolean,
default: false,
},
ackAt: {
type: Date,
},
// Hardware fields
cpu: {
type: cpuSchema,
default: () => ({}),
},
memory: {
type: memorySchema,
default: () => ({}),
},
disk: {
type: [diskSchema],
default: () => [],
},
host: {
type: hostSchema,
default: () => ({}),
},
errors: {
type: [errorSchema],
default: () => [],
},
capture: {
type: captureSchema,
default: () => ({}),
},
net: {
type: [networkInterfaceSchema],
default: () => [],
},
// PageSpeed fields
accessibility: {
type: Number,
},
bestPractices: {
type: Number,
},
seo: {
type: Number,
},
performance: {
type: Number,
},
audits: {
type: Object,
},
},
{ timestamps: true }
);
CheckSchema.index({ updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: -1 });
CheckSchema.index({ teamId: 1, updatedAt: -1 });
export default mongoose.model("Check", CheckSchema);
-49
View File
@@ -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;
File diff suppressed because it is too large. Load diff