Merge upstream/develop into fix/3120-data-leak
This commit is contained in:
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();
|
||||
}
|
||||
};
|
||||
@@ -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.
@@ -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.
File renamed without changes.
@@ -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);
|
||||
Executable → Regular
+48
-24
@@ -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;
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
import { Schema, model, type Types } from "mongoose";
|
||||
import type { MonitorStats as MonitorStatsEntity } from "@/types/monitorStats.js";
|
||||
|
||||
type MonitorStatsDocumentBase = Omit<MonitorStatsEntity, "id" | "monitorId" | "createdAt" | "updatedAt"> & {
|
||||
monitorId: Types.ObjectId;
|
||||
};
|
||||
|
||||
interface MonitorStatsDocument extends MonitorStatsDocumentBase {
|
||||
_id: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MonitorStatsSchema = new Schema<MonitorStatsDocument>(
|
||||
{
|
||||
monitorId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Monitor",
|
||||
immutable: true,
|
||||
index: true,
|
||||
required: true,
|
||||
},
|
||||
avgResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalUpChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
totalDownChecks: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
uptimePercentage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastCheckTimestamp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
timeOfLastFailure: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
const MonitorStatsModel = model<MonitorStatsDocument>("MonitorStats", MonitorStatsSchema);
|
||||
|
||||
export type { MonitorStatsDocument };
|
||||
export { MonitorStatsModel };
|
||||
export default MonitorStatsModel;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+51
-104
@@ -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
@@ -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 } },
|
||||
]);
|
||||
};
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-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 };
|
||||
@@ -1,7 +0,0 @@
|
||||
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
|
||||
|
||||
const runMigrations = async () => {
|
||||
await migrateStatusWindowThreshold();
|
||||
};
|
||||
|
||||
export { runMigrations };
|
||||
@@ -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);
|
||||
@@ -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
Reference in new issue
Block a user