getUptimeDetails

This commit is contained in:
Alex Holliday
2026-01-13 23:59:09 +00:00
parent 77764c51ee
commit 76fa99f7bd
8 changed files with 175 additions and 48 deletions
@@ -39,19 +39,6 @@ class MonitorController {
}
}
getAllMonitors = async (req: Request, res: Response, next: NextFunction) => {
try {
const monitors = await this.monitorService.getAllMonitors();
return res.status(200).json({
success: true,
msg: "Retrieved all monitors successfully",
data: monitors,
});
} catch (error) {
next(error);
}
};
getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => {
try {
await getCertificateParamValidation.validateAsync(req.params);
-11
View File
@@ -194,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);
@@ -1,10 +1,17 @@
import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
export interface IChecksRepository {
// create
// single fetch
// collection fetch
findLatestChecksByMonitorIds(monitorIds: string[]): Promise<LatestChecksMap>;
// update
// delete
findDateRangeChecksByMonitor(
monitorId: string,
startDate: Date,
endDate: Date,
dateString: string
): Promise<{
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
uptimePercentage: number;
avgResponseTime: number;
}>;
}
@@ -209,6 +209,106 @@ class MongoChecksRepistory implements IChecksRepository {
return acc;
}, {});
};
findDateRangeChecksByMonitor = async (
monitorId: string,
startDate: Date,
endDate: Date,
dateString: string
): Promise<{
groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>;
groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>;
uptimePercentage: number;
avgResponseTime: number;
}> => {
const matchStage = {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
updatedAt: { $gte: startDate, $lte: endDate },
};
const [result] = await CheckModel.aggregate([
{ $match: matchStage },
{ $sort: { updatedAt: 1 } },
{
$facet: {
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 } },
],
groupedUpChecks: [
{ $match: { status: true } },
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
},
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { _id: 1 } },
],
groupedDownChecks: [
{ $match: { status: false } },
{
$group: {
_id: {
$dateToString: { format: dateString, date: "$createdAt" },
},
totalChecks: { $sum: 1 },
avgResponseTime: { $avg: "$responseTime" },
},
},
{ $sort: { _id: 1 } },
],
},
},
]).exec();
const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0;
const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0;
return {
groupedChecks: result?.groupedChecks ?? [],
groupedUpChecks: result?.groupedUpChecks ?? [],
groupedDownChecks: result?.groupedDownChecks ?? [],
uptimePercentage,
avgResponseTime,
};
};
}
export default MongoChecksRepistory;
@@ -15,7 +15,7 @@ export interface IMonitorsRepository {
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
createBulkMonitors(monitors: Monitor[]): Promise<Monitor[]>;
// single fetch
findById(monitorId: string): Promise<Monitor | null>;
findById(monitorId: string, teamId?: string): Promise<Monitor | null>;
// collection fetch
findAll(): Promise<Monitor[] | null>;
@@ -21,12 +21,16 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return this.mapDocuments(inserted);
};
findById = async (MonitorModelId: string): Promise<Monitor> => {
const monitor = await MonitorModel.findById(MonitorModelId);
findById = async (monitorId: string, teamId?: string): Promise<Monitor> => {
const match: { _id: string; teamId?: string } = { _id: monitorId };
if (teamId) {
match.teamId = teamId;
}
const monitor = await MonitorModel.findOne(match);
if (!monitor) {
if (monitor === null || monitor === undefined) {
throw new AppError({
message: `Monitor with ID ${MonitorModelId} not found.`,
message: `Monitor with ID ${monitorId} not found.`,
status: 404,
});
}
-1
View File
@@ -38,7 +38,6 @@ class MonitorRoutes {
});
// General monitor CRUD routes
this.router.get("/", this.monitorController.getAllMonitors);
this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor);
this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors);
+55 -14
View File
@@ -1,8 +1,8 @@
import { createMonitorsBodyValidation } from "@/validation/joi.js";
import { NormalizeData } from "@/utils/dataUtils.js";
import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js";
import { type Monitor } from "@/types/index.js";
import type { MonitorType } from "@/types/monitor.js";
import type { IMonitorsRepository } from "@/repositories/index.js";
import type { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
import fs from "fs";
import { fileURLToPath } from "url";
import path from "path";
@@ -10,6 +10,7 @@ import path from "path";
import { AppError } from "../infrastructure/errorService.js";
const SERVICE_NAME = "MonitorService";
type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
export interface IMonitorService {
readonly serviceName: string;
@@ -21,7 +22,6 @@ export interface IMonitorService {
addDemoMonitors(args: { userId: string; teamId: string }): Promise<any[]>;
// read
getAllMonitors(): Promise<any[]>;
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<any>;
getMonitorStatsById(args: {
teamId: string;
@@ -85,7 +85,7 @@ export class MonitorService implements IMonitorService {
private errorService: any;
private games: any;
private monitorsRepository: IMonitorsRepository;
private checksRepository: any;
private checksRepository: IChecksRepository;
private fs: any;
constructor({
@@ -109,7 +109,7 @@ export class MonitorService implements IMonitorService {
errorService: any;
games: any;
monitorsRepository: IMonitorsRepository;
checksRepository: any;
checksRepository: IChecksRepository;
}) {
this.db = db;
this.jobQueue = jobQueue;
@@ -127,6 +127,31 @@ export class MonitorService implements IMonitorService {
return MonitorService.SERVICE_NAME;
}
private getDateRange = (dateRange: DateRangeKey) => {
const startDates = {
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
all: new Date(0),
};
return {
start: startDates[dateRange],
end: new Date(),
};
};
private getDateFormat = (dateRange: DateRangeKey): string => {
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
week: "%Y-%m-%dT00:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
all: "%Y-%m-%dT00:00:00Z",
};
return formatLookup[dateRange];
};
verifyTeamAccess = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<void> => {
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor?.teamId?.equals(teamId)) {
@@ -225,11 +250,6 @@ export class MonitorService implements IMonitorService {
return demoMonitors;
};
getAllMonitors = async (): Promise<any[]> => {
const monitors = await this.db.monitorModule.getAllMonitors();
return monitors;
};
getUptimeDetailsById = async ({
teamId,
monitorId,
@@ -242,13 +262,34 @@ export class MonitorService implements IMonitorService {
normalize?: boolean;
}): Promise<any> => {
await this.verifyTeamAccess({ teamId, monitorId });
const data = await this.db.monitorModule.getUptimeDetailsById({
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
}
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
const { start, end } = this.getDateRange(rangeKey);
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(
monitorId: monitor.id,
startDate: start,
endDate: end,
dateString: this.getDateFormat(rangeKey),
);
const monitorStats = await this.db.monitorModule.getMonitorStatsById({
monitorId,
dateRange,
normalize,
});
return data;
return {
monitorData: {
monitor,
groupedChecks: NormalizeDataUptimeDetails(checksData.groupedChecks, 10, 100),
groupedUpChecks: NormalizeDataUptimeDetails(checksData.groupedUpChecks, 10, 100),
groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100),
groupedAvgResponseTime: checksData.avgResponseTime,
groupedUptimePercentage: checksData.uptimePercentage,
},
monitorStats,
};
};
getMonitorStatsById = async ({