This commit is contained in:
Alex Holliday committed 2026-01-02 20:33:34 +00:00
1 parent 7f15811fdf
commit 8e6a8e5a06
7 files changed
+113 -114

No files matched your search

@@ -88,6 +88,7 @@ const UptimeMonitors = () => {
);
const monitors: IMonitor[] = response?.data?.monitors ?? ([] as IMonitor[]);
const checksMap = response?.data?.checksMap || {};
const count = response?.data?.count || 0;
const upCount = response?.data?.upCount || 0;
const downCount = response?.data?.downCount || 0;
@@ -156,6 +157,7 @@ const UptimeMonitors = () => {
/>
<MonitorTable
monitors={monitors}
checksMap={checksMap}
refetch={refetch}
setSelectedMonitor={setSelectedMonitor}
sortField={sortField}
@@ -14,12 +14,14 @@ import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { usePatch } from "@/hooks/UseApi";
import type { ICheck } from "@/types/check";
import type { IMonitor } from "@/types/monitor";
import type { ActionMenuItem } from "@/components/actions-menu";
import type { ChartType } from "@/features/uiSlice";
export const MonitorTable = ({
monitors,
checksMap,
refetch,
setSelectedMonitor,
sortField,
@@ -33,6 +35,7 @@ export const MonitorTable = ({
setRowsPerPage,
}: {
monitors: IMonitor[];
checksMap: Record<string, ICheck[]>;
refetch: Function;
setSelectedMonitor: Function;
sortField: string;
@@ -206,9 +209,9 @@ export const MonitorTable = ({
content: t("monitors.uptime.table.headers.responseTime"),
render: (row) => {
if (chartType === "histogram") {
return <HistogramResponseTime checks={row.latestChecks ?? []} />;
return <HistogramResponseTime checks={checksMap[row._id] ?? []} />;
} else {
return <HeatmapResponseTime checks={row.latestChecks ?? []} />;
return <HeatmapResponseTime checks={checksMap[row._id] ?? []} />;
}
},
},
+2 -1
View File
@@ -29,7 +29,7 @@ export interface IMonitor {
createdBy: string;
interval: number;
rejectUnauthorized: boolean;
latestChecks: ICheck[];
latestStatuses: MonitorStatus[];
n: number;
name: string;
port: number;
@@ -56,6 +56,7 @@ export interface IMonitorWithStats {
upCount: number;
pausedCount: number;
monitors: IMonitor[];
checksMap: Record<string, ICheck[]>;
}
export interface IMonitorWithMonitorStats {
+3 -41
View File
@@ -24,6 +24,7 @@ export const MonitorStatuses = [
"down",
"paused",
"initializing",
"resuming",
] as const;
export type MonitorStatus = (typeof MonitorStatuses)[number];
@@ -56,7 +57,7 @@ export interface IMonitor extends Document {
n: number; // Number of consecutive successes required to change status
thresholds?: IMonitorThresholds;
lastCheckedAt?: Date;
latestChecks: LatestCheck[];
latestStatuses?: MonitorStatus[];
notificationChannels?: Types.ObjectId[];
createdBy: Types.ObjectId;
updatedBy: Types.ObjectId;
@@ -98,46 +99,7 @@ const MonitorSchema = new Schema<IMonitor>(
n: { type: Number, required: true, default: 1 },
thresholds: { type: ThresholdsSchema, required: false, default: undefined },
lastCheckedAt: { type: Date },
latestChecks: {
type: [
{
status: {
type: String,
required: true,
enum: MonitorStatuses,
},
responseTime: { type: Number, required: true },
checkedAt: { type: Date, required: true },
dockerContainers: {
type: [
{
container_id: { type: String },
container_name: { type: String },
status: { type: String },
health: {
healthy: { type: Boolean },
source: { type: String },
message: { type: String },
},
running: { type: Boolean },
base_image: { type: String },
exposed_ports: [
{
port: { type: String },
protocol: { type: String },
},
],
started_at: { type: Number },
finished_at: { type: Number },
},
],
required: false,
default: undefined,
},
},
],
default: [],
},
latestStatuses: { type: [String], enum: MonitorStatuses, default: [] },
notificationChannels: {
type: [{ type: Schema.Types.ObjectId, ref: "NotificationChannel" }],
default: [],
@@ -0,0 +1,58 @@
import { Check } from "@/db/models/index.js";
import type { ICheck } from "@/db/models/index.js";
import type { IChecksRepository } from "@/repositories/checks/IChecksRepository.js";
import { CheckEntity } from "@/types/domain/check.js";
import mongoose from "mongoose";
export class MongoChecksRepository implements IChecksRepository {
private toEntity = (check: ICheck): CheckEntity => {
return {
id: check._id.toString(),
metadata: {
monitorId: check.metadata.monitorId.toString(),
teamId: check.metadata.teamId.toString(),
type: check.metadata.type,
},
status: check.status,
httpStatusCode: check.httpStatusCode,
message: check.message,
timings: check.timings,
errorMessage: check.errorMessage,
ackAt: check.ackAt,
ackBy: check.ackBy?.toString(),
system: check.system,
capture: check.capture,
lighthouse: check.lighthouse,
dockerContainers: check.dockerContainers,
createdAt: check.createdAt,
updatedAt: check.updatedAt,
expiry: check.expiry,
};
};
findLatestByMonitorIds = async (monitorIds: string[]) => {
const checks = await Check.aggregate([
{
$match: {
"metadata.monitorId": {
$in: monitorIds.map((id) => new mongoose.Types.ObjectId(id)),
},
},
},
{ $sort: { createdAt: -1 } },
{
$group: {
_id: "$metadata.monitorId",
latestChecks: { $push: "$$ROOT" },
},
},
{
$project: {
latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] },
},
},
]);
console.log(checks);
return checks;
};
}
+22 -31
View File
@@ -189,48 +189,39 @@ class MonitorService implements IMonitorService {
.skip(skip)
.limit(rowsPerPage);
if (
type.length === 1 &&
(type[0] === "pagespeed" ||
type[0] === "infrastructure" ||
type[0] === "docker")
) {
const monitorIds = monitors.map((m) => m._id);
const monitorIds = monitors.map((m) => m._id);
const checks = await Check.aggregate([
{
$match: {
"metadata.monitorId": { $in: monitorIds },
},
const checks = await Check.aggregate([
{
$match: {
"metadata.monitorId": { $in: monitorIds },
},
{ $sort: { createdAt: -1 } },
{
$group: {
_id: "$metadata.monitorId",
latestChecks: { $push: "$$ROOT" },
},
},
{ $sort: { createdAt: -1 } },
{
$group: {
_id: "$metadata.monitorId",
latestChecks: { $push: "$$ROOT" },
},
{
$project: {
latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] },
},
},
{
$project: {
latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] },
},
]);
},
]);
const checksMap = new Map(
checks.map((c: any) => [c._id.toString(), c.latestChecks])
);
const checksMap = new Map(
checks.map((c: any) => [c._id.toString(), c.latestChecks])
);
monitors.forEach((monitor) => {
monitor.latestChecks = checksMap.get(monitor._id.toString()) || [];
});
}
return {
count: counts.total,
upCount: counts.upCount,
downCount: counts.downCount,
pausedCount: counts.pausedCount,
monitors,
checksMap: Object.fromEntries(checksMap),
};
};
@@ -786,7 +777,7 @@ class MonitorService implements IMonitorService {
status: {
$cond: {
if: { $eq: ["$status", "paused"] },
then: "initializing",
then: "resuming",
else: "paused",
},
},
@@ -5,7 +5,7 @@ import {
ICheck,
} from "@/db/models/index.js";
import { StatusResponse } from "./NetworkService.js";
import { LatestCheck } from "@/db/models/monitors/Monitor.js";
// Using compact latestStatuses on Monitor for transition checks
const emptyMetrics = (): ThresholdEvaluationResult["metrics"] => ({
cpu: { breached: false },
@@ -73,49 +73,31 @@ class StatusService implements IStatusService {
const newStatus = statusResponse.status;
monitor.lastCheckedAt = new Date();
// Store latest checks for display
monitor.latestChecks = monitor.latestChecks || [];
const nextCheck: LatestCheck = {
status: newStatus,
responseTime: statusResponse.responseTime,
checkedAt: monitor.lastCheckedAt,
};
// If this is a docker type, add the snapshot
if (monitor.type === "docker") {
const payload: any = statusResponse.payload;
nextCheck.dockerContainers = payload.data;
}
monitor.latestChecks.push(nextCheck);
if (monitor.type === "docker") {
monitor.latestStatuses = monitor.latestStatuses || [];
const prevLen = monitor.latestStatuses.length;
monitor.latestStatuses.push(newStatus);
while (monitor.latestStatuses.length > MAX_LATEST_CHECKS) {
monitor.latestStatuses.shift();
}
while (monitor.latestChecks.length > MAX_LATEST_CHECKS) {
monitor.latestChecks.shift();
}
// Update monitor status
if (monitor.status === "initializing") {
monitor.status = newStatus;
return [await monitor.save(), monitor.latestChecks.length === 0];
} else {
const { n } = monitor;
const latestChecks = monitor.latestChecks.slice(-n);
// Return early if not enough statuses to evaluate
if (latestChecks.length < n) {
return [await monitor.save(), false];
}
// If all different than current status, update status
const allDifferent = latestChecks.every(
(check) => check.status !== monitor.status
);
if (allDifferent && monitor.status !== newStatus) {
monitor.status = newStatus;
}
return [await monitor.save(), allDifferent];
const lenAfter = monitor.latestStatuses.length;
const thresholdReachedNow = lenAfter >= monitor.n && prevLen < monitor.n;
return [await monitor.save(), thresholdReachedNow];
}
const { n } = monitor;
const latestN = monitor.latestStatuses.slice(-n);
if (latestN.length < n) {
return [await monitor.save(), false];
}
const allDifferent = latestN.every((s) => s !== monitor.status);
if (allDifferent && monitor.status !== newStatus) {
monitor.status = newStatus;
}
return [await monitor.save(), allDifferent];
};
calculateAvgResponseTime = (