Merge pull request #3825 from bluewave-labs/fix/check-snapshot
fix: check snapshot
This commit is contained in:
27 files changed
+383
-294
No files matched your search
@@ -11,7 +11,8 @@ export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const latestCheck = monitor?.recentChecks?.[0];
|
||||
const recentChecks = monitor?.recentChecks ?? [];
|
||||
const latestCheck = recentChecks[recentChecks.length - 1];
|
||||
// Get data from latest check
|
||||
const physicalCores = getCores(latestCheck?.cpu?.physical_core);
|
||||
const logicalCores = getCores(latestCheck?.cpu?.logical_core);
|
||||
@@ -21,13 +22,7 @@ export const StatusBoxes = ({ monitor }: { monitor: Monitor }) => {
|
||||
const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0;
|
||||
const diskTotalBytes =
|
||||
latestCheck?.disk?.reduce((acc, disk) => acc + (disk.total_bytes || 0), 0) || 0;
|
||||
const os = getOsAndPlatform(latestCheck?.host);
|
||||
|
||||
const platform = latestCheck?.host?.platform ?? undefined;
|
||||
const osPlatform =
|
||||
typeof os === "undefined" && typeof platform === "undefined"
|
||||
? undefined
|
||||
: `${os} ${platform}`;
|
||||
const osPlatform = getOsAndPlatform(latestCheck?.host);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
|
||||
@@ -21,10 +21,11 @@ export const TabOverview = ({
|
||||
}
|
||||
|
||||
const checks = stats?.checks || [];
|
||||
const recentChecks = monitor.recentChecks ?? [];
|
||||
return (
|
||||
<Stack gap={theme.spacing(8)}>
|
||||
<StatusBoxes monitor={monitor} />
|
||||
<InfraDetailsGauges snapshot={monitor.recentChecks?.[0]} />
|
||||
<InfraDetailsGauges snapshot={recentChecks[recentChecks.length - 1]} />
|
||||
<InfraDetailsCharts
|
||||
checks={checks}
|
||||
dateRange={dateRange}
|
||||
|
||||
@@ -248,7 +248,8 @@ export const InfraMonitorsTable = ({
|
||||
id: "cpu",
|
||||
content: t("pages.infrastructure.table.headers.cpu"),
|
||||
render: (row) => {
|
||||
const check = row.recentChecks?.[0];
|
||||
const recentChecks = row.recentChecks ?? [];
|
||||
const check = recentChecks[recentChecks.length - 1];
|
||||
const cpuUsage = (check?.cpu?.usage_percent || 0) * 100;
|
||||
return <Gauge progress={cpuUsage} />;
|
||||
},
|
||||
@@ -257,7 +258,8 @@ export const InfraMonitorsTable = ({
|
||||
id: "memory",
|
||||
content: t("pages.infrastructure.table.headers.memory"),
|
||||
render: (row) => {
|
||||
const check = row.recentChecks?.[0];
|
||||
const recentChecks = row.recentChecks ?? [];
|
||||
const check = recentChecks[recentChecks.length - 1];
|
||||
const memoryUsage = (check?.memory?.usage_percent || 0) * 100;
|
||||
return <Gauge progress={memoryUsage} />;
|
||||
},
|
||||
@@ -266,7 +268,8 @@ export const InfraMonitorsTable = ({
|
||||
id: "disk",
|
||||
content: t("pages.infrastructure.table.headers.disk"),
|
||||
render: (row) => {
|
||||
const check = row.recentChecks?.[0];
|
||||
const recentChecks = row.recentChecks ?? [];
|
||||
const check = recentChecks[recentChecks.length - 1];
|
||||
|
||||
const totalDiskUsage = check?.disk?.reduce(
|
||||
(acc, disk) => acc + (disk?.usage_percent || 0),
|
||||
|
||||
@@ -45,6 +45,8 @@ const PageSpeedDetails = () => {
|
||||
const monitor = monitorData?.monitorData?.monitor;
|
||||
const groupedChecks = monitorData?.monitorData?.groupedChecks || [];
|
||||
const monitorStats = monitorData?.monitorStats || null;
|
||||
const recentChecks = monitor?.recentChecks ?? [];
|
||||
const latestCheck = recentChecks[recentChecks.length - 1];
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
@@ -75,8 +77,8 @@ const PageSpeedDetails = () => {
|
||||
direction={{ xs: "column", md: "row" }}
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<PiePageSpeed latestCheck={monitor?.recentChecks?.[0]} />
|
||||
<PiePageSpeedLegend latestCheck={monitor?.recentChecks?.[0]} />
|
||||
<PiePageSpeed latestCheck={latestCheck} />
|
||||
<PiePageSpeedLegend latestCheck={latestCheck} />
|
||||
</Stack>
|
||||
</BasePage>
|
||||
);
|
||||
|
||||
@@ -36,7 +36,8 @@ const heatLevel = (value: number): GaugeFillLevel =>
|
||||
|
||||
export const ThemedInfrastructure = ({ monitor, sxApi }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const latest = monitor.recentChecks?.[0];
|
||||
const recentChecks = monitor.recentChecks ?? [];
|
||||
const latest = recentChecks[recentChecks.length - 1];
|
||||
|
||||
const renderEmpty = () => (
|
||||
<Box sx={sxApi.emptySx}>{t("pages.statusPages.monitorsList.noData")}</Box>
|
||||
|
||||
@@ -250,7 +250,44 @@ export interface ChecksSummary {
|
||||
downChecks: number;
|
||||
}
|
||||
|
||||
export type CheckSnapshot = Omit<Check, "metadata" | "__v" | "updatedAt">;
|
||||
export type SnapshotCpuInfo = Pick<
|
||||
CheckCpuInfo,
|
||||
| "physical_core"
|
||||
| "logical_core"
|
||||
| "frequency"
|
||||
| "current_frequency"
|
||||
| "temperature"
|
||||
| "usage_percent"
|
||||
>;
|
||||
export type SnapshotMemoryInfo = Pick<
|
||||
CheckMemoryInfo,
|
||||
"total_bytes" | "used_bytes" | "usage_percent"
|
||||
>;
|
||||
export type SnapshotDiskInfo = Pick<
|
||||
CheckDiskInfo,
|
||||
"device" | "total_bytes" | "used_bytes" | "usage_percent"
|
||||
>;
|
||||
export type SnapshotHostInfo = Pick<CheckHostInfo, "os" | "platform" | "pretty_name">;
|
||||
|
||||
export type CheckSnapshot = Pick<
|
||||
Check,
|
||||
| "id"
|
||||
| "status"
|
||||
| "responseTime"
|
||||
| "statusCode"
|
||||
| "message"
|
||||
| "createdAt"
|
||||
| "accessibility"
|
||||
| "bestPractices"
|
||||
| "seo"
|
||||
| "performance"
|
||||
| "audits"
|
||||
> & {
|
||||
cpu?: SnapshotCpuInfo;
|
||||
memory?: SnapshotMemoryInfo;
|
||||
disk?: SnapshotDiskInfo[];
|
||||
host?: SnapshotHostInfo;
|
||||
};
|
||||
export interface HasResponseTime {
|
||||
responseTime: number;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { MonitorModel } from "../../domain/monitors/monitor.model.js";
|
||||
import type { ILogger } from "@/utils/logger.js";
|
||||
|
||||
const KEEP_KEYS = [
|
||||
"id",
|
||||
"status",
|
||||
"responseTime",
|
||||
"statusCode",
|
||||
"message",
|
||||
"createdAt",
|
||||
"cpu",
|
||||
"memory",
|
||||
"disk",
|
||||
"host",
|
||||
"accessibility",
|
||||
"bestPractices",
|
||||
"seo",
|
||||
"performance",
|
||||
"audits",
|
||||
];
|
||||
|
||||
/**
|
||||
* Slim stored recentChecks snapshots down to the fields the slim CheckSnapshot type keeps.
|
||||
* The schema change only affects new writes; paused monitors never rotate their snapshots out.
|
||||
* Idempotent: re-running on already-slim snapshots is a no-op re-pick.
|
||||
*/
|
||||
export async function slimRecentChecks(logger: ILogger): Promise<void> {
|
||||
const SERVICE_NAME = "Migration:SlimRecentChecks";
|
||||
try {
|
||||
logger.info({ service: SERVICE_NAME, message: "Slimming recentChecks snapshots" });
|
||||
|
||||
const result = await MonitorModel.updateMany({ "recentChecks.0": { $exists: true } }, [
|
||||
{
|
||||
$set: {
|
||||
recentChecks: {
|
||||
$map: {
|
||||
input: "$recentChecks",
|
||||
as: "snap",
|
||||
in: {
|
||||
$mergeObjects: [
|
||||
// 1) keep whitelisted top-level keys only (drops timings/errors/capture/net)
|
||||
{
|
||||
$arrayToObject: {
|
||||
$filter: {
|
||||
input: { $objectToArray: "$$snap" },
|
||||
as: "kv",
|
||||
cond: { $in: ["$$kv.k", KEEP_KEYS] },
|
||||
},
|
||||
},
|
||||
},
|
||||
// 2) where a nested group exists, overwrite it with its slim pick
|
||||
{
|
||||
$cond: [
|
||||
{ $eq: [{ $type: "$$snap.cpu" }, "object"] },
|
||||
{
|
||||
cpu: {
|
||||
physical_core: "$$snap.cpu.physical_core",
|
||||
logical_core: "$$snap.cpu.logical_core",
|
||||
frequency: "$$snap.cpu.frequency",
|
||||
current_frequency: "$$snap.cpu.current_frequency",
|
||||
temperature: "$$snap.cpu.temperature",
|
||||
usage_percent: "$$snap.cpu.usage_percent",
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{
|
||||
$cond: [
|
||||
{ $eq: [{ $type: "$$snap.memory" }, "object"] },
|
||||
{
|
||||
memory: {
|
||||
total_bytes: "$$snap.memory.total_bytes",
|
||||
used_bytes: "$$snap.memory.used_bytes",
|
||||
usage_percent: "$$snap.memory.usage_percent",
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{
|
||||
$cond: [
|
||||
{ $eq: [{ $type: "$$snap.disk" }, "array"] },
|
||||
{
|
||||
disk: {
|
||||
$map: {
|
||||
input: "$$snap.disk",
|
||||
as: "d",
|
||||
in: {
|
||||
device: "$$d.device",
|
||||
total_bytes: "$$d.total_bytes",
|
||||
used_bytes: "$$d.used_bytes",
|
||||
usage_percent: "$$d.usage_percent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
{
|
||||
$cond: [
|
||||
{ $eq: [{ $type: "$$snap.host" }, "object"] },
|
||||
{
|
||||
host: {
|
||||
os: "$$snap.host.os",
|
||||
platform: "$$snap.host.platform",
|
||||
pretty_name: "$$snap.host.pretty_name",
|
||||
},
|
||||
},
|
||||
{},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
logger.info({ service: SERVICE_NAME, message: `Slimmed recentChecks on ${result.modifiedCount} monitors` });
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error({ service: SERVICE_NAME, message: `Error slimming recentChecks: ${errorMessage}` });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { cleanupDuplicateMonitorStatsForUniqueIndex } from "./0006_cleanupDuplic
|
||||
import { migrateMaintenanceWindowMonitorIdToArray } from "./0007_migrateMaintenanceWindowMonitorIdToArray.js";
|
||||
import { backfillMonitorLastEvaluatedAt } from "./0008_backfillMonitorLastEvaluatedAt.js";
|
||||
import { recomputeResponseTimeFromTimings } from "./0009_recomputeResponseTimeFromTimings.js";
|
||||
import { slimRecentChecks } from "./0010_slimRecentChecks.js";
|
||||
import type { ILogger } from "@/utils/logger.js";
|
||||
|
||||
type MigrationEntry = {
|
||||
@@ -25,6 +26,7 @@ const migrations: MigrationEntry[] = [
|
||||
{ name: "0007_migrateMaintenanceWindowMonitorIdToArray", execute: migrateMaintenanceWindowMonitorIdToArray },
|
||||
{ name: "0008_backfillMonitorLastEvaluatedAt", execute: backfillMonitorLastEvaluatedAt },
|
||||
{ name: "0009_recomputeResponseTimeFromTimings", execute: recomputeResponseTimeFromTimings },
|
||||
{ name: "0010_slimRecentChecks", execute: slimRecentChecks },
|
||||
];
|
||||
|
||||
const runMigrations = async (logger: ILogger) => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
UptimeChecksResult,
|
||||
} from "@/domain/checks/check.type.js";
|
||||
import type { MonitorType } from "@/domain/monitors/monitor.type.js";
|
||||
import type { LatestChecksMap } from "@/domain/checks/check.repository.mongo.js";
|
||||
import { CheckFilter, DateRange } from "@/types/query.js";
|
||||
|
||||
export interface IChecksRepository {
|
||||
@@ -34,7 +33,6 @@ export interface IChecksRepository {
|
||||
teamId: string,
|
||||
filter?: CheckFilter
|
||||
): Promise<ChecksQueryResult>;
|
||||
findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
|
||||
findByDateRangeAndMonitorId(
|
||||
monitorId: string,
|
||||
dateRange: DateRange,
|
||||
|
||||
@@ -27,8 +27,6 @@ import { NETWORK_ERROR } from "@/types/network.js";
|
||||
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
export type LatestChecksMap = Record<string, Check[]>;
|
||||
|
||||
class MongoChecksRepository implements IChecksRepository {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
@@ -83,6 +81,7 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
os: host?.os ?? "",
|
||||
platform: host?.platform ?? "",
|
||||
kernel_version: host?.kernel_version ?? "",
|
||||
pretty_name: host?.pretty_name ?? "",
|
||||
});
|
||||
|
||||
const mapCapture = (capture?: CheckCaptureInfo): CheckCaptureInfo => ({
|
||||
@@ -288,32 +287,6 @@ class MongoChecksRepository implements IChecksRepository {
|
||||
return { checksCount, checks: this.mapDocuments(checks) };
|
||||
};
|
||||
|
||||
findLatestByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap> => {
|
||||
if (monitorIds.length === 0) {
|
||||
return {};
|
||||
}
|
||||
const limitPerMonitor = options?.limitPerMonitor ?? 25;
|
||||
const dateFilter = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
const results = await Promise.all(
|
||||
monitorIds.map(async (monitorId) => {
|
||||
const docs = await CheckModel.find({
|
||||
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
|
||||
createdAt: { $gte: dateFilter },
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.limit(limitPerMonitor)
|
||||
.lean();
|
||||
return { monitorId, docs };
|
||||
})
|
||||
);
|
||||
|
||||
const mapped = results.reduce<LatestChecksMap>((acc, { monitorId, docs }) => {
|
||||
acc[monitorId] = docs.map((doc: CheckDocument) => this.toEntity(doc));
|
||||
return acc;
|
||||
}, {});
|
||||
return mapped;
|
||||
};
|
||||
|
||||
findByDateRangeAndMonitorId = async (monitorId: string, dateRange: DateRange, options?: { type?: MonitorType }) => {
|
||||
const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
|
||||
const start = getDateForRange(dateRange);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import type {
|
||||
Check,
|
||||
CheckAudits,
|
||||
CheckCpuInfo,
|
||||
CheckDiskInfo,
|
||||
CheckHostInfo,
|
||||
CheckMemoryInfo,
|
||||
CheckSnapshot,
|
||||
SnapshotCpuInfo,
|
||||
SnapshotDiskInfo,
|
||||
SnapshotHostInfo,
|
||||
SnapshotMemoryInfo,
|
||||
} from "@/domain/checks/check.type.js";
|
||||
import { toDateString } from "@/utils/mongoMappers.js";
|
||||
|
||||
export type CheckSnapshotSource = Pick<
|
||||
Check,
|
||||
"id" | "status" | "responseTime" | "statusCode" | "message" | "accessibility" | "bestPractices" | "seo" | "performance"
|
||||
> & {
|
||||
createdAt: string | Date;
|
||||
cpu?: CheckCpuInfo;
|
||||
memory?: CheckMemoryInfo;
|
||||
disk?: CheckDiskInfo[];
|
||||
host?: CheckHostInfo;
|
||||
audits?: CheckAudits;
|
||||
};
|
||||
|
||||
const mapCpu = (cpu?: CheckCpuInfo): SnapshotCpuInfo | undefined =>
|
||||
cpu && {
|
||||
physical_core: cpu.physical_core,
|
||||
logical_core: cpu.logical_core,
|
||||
frequency: cpu.frequency,
|
||||
current_frequency: cpu.current_frequency,
|
||||
temperature: cpu.temperature,
|
||||
usage_percent: cpu.usage_percent,
|
||||
};
|
||||
|
||||
const mapMemory = (memory?: CheckMemoryInfo): SnapshotMemoryInfo | undefined =>
|
||||
memory && {
|
||||
total_bytes: memory.total_bytes,
|
||||
used_bytes: memory.used_bytes,
|
||||
usage_percent: memory.usage_percent,
|
||||
};
|
||||
|
||||
const mapDisk = (disk?: CheckDiskInfo[]): SnapshotDiskInfo[] | undefined =>
|
||||
disk?.map((entry) => ({
|
||||
device: entry.device,
|
||||
total_bytes: entry.total_bytes,
|
||||
used_bytes: entry.used_bytes,
|
||||
usage_percent: entry.usage_percent,
|
||||
}));
|
||||
|
||||
const mapHost = (host?: CheckHostInfo): SnapshotHostInfo | undefined =>
|
||||
host && {
|
||||
os: host.os,
|
||||
platform: host.platform,
|
||||
pretty_name: host.pretty_name,
|
||||
};
|
||||
|
||||
const mapAudits = (audits?: CheckAudits): CheckAudits | undefined =>
|
||||
audits && {
|
||||
cls: audits.cls,
|
||||
si: audits.si,
|
||||
fcp: audits.fcp,
|
||||
lcp: audits.lcp,
|
||||
tbt: audits.tbt,
|
||||
};
|
||||
|
||||
export const toCheckSnapshot = (source: CheckSnapshotSource): CheckSnapshot => ({
|
||||
id: source.id,
|
||||
status: source.status,
|
||||
responseTime: source.responseTime,
|
||||
statusCode: source.statusCode,
|
||||
message: source.message,
|
||||
createdAt: toDateString(source.createdAt),
|
||||
cpu: mapCpu(source.cpu),
|
||||
memory: mapMemory(source.memory),
|
||||
disk: mapDisk(source.disk),
|
||||
host: mapHost(source.host),
|
||||
accessibility: source.accessibility,
|
||||
bestPractices: source.bestPractices,
|
||||
seo: source.seo,
|
||||
performance: source.performance,
|
||||
audits: mapAudits(source.audits),
|
||||
});
|
||||
@@ -168,8 +168,36 @@ export interface HasResponseTime {
|
||||
responseTime: number;
|
||||
}
|
||||
|
||||
export type CheckSnapshot = Omit<Check, "metadata" | "updatedAt">;
|
||||
export type SnapshotCpuInfo = Pick<
|
||||
CheckCpuInfo,
|
||||
"physical_core" | "logical_core" | "frequency" | "current_frequency" | "temperature" | "usage_percent"
|
||||
>;
|
||||
export type SnapshotMemoryInfo = Pick<CheckMemoryInfo, "total_bytes" | "used_bytes" | "usage_percent">;
|
||||
export type SnapshotDiskInfo = Pick<CheckDiskInfo, "device" | "total_bytes" | "used_bytes" | "usage_percent">;
|
||||
export type SnapshotHostInfo = Pick<CheckHostInfo, "os" | "platform" | "pretty_name">;
|
||||
|
||||
export type CheckSnapshot = Pick<
|
||||
Check,
|
||||
// uptime charts and down check tooltips
|
||||
| "id"
|
||||
| "status"
|
||||
| "responseTime"
|
||||
| "statusCode"
|
||||
| "message"
|
||||
| "createdAt"
|
||||
// pagespeed
|
||||
| "accessibility"
|
||||
| "bestPractices"
|
||||
| "seo"
|
||||
| "performance"
|
||||
| "audits"
|
||||
> & {
|
||||
// hardware monitors only
|
||||
cpu?: SnapshotCpuInfo;
|
||||
memory?: SnapshotMemoryInfo;
|
||||
disk?: SnapshotDiskInfo[];
|
||||
host?: SnapshotHostInfo;
|
||||
};
|
||||
export interface HardwareDiskStats {
|
||||
name: string;
|
||||
readSpeed: number;
|
||||
|
||||
@@ -106,7 +106,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
|
||||
start: string;
|
||||
end: string;
|
||||
}) => {
|
||||
const monitors = await this.monitorsRepository.findByIds(monitorIDs);
|
||||
const monitors = await this.monitorsRepository.findByIds(monitorIDs, { includeRecentChecks: false });
|
||||
|
||||
const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId);
|
||||
|
||||
@@ -193,7 +193,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
|
||||
const update: Partial<MaintenanceWindow> = rest;
|
||||
|
||||
if (monitors !== undefined) {
|
||||
const monitorDocs = await this.monitorsRepository.findByIds(monitors);
|
||||
const monitorDocs = await this.monitorsRepository.findByIds(monitors, { includeRecentChecks: false });
|
||||
const unauthorizedMonitors = monitorDocs.filter((monitor) => monitor.teamId !== teamId);
|
||||
if (unauthorizedMonitors.length > 0) {
|
||||
throw new AppError({
|
||||
|
||||
@@ -3,15 +3,11 @@ import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/domain/monito
|
||||
import { DnsRecordTypes, MonitorTypes, MonitorStatuses, PageSpeedStrategies, HttpMethods } from "@/domain/monitors/monitor.type.js";
|
||||
import type {
|
||||
CheckAudits,
|
||||
CheckCaptureInfo,
|
||||
CheckCpuInfo,
|
||||
CheckDiskInfo,
|
||||
CheckErrorInfo,
|
||||
CheckHostInfo,
|
||||
CheckMemoryInfo,
|
||||
CheckNetworkInterfaceInfo,
|
||||
GotTimings,
|
||||
ILighthouseAudit,
|
||||
SnapshotCpuInfo,
|
||||
SnapshotDiskInfo,
|
||||
SnapshotHostInfo,
|
||||
SnapshotMemoryInfo,
|
||||
} from "@/domain/checks/check.type.js";
|
||||
|
||||
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
||||
@@ -37,121 +33,46 @@ interface MonitorDocument extends MonitorDocumentBase {
|
||||
lastEvaluatedAt: number; // epoch ms
|
||||
}
|
||||
|
||||
const snapshotTimingPhasesSchema = new Schema<GotTimings["phases"]>(
|
||||
{
|
||||
wait: { type: Number },
|
||||
dns: { type: Number },
|
||||
tcp: { type: Number },
|
||||
tls: { type: Number },
|
||||
request: { type: Number },
|
||||
firstByte: { type: Number },
|
||||
download: { type: Number },
|
||||
total: { type: Number },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const snapshotTimingsSchema = new Schema<GotTimings>(
|
||||
{
|
||||
start: { type: Number },
|
||||
socket: { type: Number },
|
||||
lookup: { type: Number },
|
||||
connect: { type: Number },
|
||||
secureConnect: { type: Number },
|
||||
upload: { type: Number },
|
||||
response: { type: Number },
|
||||
end: { type: Number },
|
||||
phases: { type: snapshotTimingPhasesSchema },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const snapshotCpuSchema = new Schema<CheckCpuInfo>(
|
||||
const snapshotCpuSchema = new Schema<SnapshotCpuInfo>(
|
||||
{
|
||||
physical_core: { type: Number },
|
||||
logical_core: { type: Number },
|
||||
frequency: { type: Number },
|
||||
current_frequency: { type: Number },
|
||||
temperature: { type: [Number] },
|
||||
free_percent: { type: Number },
|
||||
usage_percent: { type: Number },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const snapshotMemorySchema = new Schema<CheckMemoryInfo>(
|
||||
const snapshotMemorySchema = new Schema<SnapshotMemoryInfo>(
|
||||
{
|
||||
total_bytes: { type: Number },
|
||||
available_bytes: { type: Number },
|
||||
used_bytes: { type: Number },
|
||||
usage_percent: { type: Number },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const snapshotDiskSchema = new Schema<CheckDiskInfo>(
|
||||
const snapshotDiskSchema = new Schema<SnapshotDiskInfo>(
|
||||
{
|
||||
device: { type: String },
|
||||
mountpoint: { type: String },
|
||||
total_bytes: { type: Number },
|
||||
free_bytes: { type: Number },
|
||||
used_bytes: { type: Number },
|
||||
usage_percent: { type: Number },
|
||||
total_inodes: { type: Number },
|
||||
free_inodes: { type: Number },
|
||||
used_inodes: { type: Number },
|
||||
inodes_usage_percent: { type: Number },
|
||||
read_bytes: { type: Number },
|
||||
write_bytes: { type: Number },
|
||||
read_time: { type: Number },
|
||||
write_time: { type: Number },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const snapshotHostSchema = new Schema<CheckHostInfo>(
|
||||
const snapshotHostSchema = new Schema<SnapshotHostInfo>(
|
||||
{
|
||||
os: { type: String },
|
||||
platform: { type: String },
|
||||
kernel_version: { type: String },
|
||||
pretty_name: { type: String },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const snapshotErrorSchema = new Schema<CheckErrorInfo>(
|
||||
{
|
||||
metric: { type: [String] },
|
||||
err: { type: String },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const snapshotCaptureSchema = new Schema<CheckCaptureInfo>(
|
||||
{
|
||||
version: { type: String },
|
||||
mode: { type: String },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const snapshotNetworkInterfaceSchema = new Schema<CheckNetworkInterfaceInfo>(
|
||||
{
|
||||
name: { type: String },
|
||||
bytes_sent: { type: Number },
|
||||
bytes_recv: { type: Number },
|
||||
packets_sent: { type: Number },
|
||||
packets_recv: { type: Number },
|
||||
err_in: { type: Number },
|
||||
err_out: { type: Number },
|
||||
drop_in: { type: Number },
|
||||
drop_out: { type: Number },
|
||||
fifo_in: { type: Number },
|
||||
fifo_out: { type: Number },
|
||||
},
|
||||
{ _id: false }
|
||||
);
|
||||
|
||||
const snapshotLighthouseAuditSchema = new Schema<ILighthouseAudit>(
|
||||
{
|
||||
id: { type: String },
|
||||
@@ -180,16 +101,12 @@ const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
|
||||
id: { type: String, required: true },
|
||||
status: { type: Boolean, required: true },
|
||||
responseTime: { type: Number },
|
||||
timings: { type: snapshotTimingsSchema },
|
||||
statusCode: { type: Number },
|
||||
message: { type: String },
|
||||
cpu: { type: snapshotCpuSchema },
|
||||
memory: { type: snapshotMemorySchema },
|
||||
disk: { type: [snapshotDiskSchema] },
|
||||
host: { type: snapshotHostSchema },
|
||||
errors: { type: [snapshotErrorSchema] },
|
||||
capture: { type: snapshotCaptureSchema },
|
||||
net: { type: [snapshotNetworkInterfaceSchema] },
|
||||
accessibility: { type: Number },
|
||||
bestPractices: { type: Number },
|
||||
seo: { type: Number },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MonitorType, Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot } from "@/domain/monitors/monitor.type.js";
|
||||
import type { MonitorType, Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot, MonitorScheduleFields } from "@/domain/monitors/monitor.type.js";
|
||||
|
||||
export interface TeamQueryConfig {
|
||||
limit?: number;
|
||||
@@ -26,11 +26,10 @@ export interface IMonitorsRepository {
|
||||
findByIdLean(monitorId: string): Promise<Monitor | null>;
|
||||
|
||||
// collection fetch
|
||||
findAll(): Promise<Monitor[]>;
|
||||
findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[]>;
|
||||
findAllForScheduling(): Promise<MonitorScheduleFields[]>;
|
||||
findByTeamId(teamId: string, config: TeamQueryConfig, options?: { includeRecentChecks?: boolean }): Promise<Monitor[]>;
|
||||
findByTeamIdWithStats(teamId: string, config: TeamQueryConfig): Promise<Monitor[]>;
|
||||
findByIds(monitorIds: string[]): Promise<Monitor[]>;
|
||||
findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise<Monitor[]>;
|
||||
findByIds(monitorIds: string[], options?: { includeRecentChecks?: boolean }): Promise<Monitor[]>;
|
||||
|
||||
// update
|
||||
updateById(monitorId: string, teamId: string, updates: Partial<Monitor>): Promise<Monitor>;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { MonitorModel } from "@/domain/monitors/monitor.model.js";
|
||||
import type { MonitorDocument, CheckSnapshotDocument } from "@/domain/monitors/monitor.model.js";
|
||||
import type { CheckSnapshot } from "@/domain/checks/check.type.js";
|
||||
import type { Monitor, MonitorStatus, MonitorsSummary } from "@/domain/monitors/monitor.type.js";
|
||||
import type { Monitor, MonitorScheduleFields, MonitorStatus, MonitorsSummary } from "@/domain/monitors/monitor.type.js";
|
||||
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
|
||||
import { MongoBulkWriteError } from "mongodb";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "@/domain/monitors/monitor.repository.interface.js";
|
||||
import { toStringId, toDateString } from "@/utils/mongoMappers.js";
|
||||
import { toCheckSnapshot } from "@/domain/checks/check.snapshot.js";
|
||||
class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
create = async (monitor: Monitor, teamId: string, userId: string) => {
|
||||
const monitorModel = new MonitorModel({ ...monitor, teamId, userId });
|
||||
@@ -40,13 +41,20 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
};
|
||||
|
||||
findByIdLean = async (monitorId: string): Promise<Monitor | null> => {
|
||||
const monitor = await MonitorModel.findOne({ _id: monitorId });
|
||||
const monitor = await MonitorModel.findOne({ _id: monitorId }).select({ recentChecks: 0 });
|
||||
return monitor ? this.toEntity(monitor) : null;
|
||||
};
|
||||
|
||||
findAll = async (): Promise<Monitor[]> => {
|
||||
const monitors = await MonitorModel.find();
|
||||
return this.mapDocuments(monitors);
|
||||
findAllForScheduling = async (): Promise<MonitorScheduleFields[]> => {
|
||||
const docs = await MonitorModel.find({}, { type: 1, isActive: 1, interval: 1, geoCheckEnabled: 1, geoCheckInterval: 1 }).lean();
|
||||
return docs.map((doc) => ({
|
||||
id: doc._id.toString(),
|
||||
type: doc.type,
|
||||
isActive: doc.isActive,
|
||||
interval: doc.interval,
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
||||
}));
|
||||
};
|
||||
|
||||
private queryBuilder = (config: TeamQueryConfig, teamId: string): FilterQuery<MonitorDocument> => {
|
||||
@@ -96,10 +104,15 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
return { sort: { [field]: order === "asc" ? 1 : -1 } as const, skip: Math.max(page, 0) * rowsPerPage, limit: rowsPerPage };
|
||||
};
|
||||
|
||||
findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise<Monitor[]> => {
|
||||
findByTeamId = async (teamId: string, config: TeamQueryConfig, options?: { includeRecentChecks?: boolean }): Promise<Monitor[]> => {
|
||||
const query = this.queryBuilder(config, teamId);
|
||||
const { sort, skip, limit } = this.pageOptions(config);
|
||||
const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limit);
|
||||
|
||||
const cursor = MonitorModel.find(query).sort(sort).skip(skip).limit(limit);
|
||||
if (options?.includeRecentChecks === false) {
|
||||
cursor.select({ recentChecks: 0 });
|
||||
}
|
||||
const documents = await cursor;
|
||||
return this.mapDocuments(documents);
|
||||
};
|
||||
|
||||
@@ -118,20 +131,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
return documents.map((doc) => this.toEntity(doc));
|
||||
};
|
||||
|
||||
findByIds = async (monitorIds: string[]): Promise<Monitor[]> => {
|
||||
if (!monitorIds.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
|
||||
const pipeline: PipelineStage[] = [{ $match: { _id: { $in: objectIds } } }, ...this.uptimeStatsLookupStages];
|
||||
|
||||
const documents = await MonitorModel.aggregate(pipeline);
|
||||
return documents.map((doc) => this.toEntity(doc));
|
||||
};
|
||||
|
||||
findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise<Monitor[]> => {
|
||||
findByIds = async (monitorIds: string[], options?: { includeRecentChecks?: boolean }): Promise<Monitor[]> => {
|
||||
if (!monitorIds.length) {
|
||||
return [];
|
||||
}
|
||||
@@ -140,57 +140,9 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
|
||||
const pipeline: PipelineStage[] = [
|
||||
{ $match: { _id: { $in: objectIds } } },
|
||||
{
|
||||
$lookup: {
|
||||
from: "checks",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [{ $match: { $expr: { $eq: ["$metadata.monitorId", "$$monitorId"] } } }, { $sort: { createdAt: -1 } }, { $limit: checksCount }],
|
||||
as: "checks",
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "maintenancewindows",
|
||||
let: { monitorId: "$_id" },
|
||||
pipeline: [{ $match: { $expr: { $in: ["$$monitorId", { $ifNull: ["$monitorIds", []] }] } } }],
|
||||
as: "maintenanceWindows",
|
||||
},
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "monitorstats",
|
||||
localField: "_id",
|
||||
foreignField: "monitorId",
|
||||
as: "stats",
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
isMaintenance: {
|
||||
$reduce: {
|
||||
input: "$maintenanceWindows",
|
||||
initialValue: false,
|
||||
in: {
|
||||
$or: [
|
||||
"$$value",
|
||||
{
|
||||
$and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
uptimePercentage: { $arrayElemAt: ["$stats.uptimePercentage", 0] },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
maintenanceWindows: 0,
|
||||
stats: 0,
|
||||
},
|
||||
},
|
||||
...(options?.includeRecentChecks === false ? [{ $project: { recentChecks: 0 } } as PipelineStage] : []),
|
||||
...this.uptimeStatsLookupStages,
|
||||
];
|
||||
|
||||
const documents = await MonitorModel.aggregate(pipeline);
|
||||
return documents.map((doc) => this.toEntity(doc));
|
||||
};
|
||||
@@ -245,7 +197,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
},
|
||||
...(statusPatch && { $set: statusPatch }),
|
||||
},
|
||||
{ returnDocument: "after" }
|
||||
{ returnDocument: "after", projection: { recentChecks: 0 } }
|
||||
);
|
||||
|
||||
if (!updatedMonitor) {
|
||||
@@ -473,7 +425,7 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
grpcServiceName: doc.grpcServiceName ?? undefined,
|
||||
strategy: doc.strategy ?? undefined,
|
||||
group: doc.group ?? null,
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)),
|
||||
recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => toCheckSnapshot(check)),
|
||||
geoCheckEnabled: doc.geoCheckEnabled ?? false,
|
||||
geoCheckLocations: doc.geoCheckLocations ?? [],
|
||||
geoCheckInterval: doc.geoCheckInterval ?? 300000,
|
||||
@@ -485,30 +437,6 @@ class MongoMonitorsRepository implements IMonitorsRepository {
|
||||
};
|
||||
};
|
||||
|
||||
private toCheckSnapshot = (doc: CheckSnapshotDocument): CheckSnapshot => {
|
||||
return {
|
||||
id: doc.id,
|
||||
status: doc.status,
|
||||
responseTime: doc.responseTime,
|
||||
timings: doc.timings,
|
||||
statusCode: doc.statusCode,
|
||||
message: doc.message,
|
||||
cpu: doc.cpu,
|
||||
memory: doc.memory,
|
||||
disk: doc.disk,
|
||||
host: doc.host,
|
||||
errors: doc.errors,
|
||||
capture: doc.capture,
|
||||
net: doc.net,
|
||||
accessibility: doc.accessibility,
|
||||
bestPractices: doc.bestPractices,
|
||||
seo: doc.seo,
|
||||
performance: doc.performance,
|
||||
audits: doc.audits,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
};
|
||||
};
|
||||
|
||||
deleteByTeamIdsNotIn = async (teamIds: string[]): Promise<number> => {
|
||||
const objectIds = teamIds.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const result = await MonitorModel.deleteMany({ teamId: { $nin: objectIds } });
|
||||
|
||||
@@ -363,7 +363,7 @@ export class MonitorService implements IMonitorService {
|
||||
const monitorsWithChecks = monitors.map((monitor: Monitor) => {
|
||||
const rawChecks = monitor.recentChecks ?? [];
|
||||
const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type);
|
||||
const checks = isSnapshotType ? rawChecks.slice(0, 1) : rawChecks;
|
||||
const checks = isSnapshotType ? rawChecks.slice(-1) : rawChecks;
|
||||
monitor.recentChecks = checks;
|
||||
return monitor;
|
||||
});
|
||||
@@ -519,7 +519,7 @@ export class MonitorService implements IMonitorService {
|
||||
};
|
||||
|
||||
exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise<Monitor[]> => {
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {}, { includeRecentChecks: false });
|
||||
|
||||
if (monitors.length === 0) {
|
||||
throw new AppError({ message: "No monitors found to export.", service: SERVICE_NAME, method: "exportMonitorsToJSON", status: 400 });
|
||||
|
||||
@@ -178,3 +178,5 @@ export interface Game {
|
||||
}
|
||||
|
||||
export type GamesMap = Record<string, Game>;
|
||||
|
||||
export type MonitorScheduleFields = Pick<Monitor, "id" | "type" | "isActive" | "interval" | "geoCheckEnabled" | "geoCheckInterval">;
|
||||
@@ -324,7 +324,7 @@ export class UserService implements IUserService {
|
||||
}
|
||||
|
||||
if (roles.includes("superadmin")) {
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {});
|
||||
const monitors = await this.monitorsRepository.findByTeamId(teamId, {}, { includeRecentChecks: false });
|
||||
if (monitors) {
|
||||
await Promise.all(monitors.map((monitor) => this.scheduler.deleteJob(monitor)));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IMonitorStatsRepository } from "@/domain/monitor-stats/monitor-stats.repository.interface.js";
|
||||
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
|
||||
import type { Check, CheckDiskInfo, CheckSnapshot } from "@/domain/checks/check.type.js";
|
||||
import type { Check, CheckDiskInfo } from "@/domain/checks/check.type.js";
|
||||
import { MonitorStatuses, type Monitor, type MonitorStatus } from "@/domain/monitors/monitor.type.js";
|
||||
import type {
|
||||
DockerStatusPayload,
|
||||
@@ -18,6 +18,7 @@ import { AppError } from "@/utils/AppError.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import type { HardwareStatusMetrics } from "@/types/network.js";
|
||||
import { MAX_RECENT_CHECKS } from "@/domain/monitors/monitor.type.js";
|
||||
import { toCheckSnapshot } from "@/domain/checks/check.snapshot.js";
|
||||
|
||||
const SERVICE_NAME = "StatusService";
|
||||
const HARDWARE_ALERT_COUNTER_START = 5;
|
||||
@@ -87,30 +88,6 @@ export class StatusService implements IStatusService {
|
||||
}
|
||||
};
|
||||
|
||||
private toCheckSnapshot = (check: Check): CheckSnapshot => {
|
||||
return {
|
||||
id: check.id,
|
||||
status: check.status,
|
||||
responseTime: check.responseTime,
|
||||
timings: check.timings,
|
||||
statusCode: check.statusCode,
|
||||
message: check.message,
|
||||
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,
|
||||
createdAt: check.createdAt,
|
||||
};
|
||||
};
|
||||
|
||||
private computeReachability = (
|
||||
currentStatus: MonitorStatus,
|
||||
window: Array<boolean>,
|
||||
@@ -207,7 +184,7 @@ export class StatusService implements IStatusService {
|
||||
await this.tryUpdateRunningStats(monitor, statusResponse);
|
||||
|
||||
const prevStatus = monitor.status;
|
||||
const checkSnapshot = this.toCheckSnapshot(check);
|
||||
const checkSnapshot = toCheckSnapshot(check);
|
||||
|
||||
// Project the window as it will look after updating DB
|
||||
// This is done because we need the updated status window to compute new status, but we don't
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IJobScheduler } from "@/worker/worker.interface.js";
|
||||
import { IJobsRepository } from "@/domain/jobs/job.repository.interface.js";
|
||||
import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js";
|
||||
import { Monitor, supportsGeoCheck } from "@/domain/monitors/monitor.type.js";
|
||||
import { Monitor, MonitorScheduleFields, supportsGeoCheck } from "@/domain/monitors/monitor.type.js";
|
||||
import { type Job, type JobSeed, type JobType, jobId } from "@/domain/jobs/job.type.js";
|
||||
import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js";
|
||||
import { WorkerJobsPagination, WorkerJobSummary, WorkerMetrics } from "@/worker/worker.interface.js";
|
||||
@@ -43,7 +43,7 @@ export class JobScheduler implements IJobScheduler {
|
||||
"cleanup-retention": POLL_MS,
|
||||
};
|
||||
// Helpers, builds job seeds. immediate=true runs on the next tick; otherwise jitter spreads the herd.
|
||||
protected toCheckJob = (monitor: Monitor, now: number, immediate = false): JobSeed => ({
|
||||
protected toCheckJob = (monitor: MonitorScheduleFields, now: number, immediate = false): JobSeed => ({
|
||||
id: jobId("check", monitor.id),
|
||||
type: "check",
|
||||
refId: monitor.id,
|
||||
@@ -52,7 +52,7 @@ export class JobScheduler implements IJobScheduler {
|
||||
intervalMs: monitor.interval,
|
||||
});
|
||||
|
||||
protected toGeoCheckJob = (monitor: Monitor, now: number, immediate = false): JobSeed => ({
|
||||
protected toGeoCheckJob = (monitor: MonitorScheduleFields, now: number, immediate = false): JobSeed => ({
|
||||
...this.toCheckJob(monitor, now, immediate),
|
||||
id: jobId("geo-check", monitor.id),
|
||||
type: "geo-check",
|
||||
@@ -187,7 +187,7 @@ export class JobScheduler implements IJobScheduler {
|
||||
// Seed queue
|
||||
protected reconcile = async () => {
|
||||
const now = Date.now();
|
||||
const monitors = await this.monitorsRepository.findAll();
|
||||
const monitors = await this.monitorsRepository.findAllForScheduling();
|
||||
for (const monitor of monitors) {
|
||||
await this.jobsRepository.upsertJob(this.toCheckJob(monitor, now));
|
||||
if (supportsGeoCheck(monitor.type) && monitor.geoCheckEnabled) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "../../src/domain/monitors/monitor.repository.interface.ts";
|
||||
import type { CheckSnapshot } from "../../src/domain/checks/check.type.ts";
|
||||
import type { Monitor, MonitorsSummary } from "../../src/domain/monitors/monitor.type.ts";
|
||||
import type { Monitor, MonitorScheduleFields, MonitorsSummary } from "../../src/domain/monitors/monitor.type.ts";
|
||||
|
||||
export class InMemoryMonitorsRepository implements IMonitorsRepository {
|
||||
private monitors: Monitor[] = [];
|
||||
@@ -24,8 +24,15 @@ export class InMemoryMonitorsRepository implements IMonitorsRepository {
|
||||
return { ...monitor };
|
||||
}
|
||||
|
||||
async findAll(): Promise<Monitor[]> {
|
||||
return this.monitors.map((m) => ({ ...m }));
|
||||
async findAllForScheduling(): Promise<MonitorScheduleFields[]> {
|
||||
return this.monitors.map((m) => ({
|
||||
id: m.id,
|
||||
type: m.type,
|
||||
isActive: m.isActive,
|
||||
interval: m.interval,
|
||||
geoCheckEnabled: m.geoCheckEnabled ?? false,
|
||||
geoCheckInterval: m.geoCheckInterval ?? 300000,
|
||||
}));
|
||||
}
|
||||
|
||||
async findByTeamId(_teamId: string, _config: TeamQueryConfig): Promise<Monitor[]> {
|
||||
@@ -45,10 +52,6 @@ export class InMemoryMonitorsRepository implements IMonitorsRepository {
|
||||
return monitor ? { ...monitor } : null;
|
||||
}
|
||||
|
||||
async findByIdsWithChecks(monitorIds: string[], _checksCount?: number): Promise<Monitor[]> {
|
||||
return this.findByIds(monitorIds);
|
||||
}
|
||||
|
||||
async updateById(monitorId: string, teamId: string, updates: Partial<Monitor>): Promise<Monitor> {
|
||||
const index = this.monitors.findIndex((m) => m.id === monitorId && m.teamId === teamId);
|
||||
if (index === -1) {
|
||||
|
||||
@@ -84,7 +84,7 @@ const createWorker = (overrides?: { queueMode?: QueueMode; queuePrimaryProcesses
|
||||
const monitorsRepository = {
|
||||
findByIds: jest.fn<any>().mockResolvedValue([makeMonitor()]),
|
||||
findByIdLean: jest.fn<any>().mockResolvedValue(makeMonitor()),
|
||||
findAll: jest.fn<any>().mockResolvedValue([]),
|
||||
findAllForScheduling: jest.fn<any>().mockResolvedValue([]),
|
||||
updateById: jest.fn<any>().mockResolvedValue({}),
|
||||
};
|
||||
const checksRepository = {
|
||||
@@ -183,7 +183,11 @@ describe("DBQueueWorker", () => {
|
||||
queueMode: "primary",
|
||||
queuePrimaryProcesses: false, // isolate reconcile from the polling loops
|
||||
mocks: {
|
||||
monitorsRepository: { findByIds: jest.fn<any>(), findAll: jest.fn<any>().mockResolvedValue([makeMonitor()]), updateById: jest.fn<any>() },
|
||||
monitorsRepository: {
|
||||
findByIds: jest.fn<any>(),
|
||||
findAllForScheduling: jest.fn<any>().mockResolvedValue([makeMonitor()]),
|
||||
updateById: jest.fn<any>(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -200,7 +204,11 @@ describe("DBQueueWorker", () => {
|
||||
queueMode: "primary",
|
||||
queuePrimaryProcesses: false,
|
||||
mocks: {
|
||||
monitorsRepository: { findByIds: jest.fn<any>(), findAll: jest.fn<any>().mockResolvedValue([geoMonitor]), updateById: jest.fn<any>() },
|
||||
monitorsRepository: {
|
||||
findByIds: jest.fn<any>(),
|
||||
findAllForScheduling: jest.fn<any>().mockResolvedValue([geoMonitor]),
|
||||
updateById: jest.fn<any>(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ const createScheduler = (overrides?: { queueMode?: QueueMode; mocks?: Record<str
|
||||
deleteById: jest.fn<any>().mockResolvedValue(undefined),
|
||||
};
|
||||
const monitorsRepository = {
|
||||
findAll: jest.fn<any>().mockResolvedValue([]),
|
||||
findAllForScheduling: jest.fn<any>().mockResolvedValue([]),
|
||||
};
|
||||
const logger = createMockLogger();
|
||||
const mocks = { jobsRepository, queueWorkersRepository, monitorsRepository, logger, ...overrides?.mocks };
|
||||
@@ -73,7 +73,7 @@ describe("JobScheduler", () => {
|
||||
it("primary mode reconciles: seeds a check row per monitor plus the two cleanup rows", async () => {
|
||||
const { scheduler, mocks } = createScheduler({
|
||||
queueMode: "primary",
|
||||
mocks: { monitorsRepository: { findAll: jest.fn<any>().mockResolvedValue([makeMonitor()]) } },
|
||||
mocks: { monitorsRepository: { findAllForScheduling: jest.fn<any>().mockResolvedValue([makeMonitor()]) } },
|
||||
});
|
||||
await scheduler.init();
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("JobScheduler", () => {
|
||||
it("primary mode also seeds a geo-check row for geo-enabled monitors", async () => {
|
||||
const { scheduler, mocks } = createScheduler({
|
||||
queueMode: "primary",
|
||||
mocks: { monitorsRepository: { findAll: jest.fn<any>().mockResolvedValue([makeMonitor({ geoCheckEnabled: true })]) } },
|
||||
mocks: { monitorsRepository: { findAllForScheduling: jest.fn<any>().mockResolvedValue([makeMonitor({ geoCheckEnabled: true })]) } },
|
||||
});
|
||||
await scheduler.init();
|
||||
|
||||
@@ -98,12 +98,12 @@ describe("JobScheduler", () => {
|
||||
it("worker mode heartbeats but does not reconcile (no monitor reads, no seeds)", async () => {
|
||||
const { scheduler, mocks } = createScheduler({
|
||||
queueMode: "worker",
|
||||
mocks: { monitorsRepository: { findAll: jest.fn<any>().mockResolvedValue([makeMonitor()]) } },
|
||||
mocks: { monitorsRepository: { findAllForScheduling: jest.fn<any>().mockResolvedValue([makeMonitor()]) } },
|
||||
});
|
||||
await scheduler.init();
|
||||
|
||||
expect(mocks.queueWorkersRepository.upsert).toHaveBeenCalledWith("worker-1", "worker", false);
|
||||
expect(mocks.monitorsRepository.findAll).not.toHaveBeenCalled();
|
||||
expect(mocks.monitorsRepository.findAllForScheduling).not.toHaveBeenCalled();
|
||||
expect(mocks.jobsRepository.upsertJob).not.toHaveBeenCalled();
|
||||
expect(mocks.jobsRepository.upsertCleanupJob).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("MaintenanceWindowService", () => {
|
||||
|
||||
await service.createMaintenanceWindow(defaultCreateParams);
|
||||
|
||||
expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["mon-1", "mon-2"]);
|
||||
expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], { includeRecentChecks: false });
|
||||
});
|
||||
|
||||
it("throws 403 when a monitor belongs to a different team", async () => {
|
||||
@@ -404,7 +404,7 @@ describe("MaintenanceWindowService", () => {
|
||||
body: { monitors: ["mon-1", "mon-2"] },
|
||||
});
|
||||
|
||||
expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["mon-1", "mon-2"]);
|
||||
expect(monitorsRepository.findByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], { includeRecentChecks: false });
|
||||
});
|
||||
|
||||
it("throws 403 when a new monitor belongs to a different team", async () => {
|
||||
|
||||
@@ -583,7 +583,7 @@ describe("MonitorService", () => {
|
||||
expect(result.monitors).toEqual([]);
|
||||
});
|
||||
|
||||
it("uses snapshot (first check only) for hardware type monitors", async () => {
|
||||
it("uses snapshot (latest check only) for hardware type monitors", async () => {
|
||||
const monitorsRepository = createMonitorsRepositoryMock();
|
||||
(monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ totalMonitors: 1 });
|
||||
(monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1);
|
||||
@@ -602,6 +602,7 @@ describe("MonitorService", () => {
|
||||
const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID, type: "hardware" });
|
||||
|
||||
expect(result.monitors[0].recentChecks).toHaveLength(1);
|
||||
expect(result.monitors[0].recentChecks[0].responseTime).toBe(30);
|
||||
});
|
||||
|
||||
it("uses snapshot for hardware type when type is array with hardware", async () => {
|
||||
@@ -622,6 +623,7 @@ describe("MonitorService", () => {
|
||||
const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID, type: ["hardware"] });
|
||||
|
||||
expect(result.monitors[0].recentChecks).toHaveLength(1);
|
||||
expect(result.monitors[0].recentChecks[0].responseTime).toBe(20);
|
||||
});
|
||||
|
||||
it("normalizes checks for non-snapshot types when type is an array", async () => {
|
||||
|
||||
@@ -472,7 +472,7 @@ describe("UserService", () => {
|
||||
|
||||
await service.deleteUser({ userId: "user-1", teamId: "team-1", roles: ["superadmin"] });
|
||||
|
||||
expect(monitorsRepository.findByTeamId).toHaveBeenCalledWith("team-1", {});
|
||||
expect(monitorsRepository.findByTeamId).toHaveBeenCalledWith("team-1", {}, { includeRecentChecks: false });
|
||||
expect(scheduler.deleteJob).toHaveBeenCalledTimes(2);
|
||||
expect(usersRepository.deleteById).toHaveBeenCalledWith("user-1");
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user