mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-23 02:29:30 -05:00
486 lines
16 KiB
TypeScript
486 lines
16 KiB
TypeScript
import { createMonitorsBodyValidation } from "@/validation/joi.js";
|
|
import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js";
|
|
import { type Monitor } from "@/types/index.js";
|
|
import type { MonitorType } from "@/types/monitor.js";
|
|
import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js";
|
|
import fs from "fs";
|
|
import { fileURLToPath } from "url";
|
|
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;
|
|
|
|
// create
|
|
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
|
|
createBulkMonitors(fileData: string, userId: string, teamId: string): Promise<any>;
|
|
addDemoMonitors(args: { userId: string; teamId: string }): Promise<any[]>;
|
|
|
|
// read
|
|
getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise<any>;
|
|
getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
|
|
getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise<any>;
|
|
getMonitorById(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
|
getMonitorsByTeamId(args: {
|
|
teamId: string;
|
|
limit?: number;
|
|
type?: MonitorType | MonitorType[];
|
|
page?: number;
|
|
rowsPerPage?: number;
|
|
filter?: string;
|
|
field?: string;
|
|
order?: "asc" | "desc";
|
|
}): Promise<any>;
|
|
getMonitorsWithChecksByTeamId(args: {
|
|
teamId: string;
|
|
limit?: number;
|
|
type?: MonitorType | MonitorType[];
|
|
page?: number;
|
|
rowsPerPage?: number;
|
|
filter?: string;
|
|
field?: string;
|
|
order?: "asc" | "desc";
|
|
explain?: boolean;
|
|
}): Promise<{ summary: any; count: number; monitors: any[] }>;
|
|
getAllGames(): any;
|
|
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
|
|
|
|
// update
|
|
editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise<Monitor>;
|
|
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
|
|
|
// delete
|
|
deleteMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
|
|
deleteAllMonitors(args: { teamId: string }): Promise<number>;
|
|
|
|
// other
|
|
sendTestEmail(args: { to: string }): Promise<string>;
|
|
exportMonitorsToJSON(args: { teamId: string }): Promise<any[]>;
|
|
}
|
|
|
|
export class MonitorService implements IMonitorService {
|
|
static SERVICE_NAME = SERVICE_NAME;
|
|
|
|
private jobQueue: any;
|
|
private emailService: any;
|
|
private papaparse: any;
|
|
private logger: any;
|
|
private errorService: any;
|
|
private games: any;
|
|
private monitorsRepository: IMonitorsRepository;
|
|
private checksRepository: IChecksRepository;
|
|
private monitorStatsRepository: IMonitorStatsRepository;
|
|
private statusPagesRepository: IStatusPagesRepository;
|
|
|
|
constructor({
|
|
jobQueue,
|
|
emailService,
|
|
papaparse,
|
|
logger,
|
|
errorService,
|
|
games,
|
|
monitorsRepository,
|
|
checksRepository,
|
|
monitorStatsRepository,
|
|
statusPagesRepository,
|
|
}: {
|
|
jobQueue: any;
|
|
emailService: any;
|
|
papaparse: any;
|
|
logger: any;
|
|
errorService: any;
|
|
games: any;
|
|
monitorsRepository: IMonitorsRepository;
|
|
checksRepository: IChecksRepository;
|
|
monitorStatsRepository: IMonitorStatsRepository;
|
|
statusPagesRepository: IStatusPagesRepository;
|
|
}) {
|
|
this.jobQueue = jobQueue;
|
|
this.emailService = emailService;
|
|
this.papaparse = papaparse;
|
|
this.logger = logger;
|
|
this.errorService = errorService;
|
|
this.games = games;
|
|
this.monitorsRepository = monitorsRepository;
|
|
this.checksRepository = checksRepository;
|
|
this.monitorStatsRepository = monitorStatsRepository;
|
|
this.statusPagesRepository = statusPagesRepository;
|
|
}
|
|
|
|
get serviceName(): string {
|
|
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];
|
|
};
|
|
|
|
createMonitor = async (teamId: string, userId: string, body: Monitor): Promise<void> => {
|
|
const monitor = await this.monitorsRepository.create(body, teamId, userId);
|
|
if (!monitor) {
|
|
throw new AppError("Failed to create monitor", 500);
|
|
}
|
|
|
|
this.jobQueue.addJob(monitor.id, monitor);
|
|
};
|
|
|
|
createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise<any> => {
|
|
const { parse } = this.papaparse;
|
|
|
|
return new Promise<any>((resolve, reject) => {
|
|
parse(fileData, {
|
|
header: true,
|
|
skipEmptyLines: true,
|
|
transform: (value: string, header: string): string | number | undefined => {
|
|
if (value === "") return undefined; // Empty fields become undefined
|
|
|
|
// Handle 'port' and 'interval' fields, check if they're valid numbers
|
|
if (["port", "interval"].includes(header)) {
|
|
const num = parseInt(value, 10);
|
|
if (isNaN(num)) {
|
|
throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`);
|
|
}
|
|
return num;
|
|
}
|
|
|
|
return value;
|
|
},
|
|
complete: async ({ data, errors }: { data: any[]; errors: Error[] }): Promise<void> => {
|
|
try {
|
|
if (errors.length > 0) {
|
|
throw this.errorService.createServerError("Error parsing CSV");
|
|
}
|
|
|
|
if (!data || data.length === 0) {
|
|
throw this.errorService.createServerError("CSV file contains no data rows");
|
|
}
|
|
|
|
const enrichedData = data.map((monitor: Monitor) => ({
|
|
...monitor,
|
|
userId,
|
|
teamId,
|
|
description: monitor.description || monitor.name || monitor.url,
|
|
name: monitor.name || monitor.url,
|
|
type: monitor.type || "http",
|
|
}));
|
|
|
|
await createMonitorsBodyValidation.validateAsync(enrichedData);
|
|
|
|
const monitors = await this.monitorsRepository.createBulkMonitors(enrichedData);
|
|
|
|
await Promise.all(
|
|
monitors.map(async (monitor: Monitor) => {
|
|
this.jobQueue.addJob(monitor.id, monitor);
|
|
})
|
|
);
|
|
|
|
resolve(monitors);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
},
|
|
});
|
|
});
|
|
};
|
|
|
|
addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise<any[]> => {
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const demoMonitorsPath = path.resolve(__dirname, "../../utils/demoMonitors.json");
|
|
|
|
const demoData = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8"));
|
|
const monitors: Monitor[] = demoData.map((monitor: Monitor) => {
|
|
return {
|
|
userId,
|
|
teamId,
|
|
name: monitor.name,
|
|
description: monitor.name,
|
|
type: "http",
|
|
url: monitor.url,
|
|
interval: 60000,
|
|
};
|
|
});
|
|
const demoMonitors = await this.monitorsRepository.createBulkMonitors(monitors);
|
|
|
|
await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor)));
|
|
return demoMonitors;
|
|
};
|
|
|
|
getUptimeDetailsById = async ({
|
|
teamId,
|
|
monitorId,
|
|
dateRange,
|
|
normalize,
|
|
}: {
|
|
teamId: string;
|
|
monitorId: string;
|
|
dateRange: string;
|
|
normalize?: boolean;
|
|
}): Promise<any> => {
|
|
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.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
|
type: monitor.type,
|
|
});
|
|
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
|
|
|
|
if (
|
|
checksData.monitorType !== "http" &&
|
|
checksData.monitorType !== "ping" &&
|
|
checksData.monitorType !== "docker" &&
|
|
checksData.monitorType !== "port" &&
|
|
checksData.monitorType !== "game"
|
|
) {
|
|
throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 });
|
|
}
|
|
|
|
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,
|
|
};
|
|
};
|
|
|
|
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
|
|
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
|
if (!monitor) {
|
|
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
|
}
|
|
if (monitor.type !== "hardware") {
|
|
throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 });
|
|
}
|
|
|
|
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
|
const { start, end } = this.getDateRange(rangeKey);
|
|
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
|
type: monitor.type,
|
|
});
|
|
|
|
if (checksData.monitorType !== "hardware") {
|
|
throw new AppError({ message: "Unable to load hardware stats for this monitor", status: 500 });
|
|
}
|
|
|
|
const stats = {
|
|
aggregateData: checksData.aggregateData,
|
|
upChecks: checksData.upChecks,
|
|
checks: checksData.checks,
|
|
};
|
|
|
|
return {
|
|
...monitor,
|
|
stats,
|
|
};
|
|
};
|
|
|
|
getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
|
|
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
|
if (!monitor) {
|
|
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
|
|
}
|
|
if (monitor.type !== "pagespeed") {
|
|
throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 });
|
|
}
|
|
|
|
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
|
|
const { start, end } = this.getDateRange(rangeKey);
|
|
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), {
|
|
type: monitor.type,
|
|
});
|
|
|
|
if (checksData.monitorType !== "pagespeed") {
|
|
throw new AppError({ message: "Unable to load pagespeed stats for this monitor", status: 500 });
|
|
}
|
|
|
|
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
|
|
|
|
return {
|
|
monitor: {
|
|
...monitor,
|
|
checks: checksData.checks,
|
|
},
|
|
monitorStats,
|
|
};
|
|
};
|
|
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
|
|
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
|
|
return monitor;
|
|
};
|
|
|
|
getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise<any> => {
|
|
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
|
|
type,
|
|
filter,
|
|
});
|
|
return monitors;
|
|
};
|
|
|
|
getMonitorsWithChecksByTeamId = async ({
|
|
teamId,
|
|
limit,
|
|
type,
|
|
page,
|
|
rowsPerPage,
|
|
filter,
|
|
field,
|
|
order,
|
|
explain,
|
|
}: {
|
|
teamId: string;
|
|
limit?: number;
|
|
type?: MonitorType | MonitorType[];
|
|
page?: number;
|
|
rowsPerPage?: number;
|
|
filter?: string;
|
|
field?: string;
|
|
order?: "asc" | "desc";
|
|
explain?: boolean;
|
|
}): Promise<{ summary: any; count: number; monitors: any[] }> => {
|
|
const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId);
|
|
const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter });
|
|
const monitors = await this.monitorsRepository.findByTeamId(teamId, {
|
|
limit,
|
|
type,
|
|
page,
|
|
rowsPerPage,
|
|
filter,
|
|
field,
|
|
order,
|
|
});
|
|
|
|
const monitorsList = (monitors ?? []) as Monitor[];
|
|
const snapshotTypes: MonitorType[] = ["hardware"];
|
|
const requestedTypes = Array.isArray(type) ? type : type ? [type] : [];
|
|
const snapshotOnlyRequest =
|
|
requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType));
|
|
|
|
const limitPerMonitor = snapshotOnlyRequest ? 1 : 25;
|
|
const checksMap = await this.checksRepository.findLatestByMonitorIds(
|
|
monitorsList.map((monitor) => monitor.id),
|
|
{ limitPerMonitor }
|
|
);
|
|
|
|
const monitorsWithChecks = monitorsList.map((monitor: Monitor) => {
|
|
const rawChecks = checksMap[monitor.id] ?? [];
|
|
const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type);
|
|
const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100);
|
|
return {
|
|
...monitor,
|
|
checks,
|
|
};
|
|
});
|
|
|
|
return { summary: summary ?? null, count, monitors: monitorsWithChecks };
|
|
};
|
|
|
|
getAllGames = (): any => {
|
|
return this.games;
|
|
};
|
|
|
|
getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise<string[]> => {
|
|
const groups = await this.monitorsRepository.findGroupsByTeamId(teamId);
|
|
return groups;
|
|
};
|
|
|
|
editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => {
|
|
const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body);
|
|
await this.jobQueue.updateJob(editedMonitor);
|
|
return editedMonitor;
|
|
};
|
|
|
|
pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
|
|
const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId);
|
|
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor);
|
|
return monitor;
|
|
};
|
|
|
|
deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<Monitor> => {
|
|
const monitor = await this.monitorsRepository.deleteById(monitorId, teamId);
|
|
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
|
|
await this.checksRepository.deleteByMonitorId(monitor.id);
|
|
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
|
|
await this.jobQueue.deleteJob(monitor);
|
|
return monitor;
|
|
};
|
|
|
|
deleteAllMonitors = async ({ teamId }: { teamId: string }): Promise<number> => {
|
|
const { monitors, deletedCount } = await this.monitorsRepository.deleteByTeamId(teamId);
|
|
await Promise.all(
|
|
monitors.map(async (monitor) => {
|
|
try {
|
|
await this.jobQueue.deleteJob(monitor);
|
|
await this.checksRepository.deleteByMonitorId(monitor.id);
|
|
await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id);
|
|
await this.monitorStatsRepository.deleteByMonitorId(monitor.id);
|
|
} catch (error: any) {
|
|
this.logger.warn({
|
|
message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`,
|
|
service: SERVICE_NAME,
|
|
method: "deleteAllMonitors",
|
|
stack: error.stack,
|
|
});
|
|
}
|
|
})
|
|
);
|
|
return deletedCount;
|
|
};
|
|
|
|
sendTestEmail = async ({ to }: { to: string }): Promise<string> => {
|
|
const subject = "Test email from Checkmate";
|
|
const context = { testName: "Monitoring System" };
|
|
|
|
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
|
const messageId = await this.emailService.sendEmail(to, subject, html);
|
|
|
|
if (!messageId) {
|
|
throw this.errorService.createServerError("Failed to send test email.");
|
|
}
|
|
|
|
return messageId;
|
|
};
|
|
|
|
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
|
|
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
|
|
|
|
if (!monitors || monitors.length === 0) {
|
|
throw this.errorService.createNotFoundError("No monitors to export");
|
|
}
|
|
|
|
return monitors;
|
|
};
|
|
}
|