Merge pull request #3130 from bluewave-labs/fix/monitor-service

Fix/monitor service
This commit is contained in:
Alexander Holliday
2026-01-15 11:40:33 -08:00
committed by GitHub
30 changed files with 404 additions and 327 deletions
@@ -66,7 +66,7 @@ const MonitorDetailsControlHeader = ({
gap={theme.spacing(2)}
>
<Tooltip
key={monitor?._id}
key={monitor?.id}
placement="bottom"
title={tooltipTitle}
>
@@ -78,7 +78,7 @@ const MonitorDetailsControlHeader = ({
startIcon={<EmailIcon />}
disabled={isTestNotificationsDisabled}
onClick={() => {
testAllNotifications({ monitorId: monitor?._id });
testAllNotifications({ monitorId: monitor?.id });
}}
sx={{
whiteSpace: "nowrap",
@@ -92,7 +92,7 @@ const MonitorDetailsControlHeader = ({
variant="contained"
color="secondary"
onClick={(e) => {
navigate(`/incidents/${monitor?._id}`);
navigate(`/incidents/${monitor?.id}`);
}}
>
{t("menu.incidents")}
@@ -107,7 +107,7 @@ const MonitorDetailsControlHeader = ({
}
onClick={() => {
pauseMonitor({
monitorId: monitor?._id,
monitorId: monitor?.id,
triggerUpdate,
});
}}
@@ -120,7 +120,7 @@ const MonitorDetailsControlHeader = ({
variant="contained"
color="secondary"
startIcon={<SettingsOutlinedIcon />}
onClick={() => navigate(`/${path}/configure/${monitor._id}`)}
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
>
Configure
</Button>
+5 -3
View File
@@ -50,6 +50,7 @@ export const useFetchMonitorsWithChecks = ({
const [isLoading, setIsLoading] = useState(false);
const [count, setCount] = useState(undefined);
const [monitors, setMonitors] = useState(undefined);
const [summary, setSummary] = useState(undefined);
const [networkError, setNetworkError] = useState(false);
const theme = useTheme();
@@ -68,10 +69,11 @@ export const useFetchMonitorsWithChecks = ({
order,
});
const { count, monitors } = res?.data?.data ?? {};
const { count, monitors, summary } = res?.data?.data ?? {};
const mappedMonitors = monitors.map((monitor) =>
getMonitorWithPercentage(monitor, theme)
);
setSummary(summary);
setMonitors(mappedMonitors);
setCount(count || 0);
} catch (error) {
@@ -97,7 +99,7 @@ export const useFetchMonitorsWithChecks = ({
types,
monitorUpdateTrigger,
]);
return [monitors, count, isLoading, networkError];
return [summary, monitors, count, isLoading, networkError];
};
export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => {
@@ -385,7 +387,7 @@ export const useUpdateMonitor = () => {
}),
};
await networkService.updateMonitor({
monitorId: monitor._id,
monitorId: monitor.id,
updatedFields,
});
@@ -71,7 +71,7 @@ const InfrastructureMonitors = () => {
const field = toFilterStatus !== undefined ? "status" : undefined;
const [monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({
const [summary, monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({
types: TYPES,
limit: 1,
page: page,
@@ -2,6 +2,7 @@ import { Grid, Grid2 } from "@mui/material";
import Card from "../Card/index.jsx";
const MonitorGrid = ({ shouldRender, monitors }) => {
console.log(monitors);
return (
<Grid
container
@@ -21,6 +21,7 @@ const PageSpeed = () => {
const isAdmin = useIsAdmin();
const [
summary,
monitorsWithChecks,
monitorsWithChecksCount,
monitorsWithChecksIsLoading,
+2 -2
View File
@@ -199,7 +199,7 @@ const UptimeCreate = ({ isClone = false }) => {
};
} else {
form = {
_id: monitor._id,
id: monitor.id,
url: monitor.url,
name: monitor.name || monitor.url.substring(0, 50),
statusWindowSize: monitor.statusWindowSize,
@@ -292,7 +292,7 @@ const UptimeCreate = ({ isClone = false }) => {
const handleRemove = async (event) => {
event.preventDefault();
const TEMP_MONITOR = { id: monitor._id };
const TEMP_MONITOR = { id: monitor.id };
await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/uptime" });
};
+14 -14
View File
@@ -27,8 +27,8 @@ import { useSelector, useDispatch } from "react-redux";
import { setRowsPerPage } from "../../../Features/UI/uiSlice.js";
import PropTypes from "prop-types";
import {
useFetchMonitorsWithSummary,
useFetchMonitorsWithChecks,
useFetchMonitorsByTeamId,
} from "@/Hooks/monitorHooks.js";
import { useTranslation } from "react-i18next";
@@ -103,12 +103,6 @@ const UptimeMonitors = () => {
setMonitorUpdateTrigger((prev) => !prev);
}, []);
const [monitors, monitorsSummary, monitorsWithSummaryIsLoading, networkError] =
useFetchMonitorsWithSummary({
types: TYPES,
monitorUpdateTrigger,
});
const handleReset = () => {
setSelectedState(undefined);
setSelectedTypes(undefined);
@@ -123,16 +117,17 @@ const UptimeMonitors = () => {
]);
const activeFilter = [...filterLookup].find(([key]) => key !== undefined);
const field = activeFilter?.[1] || sort?.field;
const field = activeFilter?.[1] || (search ? "name" : sort?.field);
const filter = activeFilter?.[0] || search;
const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES;
const [
summary,
monitorsWithChecks,
monitorsWithChecksCount,
monitorsWithChecksIsLoading,
monitorsWithChecksNetworkError,
networkError,
] = useFetchMonitorsWithChecks({
types: effectiveTypes,
limit: 25,
@@ -144,13 +139,18 @@ const UptimeMonitors = () => {
monitorUpdateTrigger,
});
const [monitors, listIsLoading, listNetworkError] = useFetchMonitorsByTeamId({
type: ["http", "ping", "docker", "port", "game"],
});
console.log(monitors);
useEffect(() => {
if (isSearching) {
setPage(undefined);
}
}, [isSearching]);
const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading;
const isLoading = monitorsWithChecksIsLoading;
return (
<>
@@ -174,14 +174,14 @@ const UptimeMonitors = () => {
/>
<Greeting type="uptime" />
<StatusBoxes
monitorsSummary={monitorsSummary}
shouldRender={!monitorsWithSummaryIsLoading}
monitorsSummary={summary}
shouldRender={!monitorsWithChecksIsLoading}
/>
<Stack direction={"row"}>
<MonitorCountHeader
isLoading={monitorsWithSummaryIsLoading}
monitorCount={monitorsSummary?.totalMonitors}
isLoading={monitorsWithChecksIsLoading}
monitorCount={summary?.totalMonitors}
/>
<Filter
selectedTypes={selectedTypes}
+1 -1
View File
@@ -112,7 +112,7 @@ const loginCredentials = joi.object({
});
const monitorValidation = joi.object({
_id: joi.string(),
id: joi.string(),
userId: joi.string(),
teamId: joi.string(),
statusWindowSize: joi.number().min(1).max(20).default(5).messages({
+3 -2
View File
@@ -70,7 +70,7 @@ import SettingsModule from "../db/modules/settingsModule.js";
import IncidentModule from "../db/modules/incidentModule.js";
// repositories
import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository } from "@/repositories/index.js";
import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository, MongoStatusPagesRepository } from "@/repositories/index.js";
export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => {
const serviceRegistry = new ServiceRegistry({ logger });
@@ -124,6 +124,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
const monitorsRepository = new MongoMonitorsRepository();
const checksRepository = new MongoChecksRepository();
const monitorStatsRepository = new MongoMonitorStatsRepository();
const statusPagesRepository = new MongoStatusPagesRepository();
const networkService = new NetworkService({
axios,
@@ -217,7 +218,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
errorService,
});
const monitorService = new MonitorService({
db,
jobQueue: superSimpleQueue,
stringService,
emailService,
@@ -228,6 +228,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
monitorsRepository,
checksRepository,
monitorStatsRepository,
statusPagesRepository,
});
const services = {
@@ -339,26 +339,6 @@ class MonitorController {
}
};
getMonitorsAndSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
const explain = optionalBoolean(req?.query?.explain, "explain");
const type = parseMonitorTypeFilter(req?.query?.type);
const teamId = requireTeamId(req?.user?.teamId);
const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain });
return res.status(200).json({
msg: "Monitors and summary retrieved successfully",
data: result,
});
} catch (error) {
next(error);
}
};
getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => {
try {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
@@ -394,21 +374,6 @@ class MonitorController {
next(error);
}
};
exportMonitorsToCSV = async (req: Request, res: Response, next: NextFunction) => {
try {
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const csv = await this.monitorService.exportMonitorsToCSV({ teamId });
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", "attachment; filename=monitors.csv");
return res.send(csv);
} catch (error) {
next(error);
}
};
exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => {
try {
-29
View File
@@ -171,35 +171,6 @@ const MonitorSchema = new Schema<MonitorDocument>(
}
);
MonitorSchema.pre("findOneAndDelete", async function (next) {
try {
const doc = await this.model.findOne(this.getFilter());
if (!doc) {
throw new Error("Monitor not found");
}
await Check.deleteMany({ monitorId: doc._id });
await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } });
await MonitorStats.deleteMany({ monitorId: doc?._id.toString() });
next();
} catch (error) {
next(error as Error);
}
});
MonitorSchema.pre("deleteMany", async function (next) {
const filter = this.getFilter();
const monitors = (await this.model.find(filter).select(["_id", "type"]).lean()) as { _id: Types.ObjectId }[];
for (const monitor of monitors) {
await Check.deleteMany({ monitorId: monitor._id });
await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } });
await MonitorStats.deleteMany({ monitorId: monitor._id.toString() });
}
next();
});
MonitorSchema.pre("save", function (next) {
if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) {
this.cpuAlertThreshold = this.alertThreshold;
-85
View File
@@ -1,85 +0,0 @@
import mongoose from "mongoose";
const StatusPageSchema = mongoose.Schema(
{
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
immutable: true,
required: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Team",
immutable: true,
required: true,
},
type: {
type: String,
required: true,
default: "uptime",
enum: ["uptime"],
},
companyName: {
type: String,
required: true,
default: "",
},
url: {
type: String,
unique: true,
required: true,
default: "",
},
timezone: {
type: String,
required: false,
},
color: {
type: String,
required: false,
default: "#4169E1",
},
monitors: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
required: true,
},
],
subMonitors: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
required: true,
},
],
logo: {
data: Buffer,
contentType: String,
},
isPublished: {
type: Boolean,
default: false,
},
showCharts: {
type: Boolean,
default: true,
},
showUptimePercentage: {
type: Boolean,
default: true,
},
showAdminLoginLink: {
type: Boolean,
default: false,
},
customCSS: {
type: String,
default: "",
},
},
{ timestamps: true }
);
export default mongoose.model("StatusPage", StatusPageSchema);
+115
View File
@@ -0,0 +1,115 @@
import { Schema, model, type Types } from "mongoose";
import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js";
import { StatusPageTypes } from "@/types/statusPage.js";
type StatusPageDocumentBase = Omit<
StatusPage,
"id" | "userId" | "teamId" | "monitors" | "subMonitors" | "originalMonitors" | "createdAt" | "updatedAt"
> & {
monitors: Types.ObjectId[];
subMonitors: Types.ObjectId[];
originalMonitors?: Types.ObjectId[];
logo?: StatusPageLogo | null;
};
interface StatusPageDocument extends StatusPageDocumentBase {
_id: Types.ObjectId;
userId: Types.ObjectId;
teamId: Types.ObjectId;
createdAt: Date;
updatedAt: Date;
}
const logoSchema = new Schema<StatusPageLogo & { data: Buffer }>(
{
data: { type: Buffer },
contentType: { type: String },
},
{ _id: false }
);
const StatusPageSchema = new Schema<StatusPageDocument>(
{
userId: {
type: Schema.Types.ObjectId,
ref: "User",
immutable: true,
required: true,
},
teamId: {
type: Schema.Types.ObjectId,
ref: "Team",
immutable: true,
required: true,
},
type: {
type: String,
required: true,
default: "uptime",
enum: StatusPageTypes,
},
companyName: {
type: String,
required: true,
default: "",
},
url: {
type: String,
unique: true,
required: true,
default: "",
},
timezone: {
type: String,
},
color: {
type: String,
default: "#4169E1",
},
monitors: [
{
type: Schema.Types.ObjectId,
ref: "Monitor",
required: true,
},
],
subMonitors: [
{
type: Schema.Types.ObjectId,
ref: "Monitor",
required: true,
},
],
logo: {
type: logoSchema,
default: null,
},
isPublished: {
type: Boolean,
default: false,
},
showCharts: {
type: Boolean,
default: true,
},
showUptimePercentage: {
type: Boolean,
default: true,
},
showAdminLoginLink: {
type: Boolean,
default: false,
},
customCSS: {
type: String,
default: "",
},
},
{ timestamps: true }
);
const StatusPageModel = model<StatusPageDocument>("StatusPage", StatusPageSchema);
export type { StatusPageDocument };
export { StatusPageModel };
export default StatusPageModel;
+3
View File
@@ -6,3 +6,6 @@ export { default as CheckModel } from "@/db/models/Check.js";
export * from "@/db/models/MonitorStats.js";
export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js";
export * from "@/db/models/StatusPage.js";
export { default as StatusPageModel } from "@/db/models/StatusPage.js";
+6 -7
View File
@@ -318,23 +318,22 @@ class MonitorModule {
}
};
getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => {
findMonitorsSummaryByTeamId = async ({ type, teamId, explain }) => {
try {
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const pipeline = buildMonitorsSummaryByTeamIdPipeline({ matchStage });
if (explain === true) {
return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats");
return this.Monitor.aggregate(pipeline).explain("executionStats");
}
const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }));
const { monitors, summary } = queryResult?.[0] ?? {};
return { monitors, summary };
const [summary] = await this.Monitor.aggregate(pipeline);
return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsAndSummaryByTeamId";
error.method = "findMonitorsSummaryByTeamId";
throw error;
}
};
@@ -287,6 +287,7 @@ class StatusPageModule {
deleteStatusPagesByMonitorId = async (monitorId) => {
try {
await this.StatusPage.deleteMany({ monitors: { $in: [monitorId] } });
await this.StatusPage.deleteMany({ subMonitors: { $in: [monitorId] } });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPageByMonitorId";
@@ -54,6 +54,9 @@ export interface UptimeChecksResult {
}
export interface IChecksRepository {
// create
// single fetch
// collection fetch
findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findDateRangeChecksByMonitor(
monitorId: string,
@@ -62,4 +65,7 @@ export interface IChecksRepository {
dateString: string,
options?: { type?: MonitorType }
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
// update
//delete
deleteByMonitorId(monitorId: string): Promise<number>;
}
@@ -23,7 +23,7 @@ import {
export type LatestChecksMap = Record<string, Check[]>;
class MongoChecksRepistory implements IChecksRepository {
class MongoChecksRepository implements IChecksRepository {
private toEntity = (doc: CheckDocument): Check => {
const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => {
if (!value) {
@@ -184,18 +184,13 @@ class MongoChecksRepistory implements IChecksRepository {
}
const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
const limitPerMonitor = options?.limitPerMonitor ?? 25;
const maxIntervalMs = Number(10 * 60 * 1000);
const bufferMs = Number(maxIntervalMs);
const lookbackMs = limitPerMonitor * maxIntervalMs + bufferMs;
const cutoffDate = new Date(Date.now() - lookbackMs);
const checkGroups = await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": { $in: mongoIds },
createdAt: { $gte: cutoffDate },
},
},
{ $sort: { "metadata.monitorId": 1, createdAt: -1 } },
{
$group: {
_id: "$metadata.monitorId",
@@ -390,6 +385,11 @@ class MongoChecksRepistory implements IChecksRepository {
checks: checks.map((doc) => this.toEntity(doc)),
};
};
deleteByMonitorId = async (monitorId: string): Promise<number> => {
const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId });
return result.deletedCount;
};
}
export default MongoChecksRepistory;
export default MongoChecksRepository;
+3
View File
@@ -6,3 +6,6 @@ export { default as MongoChecksRepository } from "@/repositories/checks/MongoChe
export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js";
export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js";
export * from "@/repositories/status-pages/IStatusPagesRepository.js";
export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js";
@@ -5,5 +5,6 @@ export interface IMonitorStatsRepository {
findByMonitorId(monitorId: string): Promise<MonitorStats>;
// update
// delete
deleteByMonitorId(monitorId: string): Promise<MonitorStats>;
// other
}
@@ -39,6 +39,14 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository {
}
return this.toEntity(monitorStats);
};
deleteByMonitorId = async (monitorId: string) => {
const deleted = await MonitorStatsModel.findOneAndDelete({ monitorId: new mongoose.Types.ObjectId(monitorId) });
if (!deleted) {
throw new AppError({ message: "Monitor stats not found", status: 404 });
}
return this.toEntity(deleted);
};
}
export default MongoMonitorStatsRepository;
@@ -10,6 +10,10 @@ export interface TeamQueryConfig {
order?: "asc" | "desc";
}
export interface SummaryConfig {
type?: MonitorType | MonitorType[];
}
export interface IMonitorsRepository {
// create
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
@@ -20,11 +24,18 @@ export interface IMonitorsRepository {
// collection fetch
findAll(): Promise<Monitor[] | null>;
findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null>;
// update
update(monitorId: string, updates: Partial<Monitor>): Promise<Monitor>;
updateById(monitorId: string, teamId: string, updates: Partial<Monitor>): Promise<Monitor>;
togglePauseById(monitorId: string, teamId: string): Promise<Monitor>;
// delete
deleteById(monitorId: string, teamId: string): Promise<Monitor>;
deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>;
// counts
findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise<number>;
// other
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<any>;
findGroupsByTeamId(teamId: string): Promise<string[]>;
}
@@ -1,8 +1,8 @@
import { MonitorModel } from "@/db/models/index.js";
import type { MonitorDocument } from "@/db/models/Monitor.js";
import type { Monitor, MonitorType } from "@/types/monitor.js";
import type { MonitorDocument } from "@/db/models/index.js";
import type { Monitor } from "@/types/index.js";
import mongoose, { type FilterQuery } from "mongoose";
import type { IMonitorsRepository, TeamQueryConfig } from "./IMonitorsRepository.js";
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
import { AppError } from "@/utils/AppError.js";
class MongoMonitorsRepository implements IMonitorsRepository {
@@ -44,7 +44,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
};
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null> => {
const { page = 0, rowsPerPage = 25, filter, field = "createdAt", order = "desc", type, limit } = config ?? {};
const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, limit } = config ?? {};
const query: Record<string, unknown> = {
teamId: new mongoose.Types.ObjectId(teamId),
@@ -75,9 +75,8 @@ class MongoMonitorsRepository implements IMonitorsRepository {
const sort = { [field]: order === "asc" ? 1 : -1 } as const;
const skip = Math.max(page, 0) * rowsPerPage;
const limitValue = limit ?? rowsPerPage;
const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limitValue).exec();
const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage);
return this.mapDocuments(documents);
};
@@ -97,9 +96,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return count;
};
update = async (monitorId: string, patch: Partial<Monitor>) => {
updateById = async (monitorId: string, teamId: string, patch: Partial<Monitor>) => {
const updatedMonitor = await MonitorModel.findOneAndUpdate(
{ _id: monitorId },
{ _id: monitorId, teamId },
{
$set: {
...patch,
@@ -113,6 +112,35 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return this.toEntity(updatedMonitor);
};
togglePauseById = async (monitorId: string, teamId: string) => {
const monitor = await MonitorModel.findOneAndUpdate(
{ _id: monitorId, teamId },
[
{
$set: {
isActive: { $not: "$isActive" },
status: "$$REMOVE",
},
},
],
{ new: true }
);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 });
}
return this.toEntity(monitor);
};
deleteById = async (monitorId: string, teamId: string) => {
const deletedMonitor = await MonitorModel.findOneAndDelete({ _id: monitorId, teamId });
if (!deletedMonitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 });
}
return this.toEntity(deletedMonitor);
};
deleteByTeamId = async (teamId: string) => {
const monitors = await MonitorModel.find({ teamId });
const { deletedCount } = await MonitorModel.deleteMany({ teamId });
@@ -120,6 +148,52 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return { monitors: this.mapDocuments(monitors), deletedCount };
};
findMonitorsSummaryByTeamId = async (
teamId: string,
config?: SummaryConfig
): Promise<{ totalMonitors: number; upMonitors: number; downMonitors: number; pausedMonitors: number }> => {
const match: FilterQuery<MonitorDocument> = { teamId: new mongoose.Types.ObjectId(teamId) };
if (config?.type !== undefined) {
match.type = Array.isArray(config.type) ? { $in: config.type } : config.type;
}
const pipeline = [
{ $match: match },
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{ $project: { _id: 0 } },
];
const [summary] = await MonitorModel.aggregate(pipeline);
return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 };
};
findGroupsByTeamId = async (teamId: string): Promise<string[]> => {
const groups = await MonitorModel.distinct("group", {
teamId: new mongoose.Types.ObjectId(teamId),
group: { $nin: [null, ""] },
});
return groups.sort();
};
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
if (!documents?.length) {
return [];
@@ -0,0 +1,11 @@
import type { StatusPage } from "@/types/statusPage.js";
export interface IStatusPagesRepository {
// create
// single fetch
// collection fetch
// update
// delete
// other
removeMonitorFromStatusPages(monitorId: string): Promise<number>;
}
@@ -0,0 +1,52 @@
import { IStatusPagesRepository } from "@/repositories/index.js";
import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js";
import type { StatusPage } from "@/types/statusPage.js";
import mongoose from "mongoose";
class MongoStatusPagesRepository implements IStatusPagesRepository {
private toEntity(doc: StatusPageDocument): StatusPage {
const toStringId = (value: unknown): string => {
if (value instanceof mongoose.Types.ObjectId) {
return value.toString();
}
return value?.toString() ?? "";
};
const toDateString = (value: Date | string): string => {
return value instanceof Date ? value.toISOString() : value;
};
const mapIdArray = (values?: mongoose.Types.ObjectId[]): string[] => {
return values?.map((value) => toStringId(value)) ?? [];
};
return {
id: toStringId(doc._id),
userId: toStringId(doc.userId),
teamId: toStringId(doc.teamId),
type: doc.type,
companyName: doc.companyName,
url: doc.url,
timezone: doc.timezone ?? undefined,
color: doc.color,
monitors: mapIdArray(doc.monitors),
subMonitors: mapIdArray(doc.subMonitors),
originalMonitors: doc.originalMonitors?.map((value) => toStringId(value)),
logo: doc.logo ?? undefined,
isPublished: doc.isPublished,
showCharts: doc.showCharts,
showUptimePercentage: doc.showUptimePercentage,
showAdminLoginLink: doc.showAdminLoginLink,
customCSS: doc.customCSS,
createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt),
};
}
removeMonitorFromStatusPages = async (monitorId: string): Promise<number> => {
const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } });
return res.modifiedCount;
};
}
export default MongoStatusPagesRepository;
-2
View File
@@ -19,7 +19,6 @@ class MonitorRoutes {
// Team routes
this.router.get("/team", this.monitorController.getMonitorsByTeamId);
this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId);
this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId);
this.router.get("/team/groups", this.monitorController.getGroupsByTeamId);
// Uptime routes
@@ -44,7 +43,6 @@ class MonitorRoutes {
// Other static routes
this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors);
this.router.get("/export", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToCSV);
this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON);
this.router.post("/bulk", isAllowed(["admin", "superadmin"]), upload.single("csvFile"), this.monitorController.createBulkMonitors);
this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail);
+31 -113
View File
@@ -2,7 +2,7 @@ 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 } from "@/repositories/index.js";
import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js";
import fs from "fs";
import { fileURLToPath } from "url";
import path from "path";
@@ -14,7 +14,6 @@ type DateRangeKey = "recent" | "day" | "week" | "month" | "all";
export interface IMonitorService {
readonly serviceName: string;
verifyTeamAccess(args: { teamId: string; monitorId: string }): Promise<void>;
// create
createMonitor(teamId: string, userId: string, body: Monitor): Promise<void>;
@@ -36,7 +35,6 @@ export interface IMonitorService {
field?: string;
order?: "asc" | "desc";
}): Promise<any>;
getMonitorsAndSummaryByTeamId(args: { teamId: string; type?: string | string[]; explain?: boolean }): Promise<any>;
getMonitorsWithChecksByTeamId(args: {
teamId: string;
limit?: number;
@@ -47,21 +45,20 @@ export interface IMonitorService {
field?: string;
order?: "asc" | "desc";
explain?: boolean;
}): Promise<{ count: number; monitors: any[] }>;
}): Promise<{ summary: any; count: number; monitors: any[] }>;
getAllGames(): any;
getGroupsByTeamId(args: { teamId: string }): Promise<any[]>;
getGroupsByTeamId(args: { teamId: string }): Promise<string[]>;
// update
editMonitor(args: { teamId: string; monitorId: string; body: any }): Promise<void>;
pauseMonitor(args: { teamId: string; monitorId: string }): Promise<any>;
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<any>;
deleteMonitor(args: { teamId: string; monitorId: string }): Promise<Monitor>;
deleteAllMonitors(args: { teamId: string }): Promise<number>;
// other
sendTestEmail(args: { to: string }): Promise<string>;
exportMonitorsToCSV(args: { teamId: string }): Promise<string>;
exportMonitorsToJSON(args: { teamId: string }): Promise<any[]>;
}
@@ -79,9 +76,9 @@ export class MonitorService implements IMonitorService {
private monitorsRepository: IMonitorsRepository;
private checksRepository: IChecksRepository;
private monitorStatsRepository: IMonitorStatsRepository;
private statusPagesRepository: IStatusPagesRepository;
constructor({
db,
jobQueue,
stringService,
emailService,
@@ -92,8 +89,8 @@ export class MonitorService implements IMonitorService {
monitorsRepository,
checksRepository,
monitorStatsRepository,
statusPagesRepository,
}: {
db: any;
jobQueue: any;
stringService: any;
emailService: any;
@@ -104,8 +101,8 @@ export class MonitorService implements IMonitorService {
monitorsRepository: IMonitorsRepository;
checksRepository: IChecksRepository;
monitorStatsRepository: IMonitorStatsRepository;
statusPagesRepository: IStatusPagesRepository;
}) {
this.db = db;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.emailService = emailService;
@@ -116,6 +113,7 @@ export class MonitorService implements IMonitorService {
this.monitorsRepository = monitorsRepository;
this.checksRepository = checksRepository;
this.monitorStatsRepository = monitorStatsRepository;
this.statusPagesRepository = statusPagesRepository;
}
get serviceName(): string {
@@ -147,13 +145,6 @@ export class MonitorService implements IMonitorService {
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)) {
throw this.errorService.createAuthorizationError();
}
};
createMonitor = async (teamId: string, userId: string, body: Monitor): Promise<void> => {
const monitor = await this.monitorsRepository.create(body, teamId, userId);
if (!monitor) {
@@ -256,8 +247,6 @@ export class MonitorService implements IMonitorService {
dateRange: string;
normalize?: boolean;
}): Promise<any> => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -293,7 +282,6 @@ export class MonitorService implements IMonitorService {
};
getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -325,7 +313,6 @@ export class MonitorService implements IMonitorService {
};
getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise<any> => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
if (!monitor) {
throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 });
@@ -349,7 +336,6 @@ export class MonitorService implements IMonitorService {
};
};
getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise<any> => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
return monitor;
};
@@ -362,23 +348,6 @@ export class MonitorService implements IMonitorService {
return monitors;
};
getMonitorsAndSummaryByTeamId = async ({
teamId,
type,
explain,
}: {
teamId: string;
type?: string | string[];
explain?: boolean;
}): Promise<any> => {
const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({
type,
explain,
teamId,
});
return result;
};
getMonitorsWithChecksByTeamId = async ({
teamId,
limit,
@@ -399,7 +368,8 @@ export class MonitorService implements IMonitorService {
field?: string;
order?: "asc" | "desc";
explain?: boolean;
}): Promise<{ count: number; monitors: any[] }> => {
}): 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,
@@ -433,36 +403,36 @@ export class MonitorService implements IMonitorService {
};
});
return { count, monitors: monitorsWithChecks };
return { summary: summary ?? null, count, monitors: monitorsWithChecks };
};
getAllGames = (): any => {
return this.games;
};
getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise<any[]> => {
const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId });
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: any }): Promise<void> => {
await this.verifyTeamAccess({ teamId, monitorId });
const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body });
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<any> => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.db.monitorModule.pauseMonitor({ monitorId });
monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor);
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<any> => {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId });
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);
await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id);
return monitor;
};
@@ -472,9 +442,9 @@ export class MonitorService implements IMonitorService {
monitors.map(async (monitor) => {
try {
await this.jobQueue.deleteJob(monitor);
await this.db.checkModule.deleteChecks(monitor.id);
await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor.id);
await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor.id);
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}`,
@@ -502,65 +472,13 @@ export class MonitorService implements IMonitorService {
return messageId;
};
exportMonitorsToCSV = async ({ teamId }: { teamId: string }): Promise<string> => {
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
if (!monitors || monitors.length === 0) {
throw this.errorService.createNotFoundError("No monitors to export");
}
const csvData = monitors?.filteredMonitors?.map((monitor: any) => ({
name: monitor.name,
description: monitor.description,
type: monitor.type,
url: monitor.url,
interval: monitor.interval,
port: monitor.port,
ignoreTlsErrors: monitor.ignoreTlsErrors,
isActive: monitor.isActive,
}));
const csv = this.papaparse.unparse(csvData);
return csv;
};
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<any[]> => {
const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId });
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
if (!monitors || monitors.length === 0) {
throw this.errorService.createNotFoundError("No monitors to export");
}
const json = monitors?.filteredMonitors
?.map((monitor: any) => {
const initialType = monitor.type;
let parsedType;
if (initialType === "hardware") {
parsedType = "infrastructure";
} else if (initialType === "http") {
if (monitor.url.startsWith("https://")) {
parsedType = "https";
} else {
parsedType = "http";
}
} else if (initialType === "pagespeed") {
parsedType = initialType;
} else {
// Skip unsupported types
return;
}
return {
name: monitor.name,
url: monitor.url,
type: parsedType,
interval: monitor.interval,
n: monitor.statusWindowSize,
secret: monitor.secret,
};
})
.filter(Boolean);
return json;
return monitors;
};
}
@@ -5,15 +5,6 @@ const SERVICE_NAME = "StatusService";
class StatusService {
static SERVICE_NAME = SERVICE_NAME;
/**
* @param {{
* db: any
* logger: any
* buffer: import("./bufferService.js").BufferService
* incidentService: import("../business/incidentService.js").IncidentService
* monitorsRepository: any
* }}
*/
constructor({ db, logger, buffer, incidentService, monitorsRepository }) {
this.db = db;
this.logger = logger;
@@ -204,7 +195,7 @@ class StatusService {
// Return early if not enough data points
if (monitor.statusWindow.length < monitor.statusWindowSize) {
const updated = await this.monitorsRepository.update(monitor.id, monitor);
const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor);
return {
monitor: updated,
statusChanged: false,
@@ -294,7 +285,7 @@ class StatusService {
}
monitor.status = newStatus;
const updated = await this.monitorsRepository.update(monitor.id, monitor);
const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor);
return {
monitor: updated,
+1
View File
@@ -1,3 +1,4 @@
export * from "@/types/check.js";
export * from "@/types/monitor.js";
export * from "@/types/monitorStats.js";
export * from "@/types/statusPage.js";
+29
View File
@@ -0,0 +1,29 @@
export const StatusPageTypes = ["uptime"] as const;
export type StatusPageType = (typeof StatusPageTypes)[number];
export interface StatusPageLogo {
data: Buffer;
contentType: string;
}
export interface StatusPage {
id: string;
userId: string;
teamId: string;
type: StatusPageType;
companyName: string;
url: string;
timezone?: string;
color: string;
monitors: string[];
subMonitors: string[];
originalMonitors?: string[];
logo?: StatusPageLogo | null;
isPublished: boolean;
showCharts: boolean;
showUptimePercentage: boolean;
showAdminLoginLink: boolean;
customCSS: string;
createdAt: string;
updatedAt: string;
}