CheckService

This commit is contained in:
Alex Holliday committed 2026-01-05 23:14:19 +00:00
1 parent 74f22a3e70
commit 81d6c4651f
7 files changed
+249 -125

No files matched your search

+2 -1
View File
@@ -38,13 +38,14 @@ import {
MongoChecksRepository,
MongoMonitorStatsRepository,
} from "@/repositories/index.js";
import type { CheckEntity } from "@/types/domain/index.js";
export const initServices = async () => {
const monitorRepository = new MongoMonitorRepository();
const checksRepository = new MongoChecksRepository();
const monitorStatsRepository = new MongoMonitorStatsRepository();
const checkService = new CheckService();
const checkService = new CheckService(checksRepository, monitorRepository);
const inviteService = new InviteService();
const maintenanceService = new MaintenanceService();
const monitorStatsService = new MonitorStatsService(
@@ -2,12 +2,37 @@ import type {
AggregateCheck,
CheckEntity,
Monitor,
MonitorStatus,
} from "@/types/domain/index.js";
import type { LatestChecksByMonitor } from "@/repositories/checks/MongoCheckRepository.js";
export interface IChecksRepository {
// Create
create(checkData: Partial<CheckEntity>): Promise<CheckEntity>;
// Single fetch
findById(checkId: string, teamId: string): Promise<CheckEntity | null>;
// Collection fetch
findByTeamIdAndStatus(
status: MonitorStatus,
teamId: string,
startDate: Date,
page: number,
rowsPerPage: number
): Promise<CheckEntity[]>;
findByMonitorIdAndStatus(
status: MonitorStatus,
teamId: string,
monitorId: string,
startDate: Date,
page: number,
rowsPerPage: number
): Promise<CheckEntity[]>;
findCountByMonitorId(monitorId: string): Promise<number>;
findPageByMonitorId(
monitorId: string,
page: number,
rowsPerPage: number
): Promise<CheckEntity[]>;
findLatestChecksByMonitorIds(
monitorIds: string[]
): Promise<LatestChecksByMonitor>;
@@ -22,6 +47,7 @@ export interface IChecksRepository {
): Promise<AggregateCheck[]>;
// Update
// Delete
deleteManyExcludedByMonitorIds(monitorIds: string[]): Promise<number>;
}
export default IChecksRepository;
@@ -1,5 +1,10 @@
import type { IChecksRepository } from "@/repositories/index.js";
import { CheckEntity, AggregateCheck, Monitor } from "@/types/domain/index.js";
import type {
CheckEntity,
AggregateCheck,
Monitor,
MonitorStatus,
} from "@/types/domain/index.js";
import type { ICheck } from "@/db/models/index.js";
import { Check } from "@/db/models/index.js";
import mongoose from "mongoose";
@@ -66,11 +71,11 @@ class MongoCheckRepository implements IChecksRepository {
};
if (doc.cpu || doc.memory || doc.disk || doc.host || doc.net) {
if (doc.cpu) agg.cpu = doc.cpu;
if (doc.memory) agg.memory = doc.memory;
if (Array.isArray(doc.disk)) agg.disk = doc.disk;
if (doc.host) agg.host = doc.host;
if (Array.isArray(doc.net)) agg.net = doc.net;
if (doc.cpu) (agg as any).cpu = doc.cpu;
if (doc.memory) (agg as any).memory = doc.memory;
if (Array.isArray(doc.disk)) (agg as any).disk = doc.disk;
if (doc.host) (agg as any).host = doc.host;
if (Array.isArray(doc.net)) (agg as any).net = doc.net;
}
return agg;
};
@@ -438,6 +443,111 @@ class MongoCheckRepository implements IChecksRepository {
return {};
};
create = async (checkData: Partial<CheckEntity>) => {
const check = new Check({ ...checkData });
check.metadata.monitorId = new mongoose.Types.ObjectId(checkData.monitorId);
check.metadata.teamId = new mongoose.Types.ObjectId(checkData.teamId);
check.metadata.type = checkData.type || "http";
const inserted = await check.save();
return this.toEntity(inserted);
};
findById = async (checkId: string, teamId: string) => {
const check = await Check.findOne({
_id: new mongoose.Types.ObjectId(checkId),
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
});
if (!check) {
return null;
}
return this.toEntity(check);
};
findCountByMonitorId = async (monitorId: string) => {
return await Check.countDocuments({
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
});
};
findByTeamIdAndStatus = async (
status: MonitorStatus,
teamId: string,
startDate: Date,
page: number,
rowsPerPage: number
) => {
const pageSizePlusOne = rowsPerPage + 1;
const checks = await Check.find({
status,
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
createdAt: { $gte: new Date(startDate), $lte: new Date() },
})
.sort({ createdAt: -1 })
.skip(page * rowsPerPage)
.limit(pageSizePlusOne)
.select({
_id: 1,
status: 1,
httpStatusCode: 1,
message: 1,
responseTime: 1,
createdAt: 1,
"metadata.monitorId": 1,
"metadata.teamId": 1,
"metadata.type": 1,
});
return checks.map(this.toEntity);
};
findByMonitorIdAndStatus = async (
status: MonitorStatus,
teamId: string,
monitorId: string,
startDate: Date,
page: number,
rowsPerPage: number
) => {
const pageSizePlusOne = rowsPerPage + 1;
const checks = await Check.find({
status,
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
createdAt: { $gte: startDate, $lte: new Date() },
})
.sort({ createdAt: -1 })
.skip(page * rowsPerPage)
.limit(pageSizePlusOne)
.select({
_id: 1,
status: 1,
httpStatusCode: 1,
message: 1,
responseTime: 1,
createdAt: 1,
"metadata.monitorId": 1,
"metadata.teamId": 1,
"metadata.type": 1,
});
return checks.map(this.toEntity);
};
findPageByMonitorId = async (
monitorId: string,
page: number,
rowsPerPage: number
) => {
const checks = await Check.find({
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
})
.sort({ createdAt: -1 })
.skip(page * rowsPerPage)
.limit(rowsPerPage);
return checks.map(this.toEntity);
};
findLatestChecksByMonitorIds = async (monitorIds: string[]) => {
const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
const checkMap = await Check.aggregate([
@@ -562,7 +672,6 @@ class MongoCheckRepository implements IChecksRepository {
.allowDiskUse(true)
.hint({ "metadata.monitorId": 1, createdAt: 1 });
console.log(checks);
return checks.map(this.toAggregateCheck);
};
@@ -633,6 +742,15 @@ class MongoCheckRepository implements IChecksRepository {
return checks.map(this.toAggregateCheck);
};
deleteManyExcludedByMonitorIds = async (monitorIds: string[]) => {
const result = await Check.deleteMany({
"metadata.monitorId": {
$nin: monitorIds.map((id) => new mongoose.Types.ObjectId(id)),
},
});
return result.deletedCount ?? 0;
};
}
export default MongoCheckRepository;
+68 -100
View File
@@ -1,29 +1,35 @@
import { ICheck, Check, Monitor, ISystemInfo } from "@/db/models/index.js";
import { ISystemInfo } from "@/types/domain/index.js";
import type { IDockerPayload } from "@/services/infrastructure/NetworkService.js";
import { MonitorStatus, MonitorType } from "@/types/domain/index.js";
import { StatusResponse } from "../infrastructure/NetworkService.js";
import { StatusResponse } from "@/services/infrastructure/NetworkService.js";
import type {
ICapturePayload,
ILighthousePayload,
} from "../infrastructure/NetworkService.js";
import mongoose from "mongoose";
} from "@/services/infrastructure/NetworkService.js";
import ApiError from "@/utils/ApiError.js";
import { getChildLogger } from "@/logger/Logger.js";
import { getStartDate } from "@/utils/TimeUtils.js";
import type { CheckEntity } from "@/types/domain/index.js";
import type {
IChecksRepository,
IMonitorRepository,
} from "@/repositories/index.js";
const SERVICE_NAME = "CheckService";
const logger = getChildLogger(SERVICE_NAME);
export interface ICheckService {
createCheck: (checkData: Partial<CheckEntity>) => Promise<CheckEntity>;
buildCheck: (
statusResponse: StatusResponse,
type: MonitorType
) => Promise<ICheck>;
) => Promise<Partial<CheckEntity>>;
getMonitorChecks: (
monitorId: string,
page: number,
rowsPerPage: number
) => Promise<{ checks: ICheck[]; count: number }>;
) => Promise<{ checks: CheckEntity[]; count: number }>;
getChecksByStatus: (
status: MonitorStatus,
teamId: string,
@@ -31,17 +37,27 @@ export interface ICheckService {
page: number,
rowsPerPage: number,
range: string
) => Promise<{ checks: ICheck[]; hasMore: boolean }>;
) => Promise<{ checks: CheckEntity[]; hasMore: boolean }>;
getCheckById: (checkId: string, teamId: string) => Promise<ICheck | null>;
getCheckById: (
checkId: string,
teamId: string
) => Promise<CheckEntity | null>;
cleanupOrphanedChecks: () => Promise<boolean>;
}
class CheckService implements ICheckService {
public SERVICE_NAME: string;
constructor() {
private checksRepository: IChecksRepository;
private monitorRepository: IMonitorRepository;
constructor(
checksRepository: IChecksRepository,
monitorRepository: IMonitorRepository
) {
this.SERVICE_NAME = SERVICE_NAME;
this.checksRepository = checksRepository;
this.monitorRepository = monitorRepository;
}
private isCapturePayload = (payload: any): payload is ICapturePayload => {
@@ -118,14 +134,10 @@ class CheckService implements ICheckService {
};
private buildBaseCheck = (statusResponse: StatusResponse) => {
const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId);
const teamId = new mongoose.Types.ObjectId(statusResponse.teamId);
const checkData: Partial<ICheck> = {
metadata: {
monitorId: monitorId,
teamId: teamId,
type: statusResponse?.type,
},
const check: Partial<CheckEntity> = {
monitorId: statusResponse.monitorId,
teamId: statusResponse.teamId,
type: statusResponse?.type,
status: statusResponse?.status,
httpStatusCode: statusResponse?.code,
message: statusResponse?.message,
@@ -133,7 +145,6 @@ class CheckService implements ICheckService {
timings: statusResponse?.timings,
};
const check: ICheck = new Check(checkData);
return check;
};
@@ -194,10 +205,15 @@ class CheckService implements ICheckService {
return check;
};
createCheck = async (checkData: Partial<CheckEntity>) => {
const check = await this.checksRepository.create(checkData);
return check;
};
buildCheck = async (
statusResponse: StatusResponse,
type: MonitorType
): Promise<ICheck> => {
): Promise<Partial<CheckEntity>> => {
switch (type) {
case "infrastructure":
return this.buildInfrastructureCheck(
@@ -227,11 +243,12 @@ class CheckService implements ICheckService {
cleanupOrphanedChecks = async () => {
try {
const monitorIds = await Monitor.find().distinct("_id");
const result = await Check.deleteMany({
"metadata.monitorId": { $nin: monitorIds },
});
logger.info(`Deleted ${result.deletedCount} orphaned Checks.`);
const monitorIds = (await this.monitorRepository.findAll()).map(
(m) => m.id
);
const deletedCount =
await this.checksRepository.deleteManyExcludedByMonitorIds(monitorIds);
logger.info(`Deleted ${deletedCount} orphaned Checks.`);
return true;
} catch (error) {
logger.error("Error cleaning up orphaned Checks:", error);
@@ -244,16 +261,13 @@ class CheckService implements ICheckService {
page: number,
rowsPerPage: number
) => {
const count = await Check.countDocuments({
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
});
const count = await this.checksRepository.findCountByMonitorId(monitorId);
const checks = await Check.find({
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
})
.sort({ createdAt: -1 })
.skip(page * rowsPerPage)
.limit(rowsPerPage);
const checks = await this.checksRepository.findPageByMonitorId(
monitorId,
page,
rowsPerPage
);
return { checks, count };
};
@@ -265,89 +279,43 @@ class CheckService implements ICheckService {
rowsPerPage: number,
range: string
) => {
let match;
const startDate = getStartDate(range);
let checks: CheckEntity[] = [];
if (monitorId) {
const authorized = await Monitor.exists({
_id: monitorId,
teamId,
});
const authorized = await this.monitorRepository.findById(
monitorId,
teamId
);
if (!authorized) {
throw new ApiError("Not authorized", 403);
}
match = {
checks = await this.checksRepository.findByMonitorIdAndStatus(
status,
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
createdAt: { $gte: startDate, $lte: new Date() },
};
teamId,
monitorId,
startDate,
page,
rowsPerPage
);
} else {
match = {
checks = await this.checksRepository.findByTeamIdAndStatus(
status,
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
createdAt: { $gte: startDate, $lte: new Date() },
};
teamId,
startDate,
page,
rowsPerPage
);
}
const pageSizePlusOne = rowsPerPage + 1;
const docs = await Check.find(match)
.sort({ createdAt: -1 })
.skip(page * rowsPerPage)
.limit(pageSizePlusOne)
.select({
_id: 1,
status: 1,
httpStatusCode: 1,
message: 1,
responseTime: 1,
createdAt: 1,
"metadata.monitorId": 1,
"metadata.teamId": 1,
"metadata.type": 1,
});
const hasMore = docs.length > rowsPerPage;
const limited = hasMore ? docs.slice(0, rowsPerPage) : docs;
const monitorIds = Array.from(
new Set(
limited
.map((d) => d?.metadata?.monitorId)
.filter((v): v is mongoose.Types.ObjectId => !!v)
.map((v) => v.toString())
)
);
let nameById = new Map<string, string>();
if (monitorIds.length > 0) {
const monitors = await Monitor.find({ _id: { $in: monitorIds } }).select({
_id: 1,
name: 1,
});
nameById = new Map(monitors.map((m) => [m._id.toString(), m.name]));
}
const checks = limited.map((doc) => {
const asObj = doc.toObject();
const idStr = asObj?.metadata?.monitorId?.toString?.() ?? "";
const name = idStr ? nameById.get(idStr) : undefined;
if (name) {
asObj.metadata = asObj.metadata || {};
(asObj.metadata as any).monitorId = { name } as any;
}
return asObj as ICheck;
});
const hasMore = checks.length > rowsPerPage;
return { checks, hasMore };
};
getCheckById = async (checkId: string, teamId: string) => {
return await Check.findOne({
_id: new mongoose.Types.ObjectId(checkId),
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
});
return await this.checksRepository.findById(checkId, teamId);
};
}
+16 -12
View File
@@ -3,7 +3,6 @@ import {
Incident,
IMonitor,
Monitor,
ICheck,
IUser,
} from "@/db/models/index.js";
import type { ResolutionType } from "@/db/models/index.js";
@@ -11,7 +10,10 @@ import mongoose from "mongoose";
import { getChildLogger } from "@/logger/Logger.js";
import { ThresholdEvaluationResult } from "@/services/infrastructure/StatusService.js";
import { getStartDate } from "@/utils/TimeUtils.js";
import type { Monitor as MonitorEntity } from "@/types/domain/monitor.js";
import type {
Monitor as MonitorEntity,
CheckEntity,
} from "@/types/domain/index.js";
type IncidentPopulated = Omit<IIncident, "monitorId" | "resolvedBy"> & {
monitorId: IMonitor;
@@ -21,11 +23,11 @@ type IncidentPopulated = Omit<IIncident, "monitorId" | "resolvedBy"> & {
export interface IIncidentService {
handleStatusChange: (
updatedMonitor: MonitorEntity,
lastCheck: ICheck
lastCheck: CheckEntity
) => Promise<IIncident | null>;
handleThresholdBreach: (
updatedMonitor: MonitorEntity,
lastCheck: ICheck,
lastCheck: CheckEntity,
evalResult: ThresholdEvaluationResult
) => Promise<IIncident | null>;
create: (
@@ -47,7 +49,7 @@ export interface IIncidentService {
teamId: string,
incidentId: string,
resolutionType: ResolutionType,
lastCheck?: ICheck,
lastCheck?: CheckEntity,
resolvedBy?: string,
resolutionNote?: string
) => Promise<IIncident | null>;
@@ -69,13 +71,13 @@ class IncidentService implements IIncidentService {
handleStatusChange = async (
updatedMonitor: MonitorEntity,
lastCheck: ICheck
lastCheck: CheckEntity
) => {
if (updatedMonitor.status === "down") {
const incident = await this.create(
new mongoose.Types.ObjectId(updatedMonitor.teamId),
new mongoose.Types.ObjectId(updatedMonitor.id),
lastCheck._id
new mongoose.Types.ObjectId(lastCheck.id)
);
return incident;
} else if (updatedMonitor.status === "up") {
@@ -102,7 +104,7 @@ class IncidentService implements IIncidentService {
handleThresholdBreach = async (
updatedMonitor: MonitorEntity,
lastCheck: ICheck,
lastCheck: CheckEntity,
evalResult: ThresholdEvaluationResult
) => {
const existing = await Incident.findOne({
@@ -116,7 +118,7 @@ class IncidentService implements IIncidentService {
const incident = await this.create(
new mongoose.Types.ObjectId(updatedMonitor.teamId),
new mongoose.Types.ObjectId(updatedMonitor.id),
lastCheck._id
new mongoose.Types.ObjectId(lastCheck.id)
);
if (evalResult.notes.length) {
await Incident.updateOne(
@@ -147,7 +149,7 @@ class IncidentService implements IIncidentService {
return existing;
} else if (existing) {
const resolved = await this.resolve(
updatedMonitor.teamId.toString(),
updatedMonitor.teamId,
existing._id.toString(),
"auto",
lastCheck
@@ -227,7 +229,7 @@ class IncidentService implements IIncidentService {
teamId: string,
incidentId: string,
resolutionType: ResolutionType,
lastCheck?: ICheck,
lastCheck?: CheckEntity,
resolvedBy?: string,
resolutionNote?: string
) => {
@@ -237,7 +239,9 @@ class IncidentService implements IIncidentService {
$set: {
resolved: true,
endedAt: new Date(),
endCheck: lastCheck ? lastCheck._id : undefined,
endCheck: lastCheck
? new mongoose.Types.ObjectId(lastCheck.id)
: undefined,
resolvedBy: resolvedBy ? resolvedBy : undefined,
resolutionType,
resolutionNote: resolutionNote ? resolutionNote : undefined,
@@ -69,8 +69,11 @@ class JobGenerator implements IJobGenerator {
const status = await this.networkService.requestStatus(monitor);
// Step 3. Build check
const check = await this.checkService.buildCheck(status, monitor.type);
await check.save();
const checkToInsert = await this.checkService.buildCheck(
status,
monitor.type
);
const check = await this.checkService.createCheck(checkToInsert);
// Step 4. Update monitor's status
const [updatedMonitor, statusChanged] =
@@ -4,7 +4,11 @@ import {
IMonitorRepository,
IMonitorStatsRepository,
} from "@/repositories/index.js";
import type { Monitor, MonitorStats } from "@/types/domain/index.js";
import type {
Monitor,
MonitorStats,
CheckEntity,
} from "@/types/domain/index.js";
import ApiError from "@/utils/ApiError.js";
const emptyMetrics = (): ThresholdEvaluationResult["metrics"] => ({
@@ -52,7 +56,7 @@ export interface IStatusService {
) => Promise<MonitorStats | null>;
evaluateThresholds: (
monitor: Monitor,
check: ICheck
check: CheckEntity
) => Promise<ThresholdEvaluationResult>;
}
@@ -211,7 +215,7 @@ class StatusService implements IStatusService {
evaluateThresholds = async (
monitor: Monitor,
check: ICheck
check: CheckEntity
): Promise<ThresholdEvaluationResult> => {
try {
if (monitor.type !== "infrastructure" || monitor.status !== "up")