Merge pull request #2976 from bluewave-labs/feat/v2/models
feat: add v2 models
This commit is contained in:
13 files changed
+812
-753
No files matched your search
@@ -1,157 +0,0 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import ApiError from "../../utils/ApiError.js";
|
||||
import MonitorService from "../../service/v2/business/MonitorService.js";
|
||||
import { MonitorType } from "../../db/v1/models/Monitor.js";
|
||||
class MonitorController {
|
||||
private monitorService: MonitorService;
|
||||
constructor(monitorService: MonitorService) {
|
||||
this.monitorService = monitorService;
|
||||
}
|
||||
|
||||
create = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const monitor = await this.monitorService.create(tokenizedUser, req.body);
|
||||
res.status(201).json({
|
||||
message: "Monitor created successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
get = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const range = req.query.range;
|
||||
if (!range || typeof range !== "string") throw new ApiError("Range query parameter is required", 400);
|
||||
|
||||
let monitor;
|
||||
|
||||
const status = req.query.status;
|
||||
if (status && typeof status !== "string") {
|
||||
throw new ApiError("Status query parameter must be a string", 400);
|
||||
}
|
||||
|
||||
if (req.query.embedChecks === "true") {
|
||||
monitor = await this.monitorService.getEmbedChecks(id, range, status);
|
||||
} else {
|
||||
monitor = await this.monitorService.get(id);
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: "Monitor retrieved successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
getAll = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
let monitors;
|
||||
if (req.query.embedChecks === "true") {
|
||||
const page = Math.max(1, Number(req.query.page) || 1);
|
||||
const limit = Math.max(1, Number(req.query.limit) || 10);
|
||||
const type: MonitorType[] = req.query.type as MonitorType[];
|
||||
|
||||
monitors = await this.monitorService.getAllEmbedChecks(page, limit, type);
|
||||
} else {
|
||||
monitors = await this.monitorService.getAll();
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
message: "Monitors retrieved successfully",
|
||||
data: monitors,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
toggleActive = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const monitor = await this.monitorService.toggleActive(id, tokenizedUser);
|
||||
res.status(200).json({
|
||||
message: "Monitor paused/unpaused successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
update = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
|
||||
const monitor = await this.monitorService.update(tokenizedUser, id, req.body);
|
||||
res.status(200).json({
|
||||
message: "Monitor updated successfully",
|
||||
data: monitor,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
delete = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const tokenizedUser = req.user;
|
||||
if (!tokenizedUser) {
|
||||
return res.status(401).json({ message: "Unauthorized" });
|
||||
}
|
||||
const id = req.params.id;
|
||||
if (!id) {
|
||||
throw new ApiError("Monitor ID is required", 400);
|
||||
}
|
||||
await this.monitorService.delete(id);
|
||||
|
||||
res.status(200).json({
|
||||
message: "Monitor deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default MonitorController;
|
||||
@@ -0,0 +1,25 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/checkmate";
|
||||
|
||||
export const connectDatabase = async (): Promise<boolean> => {
|
||||
try {
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log("Connected to MongoDB");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("MongoDB connection error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
export const disconnectDatabase = async (): Promise<boolean> => {
|
||||
try {
|
||||
await mongoose.disconnect();
|
||||
console.log("Disconnected from MongoDB");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("MongoDB disconnection error:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
|
||||
export interface IRole extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: string[];
|
||||
isSystem: boolean;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const roleSchema = new Schema<IRole>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 50,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxlength: 200,
|
||||
},
|
||||
|
||||
permissions: [
|
||||
{
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
isSystem: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
roleSchema.index({ name: 1 }, { unique: true });
|
||||
|
||||
export const Role = mongoose.model<IRole>("Role_v2", roleSchema);
|
||||
@@ -0,0 +1,102 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
|
||||
export interface ITokenizedUser {
|
||||
sub: string;
|
||||
roles: string[];
|
||||
}
|
||||
export interface IUser extends Document {
|
||||
_id: Types.ObjectId;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
passwordHash: string;
|
||||
roles: Types.ObjectId[];
|
||||
profile: {
|
||||
avatar?: string;
|
||||
bio?: string;
|
||||
timezone: string;
|
||||
locale: string;
|
||||
};
|
||||
preferences: {
|
||||
theme: "light" | "dark" | "system";
|
||||
};
|
||||
lastLoginAt?: Date;
|
||||
isActive: boolean;
|
||||
isVerified: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
},
|
||||
firstName: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 50,
|
||||
},
|
||||
lastName: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
maxlength: 50,
|
||||
},
|
||||
passwordHash: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role_v2",
|
||||
},
|
||||
],
|
||||
profile: {
|
||||
avatar: {
|
||||
type: String,
|
||||
},
|
||||
bio: {
|
||||
type: String,
|
||||
maxlength: 200,
|
||||
},
|
||||
timezone: {
|
||||
type: String,
|
||||
default: "UTC",
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: "en",
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
theme: {
|
||||
type: String,
|
||||
enum: ["light", "dark", "system"],
|
||||
default: "system",
|
||||
},
|
||||
},
|
||||
lastLoginAt: {
|
||||
type: Date,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isVerified: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
export const User = mongoose.model<IUser>("User_v2", userSchema);
|
||||
@@ -0,0 +1,297 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
import { MonitorType, MonitorTypes, MonitorStatus, MonitorStatuses } from "../monitors/Monitor.js";
|
||||
import type { Response } from "got";
|
||||
export type GotTimings = Response["timings"];
|
||||
export interface ITimingPhases {
|
||||
wait: number;
|
||||
dns: number;
|
||||
tcp: number;
|
||||
tls: number;
|
||||
request: number;
|
||||
firstByte: number;
|
||||
download: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ICpuInfo {
|
||||
physical_core: number;
|
||||
logical_core: number;
|
||||
frequency: number;
|
||||
current_frequency: number;
|
||||
temperature: number[]; // per-core temps
|
||||
free_percent: number;
|
||||
usage_percent: number;
|
||||
}
|
||||
|
||||
export interface IMemoryInfo {
|
||||
total_bytes: number;
|
||||
available_bytes: number;
|
||||
used_bytes: number;
|
||||
usage_percent: number;
|
||||
}
|
||||
|
||||
export interface IHostInfo {
|
||||
os?: string;
|
||||
platform?: string;
|
||||
kernel_version?: string;
|
||||
pretty_name?: string;
|
||||
}
|
||||
|
||||
export interface IDiskInfo {
|
||||
device: string;
|
||||
total_bytes: number;
|
||||
free_bytes: number;
|
||||
used_bytes: number;
|
||||
usage_percent: number;
|
||||
total_inodes?: number;
|
||||
free_inodes?: number;
|
||||
used_inodes?: number;
|
||||
inodes_usage_percent?: number;
|
||||
read_bytes?: number;
|
||||
write_bytes?: number;
|
||||
read_time?: number;
|
||||
write_time?: number;
|
||||
}
|
||||
|
||||
export interface INetInfo {
|
||||
name: string;
|
||||
bytes_sent: number;
|
||||
bytes_recv: number;
|
||||
packets_sent: number;
|
||||
packets_recv: number;
|
||||
err_in: number;
|
||||
err_out: number;
|
||||
drop_in: number;
|
||||
drop_out: number;
|
||||
fifo_in: number;
|
||||
fifo_out: number;
|
||||
}
|
||||
|
||||
export interface ICaptureInfo {
|
||||
version?: string;
|
||||
mode?: string;
|
||||
}
|
||||
|
||||
export interface ISystemInfo {
|
||||
cpu: ICpuInfo;
|
||||
memory: IMemoryInfo;
|
||||
disk: IDiskInfo[];
|
||||
host: IHostInfo;
|
||||
net: INetInfo[];
|
||||
}
|
||||
|
||||
export interface ILighthouseAudit {
|
||||
id?: string;
|
||||
title?: string;
|
||||
score?: number | null;
|
||||
displayValue?: string;
|
||||
numericValue?: number;
|
||||
numericUnit?: string;
|
||||
}
|
||||
export interface ILighthouseCategories {
|
||||
accessibility?: { score?: number | null };
|
||||
"best-practices"?: { score?: number | null };
|
||||
seo?: { score?: number | null };
|
||||
performance?: { score?: number | null };
|
||||
}
|
||||
|
||||
export interface ILighthouseResult {
|
||||
categories?: ILighthouseCategories;
|
||||
audits?: Record<string, ILighthouseAudit>;
|
||||
}
|
||||
|
||||
export interface ICheckLighthouseFields {
|
||||
accessibility: number;
|
||||
bestPractices: number;
|
||||
seo: number;
|
||||
performance: number;
|
||||
audits: {
|
||||
cls: ILighthouseAudit;
|
||||
si: ILighthouseAudit;
|
||||
fcp: ILighthouseAudit;
|
||||
lcp: ILighthouseAudit;
|
||||
tbt: ILighthouseAudit;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ICheck extends Document {
|
||||
_id: Types.ObjectId;
|
||||
monitorId: Types.ObjectId;
|
||||
type: MonitorType;
|
||||
status: MonitorStatus;
|
||||
message: string;
|
||||
responseTime?: number; // in ms
|
||||
timings?: GotTimings;
|
||||
httpStatusCode?: number;
|
||||
errorMessage?: string;
|
||||
ack: boolean;
|
||||
ackAt?: Date;
|
||||
ackBy?: Types.ObjectId;
|
||||
expiry: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
system?: ISystemInfo;
|
||||
capture?: ICaptureInfo;
|
||||
lighthouse?: ICheckLighthouseFields;
|
||||
}
|
||||
|
||||
const CheckSchema = new Schema<ICheck>(
|
||||
{
|
||||
monitorId: { type: Schema.Types.ObjectId, ref: "Monitor_v2", required: true },
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: MonitorTypes,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: MonitorStatuses,
|
||||
},
|
||||
message: { type: String, trim: true },
|
||||
responseTime: { type: Number },
|
||||
timings: {
|
||||
start: { type: Date },
|
||||
socket: { type: Date },
|
||||
lookup: { type: Date },
|
||||
connect: { type: Date },
|
||||
secureConnect: { type: Date },
|
||||
response: { type: Date },
|
||||
end: { type: Date },
|
||||
phases: {
|
||||
wait: { type: Number },
|
||||
dns: { type: Number },
|
||||
tcp: { type: Number },
|
||||
tls: { type: Number },
|
||||
request: { type: Number },
|
||||
firstByte: { type: Number },
|
||||
download: { type: Number },
|
||||
total: { type: Number },
|
||||
},
|
||||
},
|
||||
system: {
|
||||
type: {
|
||||
cpu: {
|
||||
physical_core: { type: Number },
|
||||
logical_core: { type: Number },
|
||||
frequency: { type: Number },
|
||||
current_frequency: { type: Number },
|
||||
temperature: [{ type: Number }],
|
||||
free_percent: { type: Number },
|
||||
usage_percent: { type: Number },
|
||||
},
|
||||
memory: {
|
||||
total_bytes: { type: Number },
|
||||
available_bytes: { type: Number },
|
||||
used_bytes: { type: Number },
|
||||
usage_percent: { type: Number },
|
||||
},
|
||||
disk: [
|
||||
{
|
||||
device: { type: String },
|
||||
total_bytes: { type: Number },
|
||||
free_bytes: { type: Number },
|
||||
used_bytes: { type: Number },
|
||||
usage_percent: { type: Number },
|
||||
total_inodes: { type: Number },
|
||||
free_inodes: { type: Number },
|
||||
used_inodes: { type: Number },
|
||||
inodes_usage_percent: { type: Number },
|
||||
read_bytes: { type: Number },
|
||||
write_bytes: { type: Number },
|
||||
read_time: { type: Number },
|
||||
write_time: { type: Number },
|
||||
},
|
||||
],
|
||||
host: {
|
||||
os: { type: String },
|
||||
platform: { type: String },
|
||||
kernel_version: { type: String },
|
||||
pretty_name: { type: String },
|
||||
},
|
||||
net: [
|
||||
{
|
||||
name: { type: String },
|
||||
bytes_sent: { type: Number },
|
||||
bytes_recv: { type: Number },
|
||||
packets_sent: { type: Number },
|
||||
packets_recv: { type: Number },
|
||||
err_in: { type: Number },
|
||||
err_out: { type: Number },
|
||||
drop_in: { type: Number },
|
||||
drop_out: { type: Number },
|
||||
fifo_in: { type: Number },
|
||||
fifo_out: { type: Number },
|
||||
},
|
||||
],
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
|
||||
capture: {
|
||||
type: {
|
||||
version: { type: String },
|
||||
mode: { type: String },
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
lighthouse: {
|
||||
accessibility: { type: Number, required: false },
|
||||
bestPractices: { type: Number, required: false },
|
||||
seo: { type: Number, required: false },
|
||||
performance: { type: Number, required: false },
|
||||
audits: {
|
||||
cls: {
|
||||
type: Object,
|
||||
},
|
||||
si: {
|
||||
type: Object,
|
||||
},
|
||||
fcp: {
|
||||
type: Object,
|
||||
},
|
||||
lcp: {
|
||||
type: Object,
|
||||
},
|
||||
tbt: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
type: {
|
||||
accessibility: { type: Number },
|
||||
bestPractices: { type: Number },
|
||||
seo: { type: Number },
|
||||
performance: { type: Number },
|
||||
audits: {
|
||||
cls: { type: Object },
|
||||
si: { type: Object },
|
||||
fcp: { type: Object },
|
||||
lcp: { type: Object },
|
||||
tbt: { type: Object },
|
||||
},
|
||||
},
|
||||
required: false,
|
||||
},
|
||||
|
||||
httpStatusCode: { type: Number },
|
||||
errorMessage: { type: String, trim: true },
|
||||
ack: { type: Boolean, required: true, default: false },
|
||||
ackAt: { type: Date },
|
||||
ackBy: { type: Schema.Types.ObjectId, ref: "User_v2" },
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 60 * 60 * 24 * 30,
|
||||
},
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
CheckSchema.index({ monitorId: 1, createdAt: -1 });
|
||||
CheckSchema.index({ status: 1 });
|
||||
CheckSchema.index({ status: 1, ack: 1 });
|
||||
CheckSchema.index({ ack: 1, ackAt: 1 });
|
||||
CheckSchema.index({ createdAt: -1 });
|
||||
|
||||
export const Check = mongoose.model<ICheck>("Check_v2", CheckSchema);
|
||||
@@ -0,0 +1,33 @@
|
||||
export { User } from "./auth/User.js";
|
||||
export type { IUser } from "./auth/User.js";
|
||||
export type { ITokenizedUser } from "./auth/User.js";
|
||||
export { Role } from "./auth/Role.js";
|
||||
export type { IRole } from "./auth/Role.js";
|
||||
export { connectDatabase, disconnectDatabase } from "../index.js";
|
||||
export { Monitor } from "./monitors/Monitor.js";
|
||||
export { MonitorStatuses } from "./monitors/Monitor.js";
|
||||
export type { IMonitor } from "./monitors/Monitor.js";
|
||||
export { Check } from "./checks/Check.js";
|
||||
export type {
|
||||
ICheck,
|
||||
ISystemInfo,
|
||||
ICaptureInfo,
|
||||
INetInfo,
|
||||
IDiskInfo,
|
||||
IHostInfo,
|
||||
IMemoryInfo,
|
||||
ICpuInfo,
|
||||
ILighthouseAudit,
|
||||
ITimingPhases,
|
||||
ILighthouseCategories,
|
||||
ILighthouseResult,
|
||||
ICheckLighthouseFields,
|
||||
} from "./checks/Check.js";
|
||||
export type { IMonitorStats } from "./monitors/MonitorStats.js";
|
||||
export { MonitorStats } from "./monitors/MonitorStats.js";
|
||||
export type { INotificationChannel } from "./notification-channel/NotificationChannel.js";
|
||||
export { NotificationChannel } from "./notification-channel/NotificationChannel.js";
|
||||
export type { IMaintenance } from "./maintenance/Maintenance.js";
|
||||
export { Maintenance } from "./maintenance/Maintenance.js";
|
||||
export type { IInvite } from "./invite/Invite.js";
|
||||
export { Invite } from "./invite/Invite.js";
|
||||
@@ -0,0 +1,43 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
|
||||
export interface IInvite extends Document {
|
||||
_id: Types.ObjectId;
|
||||
email: string;
|
||||
tokenHash: string;
|
||||
roles: Types.ObjectId[];
|
||||
createdBy: Types.ObjectId;
|
||||
updatedBy: Types.ObjectId;
|
||||
expiry: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const InviteSchema = new Schema<IInvite>(
|
||||
{
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Role_v2",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
tokenHash: { type: String, required: true, unique: true },
|
||||
expiry: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 60 * 60 * 24,
|
||||
},
|
||||
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
export const Invite = mongoose.model<IInvite>("Invite_v2", InviteSchema);
|
||||
@@ -0,0 +1,39 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
|
||||
export interface IMaintenance extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
monitors: Types.ObjectId[];
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
createdBy: Types.ObjectId;
|
||||
updatedBy: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MaintenanceSchema = new Schema<IMaintenance>(
|
||||
{
|
||||
name: { type: String, required: true, trim: true },
|
||||
isActive: { type: Boolean, required: true, default: true },
|
||||
monitors: [
|
||||
{
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Monitor_v2",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
startTime: { type: Date, required: true },
|
||||
endTime: { type: Date, required: true },
|
||||
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
MaintenanceSchema.index({ isActive: 1 });
|
||||
MaintenanceSchema.index({ startTime: 1 });
|
||||
MaintenanceSchema.index({ endTime: 1 });
|
||||
|
||||
export const Maintenance = mongoose.model<IMaintenance>("Maintenance_v2", MaintenanceSchema);
|
||||
@@ -0,0 +1,96 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
import { Check, MonitorStats } from "../index.js";
|
||||
|
||||
export const MonitorTypes = ["http", "https", "ping", "infrastructure", "pagespeed"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const MonitorStatuses = ["up", "down", "paused", "initializing"] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
export interface IMonitor extends Document {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
url: string;
|
||||
secret?: string;
|
||||
type: MonitorType;
|
||||
interval: number; // in ms
|
||||
isActive: boolean;
|
||||
status: MonitorStatus;
|
||||
n: number; // Number of consecutive successes required to change status
|
||||
lastCheckedAt?: Date;
|
||||
latestChecks: {
|
||||
status: MonitorStatus;
|
||||
responseTime: number;
|
||||
checkedAt: Date;
|
||||
}[];
|
||||
notificationChannels?: Types.ObjectId[];
|
||||
createdBy: Types.ObjectId;
|
||||
updatedBy: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MonitorSchema = new Schema<IMonitor>(
|
||||
{
|
||||
name: { type: String, required: true, trim: true, maxlength: 100 },
|
||||
url: { type: String, required: true, trim: true },
|
||||
secret: { type: String, required: false },
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: MonitorTypes,
|
||||
},
|
||||
interval: { type: Number, required: true, default: 60000 },
|
||||
isActive: { type: Boolean, required: true, default: true },
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "initializing",
|
||||
enum: MonitorStatuses,
|
||||
},
|
||||
n: { type: Number, required: true, default: 1 },
|
||||
lastCheckedAt: { type: Date },
|
||||
latestChecks: {
|
||||
type: [
|
||||
{
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: MonitorStatuses,
|
||||
},
|
||||
responseTime: { type: Number, required: true },
|
||||
checkedAt: { type: Date, required: true },
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
},
|
||||
notificationChannels: {
|
||||
type: [{ type: Schema.Types.ObjectId, ref: "NotificationChannel_v2" }],
|
||||
default: [],
|
||||
},
|
||||
|
||||
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
MonitorSchema.pre("deleteOne", { document: true, query: false }, async function (next) {
|
||||
try {
|
||||
const monitorId = this._id;
|
||||
await Check.deleteMany({ monitorId });
|
||||
await MonitorStats.deleteMany({ monitorId });
|
||||
next();
|
||||
} catch (error: any) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
MonitorSchema.index({ isActive: 1 });
|
||||
MonitorSchema.index({ status: 1 });
|
||||
MonitorSchema.index({ type: 1 });
|
||||
MonitorSchema.index({ lastCheckedAt: 1 });
|
||||
MonitorSchema.index({ isActive: 1, status: 1 });
|
||||
MonitorSchema.index({ createdBy: 1 });
|
||||
MonitorSchema.index({ updatedBy: 1 });
|
||||
|
||||
export const Monitor = mongoose.model<IMonitor>("Monitor_v2", MonitorSchema);
|
||||
@@ -0,0 +1,77 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
import { MonitorStatus, MonitorStatuses } from "./Monitor.js";
|
||||
|
||||
export interface IMonitorStats extends mongoose.Document {
|
||||
monitorId: mongoose.Types.ObjectId;
|
||||
avgResponseTime: number;
|
||||
maxResponseTime: number;
|
||||
totalChecks: number;
|
||||
totalUpChecks: number;
|
||||
totalDownChecks: number;
|
||||
uptimePercentage: number;
|
||||
lastCheckTimestamp: number;
|
||||
lastResponseTime: number;
|
||||
timeOfLastFailure: number;
|
||||
currentStreak: number;
|
||||
currentStreakStatus: MonitorStatus;
|
||||
currentStreakStartedAt: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const MonitorStatsSchema = new Schema<IMonitorStats>(
|
||||
{
|
||||
monitorId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Monitor_v2",
|
||||
immutable: true,
|
||||
index: true,
|
||||
},
|
||||
avgResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
maxResponseTime: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
lastResponseTime: {
|
||||
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,
|
||||
},
|
||||
timeOfLastFailure: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
currentStreak: { type: Number, required: false, default: 0 },
|
||||
currentStreakStatus: {
|
||||
type: String,
|
||||
required: false,
|
||||
enum: MonitorStatuses,
|
||||
},
|
||||
currentStreakStartedAt: { type: Number, required: false },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
export const MonitorStats = mongoose.model<IMonitorStats>("MonitorStats_v2", MonitorStatsSchema);
|
||||
@@ -0,0 +1,50 @@
|
||||
import mongoose, { Schema, Document, Types } from "mongoose";
|
||||
export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const;
|
||||
export type ChannelType = (typeof ChannelTypes)[number];
|
||||
|
||||
export interface INotificationChannelConfig {
|
||||
url?: string; // For webhook, slack, discord
|
||||
emailAddress?: string; // For email
|
||||
}
|
||||
|
||||
export interface INotificationChannel {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
type: ChannelType;
|
||||
config: INotificationChannelConfig;
|
||||
isActive: boolean;
|
||||
createdBy: Types.ObjectId;
|
||||
updatedBy: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const NotificationChannelConfigSchema = new Schema<INotificationChannelConfig>(
|
||||
{
|
||||
url: { type: String, required: false },
|
||||
emailAddress: { type: String, required: false },
|
||||
},
|
||||
{ _id: false, strict: "throw" }
|
||||
);
|
||||
|
||||
const NotificationChannelSchema = new Schema<INotificationChannel>(
|
||||
{
|
||||
name: { type: String, required: true, trim: true },
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ChannelTypes,
|
||||
},
|
||||
config: { type: NotificationChannelConfigSchema, required: true },
|
||||
isActive: { type: Boolean, required: true, default: true },
|
||||
createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
NotificationChannelSchema.index({ isActive: 1 });
|
||||
NotificationChannelSchema.index({ type: 1 });
|
||||
NotificationChannelSchema.index({ type: 1, isActive: 1 });
|
||||
|
||||
export const NotificationChannel = mongoose.model<INotificationChannel>("NotificationChannel_v2", NotificationChannelSchema);
|
||||
@@ -1,132 +0,0 @@
|
||||
import { json } from "stream/consumers";
|
||||
import { ICheck, Check, Monitor, ISystemInfo, ICaptureInfo } from "../../../db/v1/models/index.js";
|
||||
import { MonitorType } from "../../../db/v1/models/Monitor.js";
|
||||
import { StatusResponse } from "../infrastructure/NetworkService.js";
|
||||
import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js";
|
||||
import mongoose from "mongoose";
|
||||
|
||||
export interface ICheckService {
|
||||
buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise<ICheck>;
|
||||
cleanupOrphanedChecks: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
class CheckService implements ICheckService {
|
||||
private isCapturePayload = (payload: any): payload is ICapturePayload => {
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
|
||||
if (!("data" in payload) || typeof payload.data !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = payload.data as Partial<ISystemInfo>;
|
||||
if (!data.cpu || typeof data.cpu !== "object" || typeof data.cpu.usage_percent !== "number") {
|
||||
return false;
|
||||
}
|
||||
s;
|
||||
|
||||
if (!data.memory || typeof data.memory !== "object" || typeof data.memory.usage_percent !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.disk && !Array.isArray(data.disk)) {
|
||||
return false;
|
||||
}
|
||||
if (data.net && !Array.isArray(data.net)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!("capture" in payload) || typeof payload.capture !== "object") return false;
|
||||
const capture = payload.capture as Record<string, any>;
|
||||
if (typeof capture.version !== "string" || typeof capture.mode !== "string") return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
private isPagespeedPayload = (payload: any): payload is ILighthousePayload => {
|
||||
if (!payload || typeof payload !== "object") return false;
|
||||
|
||||
if (!("lighthouseResult" in payload) || typeof payload.lighthouseResult !== "object") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
private buildBaseCheck = (statusResponse: StatusResponse) => {
|
||||
const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId);
|
||||
const check = new Check({
|
||||
monitorId: monitorId,
|
||||
type: statusResponse?.type,
|
||||
status: statusResponse?.status,
|
||||
message: statusResponse?.message,
|
||||
responseTime: statusResponse?.responseTime,
|
||||
timings: statusResponse?.timings,
|
||||
});
|
||||
return check;
|
||||
};
|
||||
|
||||
private buildInfrastructureCheck = (statusResponse: StatusResponse<ICapturePayload>) => {
|
||||
if (!this.isCapturePayload(statusResponse.payload)) {
|
||||
throw new Error("Invalid payload for infrastructure monitor");
|
||||
}
|
||||
const check = this.buildBaseCheck(statusResponse);
|
||||
check.system = statusResponse.payload.data;
|
||||
check.capture = statusResponse.payload.capture;
|
||||
return check;
|
||||
};
|
||||
|
||||
private buildPagespeedCheck = (statusResponse: StatusResponse<ILighthousePayload>) => {
|
||||
if (!this.isPagespeedPayload(statusResponse.payload)) {
|
||||
throw new Error("Invalid payload for pagespeed monitor");
|
||||
}
|
||||
const check = this.buildBaseCheck(statusResponse);
|
||||
const lighthouseResult = statusResponse?.payload?.lighthouseResult;
|
||||
check.lighthouse = {
|
||||
accessibility: lighthouseResult?.categories?.accessibility?.score || 0,
|
||||
bestPractices: lighthouseResult?.categories?.["best-practices"]?.score || 0,
|
||||
seo: lighthouseResult?.categories?.seo?.score || 0,
|
||||
performance: lighthouseResult?.categories?.performance?.score || 0,
|
||||
audits: {
|
||||
cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {},
|
||||
si: lighthouseResult?.audits?.["speed-index"] || {},
|
||||
fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {},
|
||||
lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {},
|
||||
tbt: lighthouseResult?.audits?.["total-blocking-time"] || {},
|
||||
},
|
||||
};
|
||||
return check;
|
||||
};
|
||||
|
||||
buildCheck = async (statusResponse: StatusResponse, type: MonitorType): Promise<ICheck> => {
|
||||
switch (type) {
|
||||
case "infrastructure":
|
||||
return this.buildInfrastructureCheck(statusResponse as StatusResponse<ICapturePayload>);
|
||||
|
||||
case "pagespeed":
|
||||
return this.buildPagespeedCheck(statusResponse as StatusResponse<ILighthousePayload>);
|
||||
case "http":
|
||||
case "https":
|
||||
return this.buildBaseCheck(statusResponse);
|
||||
|
||||
case "ping":
|
||||
return this.buildBaseCheck(statusResponse);
|
||||
default:
|
||||
throw new Error(`Unsupported monitor type: ${type}`);
|
||||
}
|
||||
};
|
||||
|
||||
cleanupOrphanedChecks = async () => {
|
||||
try {
|
||||
const monitorIds = await Monitor.find().distinct("_id");
|
||||
const result = await Check.deleteMany({
|
||||
monitorId: { $nin: monitorIds },
|
||||
});
|
||||
console.log(`Deleted ${result.deletedCount} orphaned Checks.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error cleaning up orphaned Checks:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default CheckService;
|
||||
@@ -1,464 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
import { IMonitor, Monitor, ITokenizedUser, MonitorStats, Check } from "../../../db/v1/models/index.js";
|
||||
import ApiError from "../../../utils/ApiError.js";
|
||||
import { IJobQueue } from "../infrastructure/JobQueue.js";
|
||||
import { MonitorWithChecksResponse } from "../../../types/monitor-response-with-checks.js";
|
||||
import { MonitorStatus, MonitorType } from "../../../db/v1/models/monitors/Monitor.js";
|
||||
export interface IMonitorService {
|
||||
create: (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => Promise<IMonitor>;
|
||||
getAll: () => Promise<IMonitor[]>;
|
||||
getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise<any[]>;
|
||||
get: (monitorId: string) => Promise<IMonitor>;
|
||||
getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise<MonitorWithChecksResponse>;
|
||||
toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise<IMonitor>;
|
||||
update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial<IMonitor>) => Promise<IMonitor>;
|
||||
delete: (monitorId: string) => Promise<boolean>;
|
||||
}
|
||||
|
||||
class MonitorService implements IMonitorService {
|
||||
private jobQueue: IJobQueue;
|
||||
constructor(jobQueue: IJobQueue) {
|
||||
this.jobQueue = jobQueue;
|
||||
}
|
||||
|
||||
create = async (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => {
|
||||
const monitor = await Monitor.create({
|
||||
...monitorData,
|
||||
createdBy: tokenizedUser.sub,
|
||||
updatedBy: tokenizedUser.sub,
|
||||
});
|
||||
await MonitorStats.create({
|
||||
monitorId: monitor._id,
|
||||
currentStreakStartedAt: Date.now(),
|
||||
});
|
||||
await this.jobQueue.addJob(monitor);
|
||||
return monitor;
|
||||
};
|
||||
|
||||
getAll = async () => {
|
||||
return Monitor.find();
|
||||
};
|
||||
|
||||
getAllEmbedChecks = async (page: number, limit: number, type: MonitorType[] = []) => {
|
||||
const skip = (page - 1) * limit;
|
||||
let find = {};
|
||||
if (type.length > 0) find = { type: { $in: type } };
|
||||
const monitors = await Monitor.find(find).skip(skip).limit(limit);
|
||||
return monitors;
|
||||
};
|
||||
|
||||
get = async (monitorId: string) => {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (!monitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
return monitor;
|
||||
};
|
||||
|
||||
private getStartDate(range: string): Date {
|
||||
const now = new Date();
|
||||
switch (range) {
|
||||
case "30m":
|
||||
return new Date(now.getTime() - 30 * 60 * 1000);
|
||||
case "24h":
|
||||
return new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
case "7d":
|
||||
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
case "30d":
|
||||
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
default:
|
||||
throw new ApiError("Invalid range parameter", 400);
|
||||
}
|
||||
}
|
||||
|
||||
private getDateFormat(range: string): string {
|
||||
switch (range) {
|
||||
case "30m":
|
||||
return "%Y-%m-%dT%H:%M:00Z";
|
||||
case "24h":
|
||||
case "7d":
|
||||
return "%Y-%m-%dT%H:00:00Z";
|
||||
case "30d":
|
||||
return "%Y-%m-%d";
|
||||
default:
|
||||
throw new ApiError("Invalid range parameter", 400);
|
||||
}
|
||||
}
|
||||
|
||||
private getBaseGroup = (dateFormat: string): Record<string, any> => {
|
||||
return {
|
||||
_id: { $dateToString: { format: dateFormat, date: "$createdAt" } },
|
||||
count: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
};
|
||||
};
|
||||
|
||||
private getBaseProjection = (): object => {
|
||||
return { status: 1, responseTime: 1, createdAt: 1 };
|
||||
};
|
||||
|
||||
private getPageSpeedGroup = (dateFormat: string): Record<string, any> => {
|
||||
return {
|
||||
_id: { $dateToString: { format: dateFormat, date: "$createdAt" } },
|
||||
count: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
accessibility: { $avg: "$lighthouse.accessibility" },
|
||||
bestPractices: { $avg: "$lighthouse.bestPractices" },
|
||||
seo: { $avg: "$lighthouse.seo" },
|
||||
performance: { $avg: "$lighthouse.performance" },
|
||||
cls: { $avg: "$lighthouse.audits.cls.score" },
|
||||
si: { $avg: "$lighthouse.audits.si.score" },
|
||||
fcp: { $avg: "$lighthouse.audits.fcp.score" },
|
||||
lcp: { $avg: "$lighthouse.audits.lcp.score" },
|
||||
tbt: { $avg: "$lighthouse.audits.tbt.score" },
|
||||
};
|
||||
};
|
||||
|
||||
private getPageSpeedProjection = (): object => {
|
||||
const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 };
|
||||
projectStage["lighthouse.accessibility"] = 1;
|
||||
projectStage["lighthouse.seo"] = 1;
|
||||
projectStage["lighthouse.bestPractices"] = 1;
|
||||
projectStage["lighthouse.performance"] = 1;
|
||||
projectStage["lighthouse.audits.cls.score"] = 1;
|
||||
projectStage["lighthouse.audits.si.score"] = 1;
|
||||
projectStage["lighthouse.audits.fcp.score"] = 1;
|
||||
projectStage["lighthouse.audits.lcp.score"] = 1;
|
||||
projectStage["lighthouse.audits.tbt.score"] = 1;
|
||||
return projectStage;
|
||||
};
|
||||
|
||||
private getInfraGroup = (dateFormat: string): Record<string, any> => {
|
||||
return {
|
||||
_id: { $dateToString: { format: dateFormat, date: "$createdAt" } },
|
||||
count: { $sum: 1 },
|
||||
avgResponseTime: { $avg: "$responseTime" },
|
||||
physicalCores: { $last: "$system.cpu.physical_core" },
|
||||
logicalCores: { $last: "$system.cpu.logical_core" },
|
||||
frequency: { $avg: "$system.cpu.frequency" },
|
||||
currentFrequency: { $last: "$system.cpu.current_frequency" },
|
||||
tempsArrays: { $push: "$system.cpu.temperature" },
|
||||
freePercent: { $avg: "$system.cpu.free_percent" },
|
||||
usedPercent: { $avg: "$system.cpu.usage_percent" },
|
||||
total_bytes: { $last: "$system.memory.total_bytes" },
|
||||
available_bytes: { $last: "$system.memory.available_bytes" },
|
||||
used_bytes: { $last: "$system.memory.used_bytes" },
|
||||
memory_usage_percent: { $avg: "$system.memory.usage_percent" },
|
||||
disksArray: { $push: "$system.disk" },
|
||||
os: { $last: "$system.host.os" },
|
||||
platform: { $last: "$system.host.platform" },
|
||||
kernel_version: { $last: "$system.host.kernel_version" },
|
||||
pretty_name: { $last: "$system.host.pretty_name" },
|
||||
netsArray: { $push: "$system.net" },
|
||||
};
|
||||
};
|
||||
|
||||
private getInfraProjection = (): object => {
|
||||
const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 };
|
||||
projectStage["system.cpu.physical_core"] = 1;
|
||||
projectStage["system.cpu.logical_core"] = 1;
|
||||
projectStage["system.cpu.frequency"] = 1;
|
||||
projectStage["system.cpu.current_frequency"] = 1;
|
||||
projectStage["system.cpu.temperature"] = 1;
|
||||
projectStage["system.cpu.free_percent"] = 1;
|
||||
projectStage["system.cpu.usage_percent"] = 1;
|
||||
projectStage["system.memory.total_bytes"] = 1;
|
||||
projectStage["system.memory.available_bytes"] = 1;
|
||||
projectStage["system.memory.used_bytes"] = 1;
|
||||
projectStage["system.memory.usage_percent"] = 1;
|
||||
projectStage["system.disk"] = 1;
|
||||
projectStage["system.host.os"] = 1;
|
||||
projectStage["system.host.platform"] = 1;
|
||||
projectStage["system.host.kernel_version"] = 1;
|
||||
projectStage["system.host.pretty_name"] = 1;
|
||||
projectStage["system.net"] = 1;
|
||||
return projectStage;
|
||||
};
|
||||
|
||||
private getFinalProjection = (type: string): object => {
|
||||
if (type === "pagespeed") {
|
||||
return {
|
||||
_id: 1,
|
||||
count: 1,
|
||||
avgResponseTime: 1,
|
||||
accessibility: "$accessibility",
|
||||
seo: "$seo",
|
||||
bestPractices: "$bestPractices",
|
||||
performance: "$performance",
|
||||
cls: "$cls",
|
||||
si: "$si",
|
||||
fcp: "$fcp",
|
||||
lcp: "$lcp",
|
||||
tbt: "$tbt",
|
||||
};
|
||||
}
|
||||
|
||||
if (type === "infrastructure") {
|
||||
return {
|
||||
_id: 1,
|
||||
count: 1,
|
||||
avgResponseTime: 1,
|
||||
cpu: {
|
||||
physicalCores: "$physicalCores",
|
||||
logicalCores: "$logicalCores",
|
||||
frequency: "$frequency",
|
||||
currentFrequency: "$currentFrequency",
|
||||
temperatures: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, { $size: { $arrayElemAt: ["$tempsArrays", 0] } }],
|
||||
},
|
||||
as: "idx",
|
||||
in: {
|
||||
$avg: {
|
||||
$map: {
|
||||
input: "$tempsArrays",
|
||||
as: "arr",
|
||||
in: { $arrayElemAt: ["$$arr", "$$idx"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
freePercent: "$freePercent",
|
||||
usedPercent: "$usedPercent",
|
||||
},
|
||||
memory: {
|
||||
total_bytes: "$total_bytes",
|
||||
available_bytes: "$available_bytes",
|
||||
used_bytes: "$used_bytes",
|
||||
usage_percent: "$memory_usage_percent",
|
||||
},
|
||||
disks: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, { $size: { $arrayElemAt: ["$disksArray", 0] } }],
|
||||
},
|
||||
as: "idx",
|
||||
in: {
|
||||
$let: {
|
||||
vars: {
|
||||
diskGroup: {
|
||||
$map: {
|
||||
input: "$disksArray",
|
||||
as: "diskArr",
|
||||
in: { $arrayElemAt: ["$$diskArr", "$$idx"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
in: {
|
||||
device: { $arrayElemAt: ["$$diskGroup.device", 0] },
|
||||
total_bytes: { $avg: "$$diskGroup.total_bytes" },
|
||||
free_bytes: { $avg: "$$diskGroup.free_bytes" },
|
||||
used_bytes: { $avg: "$$diskGroup.used_bytes" },
|
||||
usage_percent: { $avg: "$$diskGroup.usage_percent" },
|
||||
total_inodes: { $avg: "$$diskGroup.total_inodes" },
|
||||
free_inodes: { $avg: "$$diskGroup.free_inodes" },
|
||||
used_inodes: { $avg: "$$diskGroup.used_inodes" },
|
||||
inodes_usage_percent: {
|
||||
$avg: "$$diskGroup.inodes_usage_percent",
|
||||
},
|
||||
read_bytes: { $avg: "$$diskGroup.read_bytes" },
|
||||
write_bytes: { $avg: "$$diskGroup.write_bytes" },
|
||||
read_time: { $avg: "$$diskGroup.read_time" },
|
||||
write_time: { $avg: "$$diskGroup.write_time" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
host: {
|
||||
os: "$os",
|
||||
platform: "$platform",
|
||||
kernel_version: "$kernel_version",
|
||||
pretty_name: "$pretty_name",
|
||||
},
|
||||
net: {
|
||||
$map: {
|
||||
input: {
|
||||
$range: [0, { $size: { $arrayElemAt: ["$netsArray", 0] } }],
|
||||
},
|
||||
as: "idx",
|
||||
in: {
|
||||
$let: {
|
||||
vars: {
|
||||
netGroup: {
|
||||
$map: {
|
||||
input: "$netsArray",
|
||||
as: "netArr",
|
||||
in: { $arrayElemAt: ["$$netArr", "$$idx"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
in: {
|
||||
name: { $arrayElemAt: ["$$netGroup.name", 0] },
|
||||
bytes_sent: { $avg: "$$netGroup.bytes_sent" },
|
||||
bytes_recv: { $avg: "$$netGroup.bytes_recv" },
|
||||
packets_sent: { $avg: "$$netGroup.packets_sent" },
|
||||
packets_recv: { $avg: "$$netGroup.packets_recv" },
|
||||
err_in: { $avg: "$$netGroup.err_in" },
|
||||
err_out: { $avg: "$$netGroup.err_out" },
|
||||
drop_in: { $avg: "$$netGroup.drop_in" },
|
||||
drop_out: { $avg: "$$netGroup.drop_out" },
|
||||
fifo_in: { $avg: "$$netGroup.fifo_in" },
|
||||
fifo_out: { $avg: "$$netGroup.fifo_out" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
getEmbedChecks = async (monitorId: string, range: string, status: string | undefined): Promise<MonitorWithChecksResponse> => {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (!monitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
const startDate = this.getStartDate(range);
|
||||
const dateFormat = this.getDateFormat(range);
|
||||
|
||||
// Build match stage
|
||||
const matchStage: {
|
||||
monitorId: mongoose.Types.ObjectId;
|
||||
createdAt: { $gte: Date };
|
||||
status?: string;
|
||||
} = {
|
||||
monitorId: monitor._id,
|
||||
createdAt: { $gte: startDate },
|
||||
};
|
||||
|
||||
if (status) {
|
||||
matchStage.status = status;
|
||||
}
|
||||
|
||||
let groupClause;
|
||||
|
||||
if (monitor.type === "pagespeed") {
|
||||
groupClause = this.getPageSpeedGroup(dateFormat);
|
||||
} else if (monitor.type === "infrastructure") {
|
||||
groupClause = this.getInfraGroup(dateFormat);
|
||||
} else {
|
||||
groupClause = this.getBaseGroup(dateFormat);
|
||||
}
|
||||
|
||||
let projectStage;
|
||||
if (monitor.type === "pagespeed") {
|
||||
projectStage = this.getPageSpeedProjection();
|
||||
} else if (monitor.type === "infrastructure") {
|
||||
projectStage = this.getInfraProjection();
|
||||
} else {
|
||||
projectStage = this.getBaseProjection();
|
||||
}
|
||||
|
||||
let finalProjection = {};
|
||||
if (monitor.type === "pagespeed" || monitor.type === "infrastructure") {
|
||||
finalProjection = this.getFinalProjection(monitor.type);
|
||||
} else {
|
||||
finalProjection = { _id: 1, count: 1, avgResponseTime: 1 };
|
||||
}
|
||||
|
||||
const checks = await Check.aggregate([
|
||||
{
|
||||
$match: matchStage,
|
||||
},
|
||||
{ $sort: { createdAt: 1 } },
|
||||
{ $project: projectStage },
|
||||
{ $group: groupClause },
|
||||
{ $sort: { _id: -1 } },
|
||||
{
|
||||
$project: finalProjection,
|
||||
},
|
||||
]);
|
||||
|
||||
// Get monitor stats
|
||||
const monitorStats = await MonitorStats.findOne({
|
||||
monitorId: monitor._id,
|
||||
}).lean();
|
||||
|
||||
if (!monitorStats) {
|
||||
throw new ApiError("Monitor stats not found", 404);
|
||||
}
|
||||
|
||||
return {
|
||||
monitor: monitor.toObject(),
|
||||
checks,
|
||||
stats: monitorStats,
|
||||
};
|
||||
};
|
||||
|
||||
async toggleActive(id: string, tokenizedUser: ITokenizedUser) {
|
||||
const pendingStatus: MonitorStatus = "initializing";
|
||||
const updatedMonitor = await Monitor.findOneAndUpdate(
|
||||
{ _id: id },
|
||||
[
|
||||
{
|
||||
$set: {
|
||||
isActive: { $not: "$isActive" },
|
||||
status: pendingStatus,
|
||||
updatedBy: tokenizedUser.sub,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
},
|
||||
],
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
if (!updatedMonitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
|
||||
await this.jobQueue.updateJob(updatedMonitor);
|
||||
|
||||
if (updatedMonitor?.isActive) {
|
||||
await this.jobQueue.resumeJob(updatedMonitor);
|
||||
} else {
|
||||
await this.jobQueue.pauseJob(updatedMonitor);
|
||||
}
|
||||
return updatedMonitor;
|
||||
}
|
||||
|
||||
async update(tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial<IMonitor>) {
|
||||
const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "m", "notificationChannels"];
|
||||
const safeUpdate: Partial<IMonitor> = {};
|
||||
|
||||
for (const field of allowedFields) {
|
||||
if (updateData[field] !== undefined) {
|
||||
(safeUpdate as any)[field] = updateData[field];
|
||||
}
|
||||
}
|
||||
|
||||
const updatedMonitor = await Monitor.findByIdAndUpdate(
|
||||
monitorId,
|
||||
{
|
||||
$set: {
|
||||
...safeUpdate,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: tokenizedUser.sub,
|
||||
},
|
||||
},
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
|
||||
if (!updatedMonitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
await this.jobQueue.updateJob(updatedMonitor);
|
||||
return updatedMonitor;
|
||||
}
|
||||
|
||||
async delete(monitorId: string) {
|
||||
const monitor = await Monitor.findById(monitorId);
|
||||
if (!monitor) {
|
||||
throw new ApiError("Monitor not found", 404);
|
||||
}
|
||||
await monitor.deleteOne();
|
||||
await this.jobQueue.deleteJob(monitor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default MonitorService;
|
||||
Reference in new issue
Block a user