mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-05-20 08:28:48 -05:00
Merge pull request #3270 from bluewave-labs/feat/monitor-status
feat: monitor status
This commit is contained in:
@@ -105,6 +105,29 @@ export const PausedStatusBox = ({ n }: { n: number }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const MaintenanceStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.maintenance")}
|
||||
n={n}
|
||||
color={theme.palette.warning.light}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const InitializingStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.initializing")}
|
||||
n={n}
|
||||
color={theme.palette.warning.light}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TotalChecksBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
@@ -138,15 +161,3 @@ export const UpChecksBox = ({ n }: { n: number }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const InitializingStatusBox = ({ n }: { n: number }) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StatusBox
|
||||
label={t("pages.common.monitors.status.initializing")}
|
||||
n={n}
|
||||
color={theme.palette.warning.light}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,33 +11,24 @@ import { useTranslation } from "react-i18next";
|
||||
export const ValueTypes = ["positive", "negative", "neutral"] as const;
|
||||
export type ValueType = (typeof ValueTypes)[number];
|
||||
|
||||
export const StatusLabel = ({
|
||||
status,
|
||||
isActive,
|
||||
sx,
|
||||
}: {
|
||||
status: MonitorStatus;
|
||||
isActive?: boolean;
|
||||
sx?: SxProps;
|
||||
}) => {
|
||||
export const StatusLabel = ({ status, sx }: { status: MonitorStatus; sx?: SxProps }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const palette = getStatusPalette(status);
|
||||
|
||||
const determineStatus = (
|
||||
isActive: boolean | undefined,
|
||||
status: MonitorStatus
|
||||
): string => {
|
||||
if (isActive === false) {
|
||||
const determineStatus = (status: MonitorStatus): string => {
|
||||
if (status === "up") {
|
||||
return t("pages.common.monitors.status.up");
|
||||
} else if (status === "down") {
|
||||
return t("pages.common.monitors.status.down");
|
||||
} else if (status === "maintenance") {
|
||||
return t("pages.common.monitors.status.maintenance");
|
||||
} else if (status === "paused") {
|
||||
return t("pages.common.monitors.status.paused");
|
||||
} else if (status === "initializing") {
|
||||
return t("pages.common.monitors.status.initializing");
|
||||
}
|
||||
|
||||
if (status === true) {
|
||||
return t("pages.common.monitors.status.up");
|
||||
}
|
||||
if (status === false) {
|
||||
return t("pages.common.monitors.status.down");
|
||||
}
|
||||
return t("pages.common.monitors.status.initializing");
|
||||
};
|
||||
|
||||
@@ -64,9 +55,7 @@ export const StatusLabel = ({
|
||||
borderRadius="50%"
|
||||
marginRight="5px"
|
||||
/>
|
||||
<Typography textTransform={"capitalize"}>
|
||||
{determineStatus(isActive, status)}
|
||||
</Typography>
|
||||
<Typography textTransform={"capitalize"}>{determineStatus(status)}</Typography>
|
||||
</BaseBox>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
InitializingStatusBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import Stack from "@mui/material/Stack";
|
||||
|
||||
import type { MonitorsSummary } from "@/Types/Monitor";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
interface MonitorsSummaryProps {
|
||||
summary: MonitorsSummary | null;
|
||||
}
|
||||
|
||||
export const HeaderMonitorsSummary = ({ summary }: MonitorsSummaryProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
<InitializingStatusBox n={summary?.initializingMonitors || 0} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -11,3 +11,4 @@ export * from "./charts/PiePageSpeedLegend";
|
||||
export * from "./charts/HistogramPageSpeedDetails";
|
||||
export * from "./charts/HistogramPageSpeedDetailsTooltip";
|
||||
export * from "./charts/HistogramInfrastructure";
|
||||
export * from "./HeaderMonitorsSummary";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/Components/v2/design-elements";
|
||||
import Box from "@mui/material/Box";
|
||||
import type { Header } from "@/Components/v2/design-elements/Table";
|
||||
import type { Monitor, MonitorStatus } from "@/Types/Monitor";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatDateWithTz } from "@/Utils/TimeUtils";
|
||||
@@ -52,7 +52,7 @@ export const ChecksTable = ({
|
||||
id: "status",
|
||||
content: "Status",
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status as MonitorStatus} />;
|
||||
return <StatusLabel status={row.status === true ? "up" : "down"} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -195,12 +195,7 @@ export const InfraMonitorsTable = ({
|
||||
</Typography>
|
||||
),
|
||||
render: (row) => {
|
||||
return (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
isActive={row.isActive}
|
||||
/>
|
||||
);
|
||||
return <StatusLabel status={row.status} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import {
|
||||
MonitorBasePageWithStates,
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { MonitorBasePageWithStates } from "@/Components/v2/design-elements";
|
||||
import { HeaderCreate } from "@/Components/v2/common";
|
||||
import { ControlsFilter } from "@/Components/v2/monitors";
|
||||
import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/v2/monitors";
|
||||
import { TextField, Dialog } from "@/Components/v2/inputs";
|
||||
|
||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||
@@ -99,7 +94,7 @@ const InfrastructureMonitors = () => {
|
||||
{ refreshInterval: 5000, keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { summary, count } = monitorsWithChecksData ?? {};
|
||||
const { summary, count } = monitorsWithChecksData ?? { summary: null, count: 0 };
|
||||
const isLoading = monitorsWithChecksLoading;
|
||||
|
||||
// Check if any filters are active
|
||||
@@ -138,14 +133,7 @@ const InfrastructureMonitors = () => {
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
</Stack>
|
||||
<HeaderMonitorsSummary summary={summary} />
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
justifyContent={isSmall ? "flex-start" : "space-between"}
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
import {
|
||||
MonitorBasePageWithStates,
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
PageSpeedKeyPriorityFallback,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { Dialog } from "@/Components/v2/inputs";
|
||||
import { HeaderCreate } from "@/Components/v2/common";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { useIsAdmin } from "@/Hooks/useIsAdmin";
|
||||
import { useGet, useDelete } from "@/Hooks/UseApi";
|
||||
import type { MonitorsWithChecksResponse } from "@/Types/Monitor";
|
||||
import type { AppSettingsResponse } from "@/Types/Settings";
|
||||
import { HeaderMonitorsSummary } from "@/Components/v2/monitors";
|
||||
|
||||
const PageSpeedMonitorsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const { deleteFn, loading: isDeleting } = useDelete();
|
||||
|
||||
@@ -48,8 +43,8 @@ const PageSpeedMonitorsPage = () => {
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
|
||||
const monitors = monitorsData?.monitors;
|
||||
const monitorsCount = monitorsData?.count;
|
||||
const summary = monitorsData?.summary;
|
||||
const monitorsCount = monitorsData?.count ?? 0;
|
||||
const summary = monitorsData?.summary ?? null;
|
||||
|
||||
const isLoading = monitorsIsLoading || settingsIsLoading;
|
||||
|
||||
@@ -80,14 +75,7 @@ const PageSpeedMonitorsPage = () => {
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<Stack
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
</Stack>
|
||||
<HeaderMonitorsSummary summary={summary} />
|
||||
<PageSpeedMonitorsTable
|
||||
monitors={monitors || []}
|
||||
refetch={refetch}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { StatusLabel, BaseBox } from "@/Components/v2/design-elements";
|
||||
import { SwitchComponent } from "@/Components/v2/inputs";
|
||||
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { determineState } from "@/Utils/MonitorUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useState } from "react";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
@@ -54,7 +53,6 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||
)}
|
||||
|
||||
{monitors?.map((monitor) => {
|
||||
const status = determineState(monitor);
|
||||
return (
|
||||
<BaseBox
|
||||
key={monitor.id}
|
||||
@@ -87,10 +85,7 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<StatusLabel
|
||||
status={status === "up"}
|
||||
isActive={monitor.isActive}
|
||||
/>
|
||||
<StatusLabel status={monitor.status} />
|
||||
</Stack>
|
||||
{statusPage.showCharts !== false && (
|
||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
||||
|
||||
@@ -12,16 +12,16 @@ const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => {
|
||||
icon: <AlertTriangle size={24} />,
|
||||
};
|
||||
|
||||
if (monitors.every((monitor) => monitor.status === true)) {
|
||||
if (monitors.every((monitor) => monitor.status === "up")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allUp");
|
||||
monitorsStatus.color = theme.palette.success.main;
|
||||
monitorsStatus.icon = <CircleCheck size={24} />;
|
||||
return monitorsStatus;
|
||||
} else if (monitors.every((monitor) => monitor.status === false)) {
|
||||
} else if (monitors.every((monitor) => monitor.status === "down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allDown");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
return monitorsStatus;
|
||||
} else if (monitors.some((monitor) => monitor.status === false)) {
|
||||
} else if (monitors.some((monitor) => monitor.status === "down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.degraded");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
return monitorsStatus;
|
||||
|
||||
@@ -15,7 +15,7 @@ const getHeaders = (t: Function, uiTimezone: string) => {
|
||||
id: "status",
|
||||
content: t("common.table.headers.status"),
|
||||
render: (row) => {
|
||||
return <StatusLabel status={row.status} />;
|
||||
return <StatusLabel status={row.status === true ? "up" : "down"} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -186,12 +186,7 @@ export const MonitorTable = ({
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
return (
|
||||
<StatusLabel
|
||||
status={row.status}
|
||||
isActive={row.isActive}
|
||||
/>
|
||||
);
|
||||
return <StatusLabel status={row.status} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { ControlsFilter } from "@/Components/v2/monitors";
|
||||
import {
|
||||
MonitorBasePageWithStates,
|
||||
UpStatusBox,
|
||||
DownStatusBox,
|
||||
PausedStatusBox,
|
||||
} from "@/Components/v2/design-elements";
|
||||
import { ControlsFilter, HeaderMonitorsSummary } from "@/Components/v2/monitors";
|
||||
import { MonitorBasePageWithStates } from "@/Components/v2/design-elements";
|
||||
import { TextField, Dialog } from "@/Components/v2/inputs";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable";
|
||||
@@ -99,7 +94,11 @@ const UptimeMonitorsPage = () => {
|
||||
{ refreshInterval: 5000, keepPreviousData: true }
|
||||
);
|
||||
|
||||
const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {};
|
||||
const {
|
||||
monitors: monitorsWithChecks,
|
||||
summary,
|
||||
count,
|
||||
} = monitorsWithChecksData ?? { monitors: null, summary: null, count: 0 };
|
||||
|
||||
// Delete hook
|
||||
const { deleteFn, loading: isDeleting } = useDelete();
|
||||
@@ -150,14 +149,8 @@ const UptimeMonitorsPage = () => {
|
||||
isLoading={isLoading}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<UpStatusBox n={summary?.upMonitors || 0} />
|
||||
<DownStatusBox n={summary?.downMonitors || 0} />
|
||||
<PausedStatusBox n={summary?.pausedMonitors || 0} />
|
||||
</Stack>
|
||||
|
||||
<HeaderMonitorsSummary summary={summary} />
|
||||
|
||||
<Stack
|
||||
direction={isSmall ? "column" : "row"}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { GroupedCheck, CheckSnapshot } from "@/Types/Check";
|
||||
export type MonitorStatus = boolean | undefined;
|
||||
|
||||
export const MonitorTypes = [
|
||||
"http",
|
||||
@@ -13,6 +12,15 @@ export const MonitorTypes = [
|
||||
] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const MonitorStatuses = [
|
||||
"up",
|
||||
"down",
|
||||
"paused",
|
||||
"initializing",
|
||||
"maintenance",
|
||||
] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export interface MonitorThresholds {
|
||||
usage_cpu?: number;
|
||||
usage_memory?: number;
|
||||
@@ -28,7 +36,7 @@ export interface Monitor {
|
||||
teamId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
status?: boolean;
|
||||
status: MonitorStatus;
|
||||
statusWindow: boolean[];
|
||||
statusWindowSize: number;
|
||||
statusWindowThreshold: number;
|
||||
@@ -66,6 +74,8 @@ export interface MonitorsSummary {
|
||||
upMonitors: number;
|
||||
downMonitors: number;
|
||||
pausedMonitors: number;
|
||||
initializingMonitors: number;
|
||||
maintenanceMonitors: number;
|
||||
}
|
||||
|
||||
export interface MonitorsWithChecksResponse {
|
||||
|
||||
@@ -2,13 +2,6 @@ import type { Monitor, MonitorStatus, MonitorType } from "@/Types/Monitor";
|
||||
import type { PaletteKey } from "@/Utils/Theme/v2Theme";
|
||||
import type { ValueType } from "@/Components/v2/design-elements/StatusLabel";
|
||||
|
||||
export const determineState = (monitor: Monitor) => {
|
||||
if (typeof monitor === "undefined") return "pending";
|
||||
if (monitor?.isActive === false) return "paused";
|
||||
if (monitor?.status === undefined) return "pending";
|
||||
return monitor?.status == true ? "up" : "down";
|
||||
};
|
||||
|
||||
export const getMonitorPath = (type: MonitorType): string => {
|
||||
const pathMap: Record<MonitorType, string> = {
|
||||
http: "uptime",
|
||||
@@ -24,10 +17,10 @@ export const getMonitorPath = (type: MonitorType): string => {
|
||||
};
|
||||
|
||||
export const getStatusPalette = (status: MonitorStatus): PaletteKey => {
|
||||
if (status === true) {
|
||||
if (status === "up") {
|
||||
return "success";
|
||||
}
|
||||
if (status === false) {
|
||||
if (status === "down") {
|
||||
return "error";
|
||||
}
|
||||
return "warning";
|
||||
@@ -43,11 +36,11 @@ export const getValuePalette = (value: ValueType): PaletteKey => {
|
||||
};
|
||||
|
||||
export const getStatusColor = (status: MonitorStatus, theme: any): string => {
|
||||
if (status === true) {
|
||||
if (status === "up") {
|
||||
return theme.palette.success.light;
|
||||
}
|
||||
|
||||
if (status === false) {
|
||||
if (status === "down") {
|
||||
return theme.palette.error.light;
|
||||
}
|
||||
|
||||
@@ -101,9 +94,9 @@ export const getStatusPageHeaderConfig = (
|
||||
return { paletteKey: "error", message: "No monitors available" };
|
||||
}
|
||||
|
||||
const allUp = monitors.every((monitor) => monitor.status === true);
|
||||
const anyDown = monitors.some((monitor) => monitor.status === false);
|
||||
const allDown = monitors.every((monitor) => monitor.status === false);
|
||||
const allUp = monitors.every((monitor) => monitor.status === "up");
|
||||
const anyDown = monitors.some((monitor) => monitor.status === "down");
|
||||
const allDown = monitors.every((monitor) => monitor.status === "down");
|
||||
|
||||
if (allUp)
|
||||
return {
|
||||
|
||||
@@ -775,6 +775,7 @@
|
||||
"status": {
|
||||
"down": "down",
|
||||
"initializing": "initializing",
|
||||
"maintenance": "maintenance",
|
||||
"paused": "paused",
|
||||
"total": "total",
|
||||
"up": "up"
|
||||
|
||||
@@ -206,6 +206,7 @@ export const initializeServices = async ({
|
||||
buffer: bufferService,
|
||||
incidentService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
});
|
||||
|
||||
const superSimpleQueue = await SuperSimpleQueue.create({
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { Schema, model, Types, type UpdateQuery } from "mongoose";
|
||||
import type { Monitor, MonitorMatchMethod, MonitorThresholds, CheckSnapshot } from "@/types/monitor.js";
|
||||
import { MonitorTypes } from "@/types/monitor.js";
|
||||
import Check from "./Check.js";
|
||||
import MonitorStats from "./MonitorStats.js";
|
||||
import StatusPage from "./StatusPage.js";
|
||||
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
|
||||
|
||||
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
||||
|
||||
@@ -68,8 +65,9 @@ const MonitorSchema = new Schema<MonitorDocument>(
|
||||
type: String,
|
||||
},
|
||||
status: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
type: String,
|
||||
enum: MonitorStatuses,
|
||||
default: "initializing",
|
||||
},
|
||||
statusWindow: {
|
||||
type: [Boolean],
|
||||
|
||||
@@ -55,7 +55,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
query.isActive = filter === "true";
|
||||
break;
|
||||
case "status":
|
||||
query.status = filter === "true";
|
||||
query.status = filter;
|
||||
break;
|
||||
case "type":
|
||||
query.type = filter;
|
||||
@@ -181,7 +181,13 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
{
|
||||
$set: {
|
||||
isActive: { $not: "$isActive" },
|
||||
status: "$$REMOVE",
|
||||
status: {
|
||||
$cond: {
|
||||
if: { $eq: ["$status", "paused"] },
|
||||
then: "initializing",
|
||||
else: "paused",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -223,17 +229,27 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
totalMonitors: { $sum: 1 },
|
||||
upMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", true] }, 1, 0],
|
||||
$cond: [{ $eq: ["$status", "up"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
downMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", false] }, 1, 0],
|
||||
$cond: [{ $eq: ["$status", "down"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
pausedMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
|
||||
$cond: [{ $eq: ["$status", "paused"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
initializingMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", "initializing"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
maintenanceMonitors: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ["$status", "maintenance"] }, 1, 0],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -242,7 +258,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
];
|
||||
|
||||
const [summary] = await MonitorModel.aggregate(pipeline);
|
||||
return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 };
|
||||
return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0, initializingMonitors: 0, maintenanceMonitors: 0 };
|
||||
};
|
||||
|
||||
findGroupsByTeamId = async (teamId: string): Promise<string[]> => {
|
||||
@@ -284,7 +300,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
teamId: toStringId(doc.teamId),
|
||||
name: doc.name,
|
||||
description: doc.description ?? undefined,
|
||||
status: doc.status ?? undefined,
|
||||
status: doc.status ?? "initializing",
|
||||
statusWindow: doc.statusWindow ?? [],
|
||||
statusWindowSize: doc.statusWindowSize,
|
||||
statusWindowThreshold: doc.statusWindowThreshold,
|
||||
@@ -331,45 +347,13 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
|
||||
const notificationIds = (doc.notifications ?? []).map((notification: unknown) => toStringId(notification));
|
||||
|
||||
const checks: Check[] = (doc.checks ?? []).map((check: any) => ({
|
||||
id: toStringId(check._id),
|
||||
metadata: {
|
||||
monitorId: toStringId(check.metadata?.monitorId),
|
||||
teamId: toStringId(check.metadata?.teamId),
|
||||
type: check.metadata?.type,
|
||||
},
|
||||
status: check.status,
|
||||
responseTime: check.responseTime,
|
||||
timings: check.timings,
|
||||
statusCode: check.statusCode,
|
||||
message: check.message,
|
||||
ack: check.ack,
|
||||
ackAt: check.ackAt ?? null,
|
||||
expiry: toDateString(check.expiry),
|
||||
cpu: check.cpu,
|
||||
memory: check.memory,
|
||||
disk: check.disk,
|
||||
host: check.host,
|
||||
errors: check.errors,
|
||||
capture: check.capture,
|
||||
net: check.net,
|
||||
accessibility: check.accessibility,
|
||||
bestPractices: check.bestPractices,
|
||||
seo: check.seo,
|
||||
performance: check.performance,
|
||||
audits: check.audits,
|
||||
__v: check.__v,
|
||||
createdAt: toDateString(check.createdAt),
|
||||
updatedAt: toDateString(check.updatedAt),
|
||||
}));
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
userId: toStringId(doc.userId),
|
||||
teamId: toStringId(doc.teamId),
|
||||
name: doc.name,
|
||||
description: doc.description ?? undefined,
|
||||
status: doc.status ?? undefined,
|
||||
status: doc.status ?? "initializing",
|
||||
statusWindow: doc.statusWindow ?? [],
|
||||
statusWindowSize: doc.statusWindowSize,
|
||||
statusWindowThreshold: doc.statusWindowThreshold,
|
||||
|
||||
@@ -46,7 +46,7 @@ class IncidentService {
|
||||
handleIncident = async (monitor: Monitor, code: number): Promise<Incident | null> => {
|
||||
const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId);
|
||||
// Monitor is down, create an incident
|
||||
if (monitor.status === false) {
|
||||
if (monitor.status === "down") {
|
||||
if (activeIncident) {
|
||||
return activeIncident;
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Monitor } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
|
||||
import IncidentService from "@/service/business/incidentService.js";
|
||||
import { IMaintenanceWindowsRepository } from "@/repositories/index.js";
|
||||
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
|
||||
|
||||
class SuperSimpleQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
@@ -16,6 +16,7 @@ class SuperSimpleQueueHelper {
|
||||
private buffer: any;
|
||||
private incidentService: IncidentService;
|
||||
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
|
||||
constructor({
|
||||
logger,
|
||||
@@ -26,6 +27,7 @@ class SuperSimpleQueueHelper {
|
||||
buffer,
|
||||
incidentService,
|
||||
maintenanceWindowsRepository,
|
||||
monitorsRepository,
|
||||
}: {
|
||||
logger: any;
|
||||
networkService: INetworkService;
|
||||
@@ -35,6 +37,7 @@ class SuperSimpleQueueHelper {
|
||||
buffer: any;
|
||||
incidentService: IncidentService;
|
||||
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
}) {
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
@@ -44,6 +47,7 @@ class SuperSimpleQueueHelper {
|
||||
this.notificationsService = notificationsService;
|
||||
this.incidentService = incidentService;
|
||||
this.maintenanceWindowsRepository = maintenanceWindowsRepository;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -59,7 +63,8 @@ class SuperSimpleQueueHelper {
|
||||
throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" });
|
||||
}
|
||||
|
||||
// Step 1. Check for maintenacne window, if found, skip the check
|
||||
// Step 1. Check for maintenance window, if found, skip the check
|
||||
|
||||
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId);
|
||||
if (maintenanceWindowActive) {
|
||||
this.logger.debug({
|
||||
@@ -67,6 +72,9 @@ class SuperSimpleQueueHelper {
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
});
|
||||
if (monitor.status !== "maintenance") {
|
||||
await this.monitorsRepository.updateById(monitorId, teamId, { status: "maintenance" });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ export const buildHardwareEmail = async (emailService: any, monitor: Monitor, al
|
||||
};
|
||||
|
||||
export const buildEmail = async (emailService: any, monitor: Monitor): Promise<string> => {
|
||||
const template = monitor.status === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
const template = monitor.status === "up" ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url };
|
||||
const html = await emailService.buildEmail(template, context);
|
||||
return html;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js";
|
||||
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification, MonitorStatus } from "@/types/index.js";
|
||||
import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js";
|
||||
import { INotificationProvider } from "./notificationProviders/INotificationProvider.js";
|
||||
|
||||
export interface INotificationsService {
|
||||
createNotification: (notificationData: Partial<Notification>) => Promise<Notification>;
|
||||
findById: (id: string, teamId: string) => Promise<Notification>;
|
||||
@@ -11,7 +12,7 @@ export interface INotificationsService {
|
||||
handleNotifications: (
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
prevStatus: boolean | undefined,
|
||||
prevStatus: MonitorStatus,
|
||||
statusChanged: boolean
|
||||
) => Promise<boolean>;
|
||||
|
||||
@@ -228,12 +229,7 @@ export class NotificationsService implements INotificationsService {
|
||||
return await this.emailProvider.sendAlert(notification, syntheticMonitor, baseStatus);
|
||||
};
|
||||
|
||||
handleNotifications = async (
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
prevStatus: boolean | undefined,
|
||||
statusChanged: boolean
|
||||
) => {
|
||||
handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, prevStatus: MonitorStatus, statusChanged: boolean) => {
|
||||
const { type } = monitor;
|
||||
const payload = monitorStatusResponse.payload as HardwareStatusPayload;
|
||||
// If this is a non-hardeware type monitor and status did not change, we're done
|
||||
|
||||
@@ -2,7 +2,6 @@ import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import MonitorStats from "../../db/models/MonitorStats.js";
|
||||
import { CheckModel } from "@/db/models/index.js";
|
||||
import type {
|
||||
CheckErrorInfo,
|
||||
Monitor,
|
||||
MonitorStatusResponse,
|
||||
StatusChangeResult,
|
||||
@@ -15,7 +14,6 @@ const SERVICE_NAME = "StatusService";
|
||||
|
||||
export interface IStatusService {
|
||||
updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise<boolean>;
|
||||
getStatusString(status: boolean | undefined): string;
|
||||
handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise<void>;
|
||||
updateMonitorStatus(
|
||||
statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>,
|
||||
@@ -39,7 +37,7 @@ export class StatusService implements IStatusService {
|
||||
return StatusService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }) {
|
||||
async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: MonitorStatusResponse }) {
|
||||
try {
|
||||
const monitorId = monitor.id;
|
||||
const { responseTime, status } = networkResponse;
|
||||
@@ -60,7 +58,7 @@ export class StatusService implements IStatusService {
|
||||
// Update stats
|
||||
|
||||
// Last response time
|
||||
stats.lastResponseTime = responseTime;
|
||||
stats.lastResponseTime = responseTime ?? 0;
|
||||
|
||||
// Avg response time:
|
||||
let avgResponseTime = stats.avgResponseTime;
|
||||
@@ -111,12 +109,6 @@ export class StatusService implements IStatusService {
|
||||
}
|
||||
}
|
||||
|
||||
getStatusString = (status: boolean | undefined) => {
|
||||
if (status === true) return "up";
|
||||
if (status === false) return "down";
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => {
|
||||
try {
|
||||
let savedCheck;
|
||||
@@ -208,9 +200,7 @@ export class StatusService implements IStatusService {
|
||||
monitor.recentChecks.shift();
|
||||
}
|
||||
|
||||
if (monitor.status === undefined || monitor.status === null) {
|
||||
monitor.status = status;
|
||||
}
|
||||
monitor.status = status === true ? "up" : "down";
|
||||
|
||||
const prevStatus = monitor.status;
|
||||
let newStatus = monitor.status;
|
||||
@@ -233,13 +223,13 @@ export class StatusService implements IStatusService {
|
||||
const failureRate = (failures / monitor.statusWindow.length) * 100;
|
||||
|
||||
// If threshold has been met and the monitor is not already down, mark down:
|
||||
if (failureRate >= monitor.statusWindowThreshold && monitor.status !== false) {
|
||||
newStatus = false;
|
||||
if (failureRate >= monitor.statusWindowThreshold && monitor.status !== "down") {
|
||||
newStatus = "down";
|
||||
statusChanged = true;
|
||||
}
|
||||
// If the failure rate is below the threshold and the monitor is down, recover:
|
||||
else if (failureRate < monitor.statusWindowThreshold && monitor.status === false) {
|
||||
newStatus = true;
|
||||
else if (failureRate < monitor.statusWindowThreshold && monitor.status === "down") {
|
||||
newStatus = "up";
|
||||
statusChanged = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Check } from "@/types/check.js";
|
||||
import type { CheckSnapshot } from "@/types/check.js";
|
||||
export type { CheckSnapshot } from "@/types/check.js";
|
||||
|
||||
export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const;
|
||||
export type MonitorType = (typeof MonitorTypes)[number];
|
||||
|
||||
export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance"] as const;
|
||||
export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export interface MonitorThresholds {
|
||||
usage_cpu?: number;
|
||||
usage_memory?: number;
|
||||
@@ -20,7 +22,7 @@ export interface Monitor {
|
||||
teamId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
status?: boolean;
|
||||
status: MonitorStatus;
|
||||
statusWindow: boolean[];
|
||||
statusWindowSize: number;
|
||||
statusWindowThreshold: number;
|
||||
@@ -56,6 +58,8 @@ export interface MonitorsSummary {
|
||||
upMonitors: number;
|
||||
downMonitors: number;
|
||||
pausedMonitors: number;
|
||||
initializingMonitors: number;
|
||||
maintenanceMonitors: number;
|
||||
}
|
||||
|
||||
export interface MonitorsWithChecksByTeamIdResult {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
Monitor,
|
||||
MonitorMatchMethod,
|
||||
MonitorType,
|
||||
MonitorStatus,
|
||||
} from "@/types/index.js";
|
||||
|
||||
export interface MonitorStatusResponse<T = any> {
|
||||
@@ -106,7 +107,7 @@ export interface MonitorPayloadMap {
|
||||
export type StatusChangeResult = {
|
||||
monitor: Monitor;
|
||||
statusChanged: boolean;
|
||||
prevStatus: boolean | undefined;
|
||||
prevStatus: MonitorStatus;
|
||||
code: number;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user