checks repo, barrel export
This commit is contained in:
1 parent
07957d3a1f
commit
62e86434c2
2 files changed
+887
-1
No files matched your search
@@ -0,0 +1,873 @@
|
||||
import type { Pool } from "pg";
|
||||
import type { IChecksRepository } from "@/repositories/checks/IChecksRepository.js";
|
||||
import type {
|
||||
Check,
|
||||
CheckAudits,
|
||||
CheckCaptureInfo,
|
||||
CheckCpuInfo,
|
||||
CheckDiskInfo,
|
||||
CheckErrorInfo,
|
||||
CheckHostInfo,
|
||||
CheckMemoryInfo,
|
||||
CheckNetworkInterfaceInfo,
|
||||
ChecksQueryResult,
|
||||
ChecksSummary,
|
||||
MonitorType,
|
||||
PageSpeedChecksResult,
|
||||
HardwareChecksResult,
|
||||
UptimeChecksResult,
|
||||
GotTimings,
|
||||
} from "@/types/index.js";
|
||||
import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
|
||||
import { getDateForRange } from "@/utils/dataUtils.js";
|
||||
|
||||
// Map MongoDB dateString formats to TimescaleDB time_bucket intervals
|
||||
const dateStringToBucket = (dateString: string): string => {
|
||||
if (dateString.includes("%M")) return "1 minute";
|
||||
if (dateString.includes("%H")) return "1 hour";
|
||||
return "1 day";
|
||||
};
|
||||
|
||||
export class TimescaleChecksRepository implements IChecksRepository {
|
||||
constructor(private pool: Pool) {}
|
||||
|
||||
create = async (check: Check): Promise<Check> => {
|
||||
const row = await this.insertCheck(check);
|
||||
if (!row) {
|
||||
throw new Error("Failed to create check");
|
||||
}
|
||||
|
||||
// Insert child records
|
||||
if (check.disk?.length) {
|
||||
await this.insertDisks(row.id, row.created_at, check.disk);
|
||||
}
|
||||
if (check.net?.length) {
|
||||
await this.insertNetworkInterfaces(row.id, row.created_at, check.net);
|
||||
}
|
||||
if (check.errors?.length) {
|
||||
await this.insertErrors(row.id, row.created_at, check.errors);
|
||||
}
|
||||
|
||||
return this.toEntity(row, check.disk, check.errors, check.net);
|
||||
};
|
||||
|
||||
createChecks = async (checks: Check[]): Promise<Check[]> => {
|
||||
const results: Check[] = [];
|
||||
for (const check of checks) {
|
||||
results.push(await this.create(check));
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
findByMonitorId = async (
|
||||
monitorId: string,
|
||||
sortOrder: string,
|
||||
dateRange: string,
|
||||
filter: string | undefined,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
status: boolean | undefined
|
||||
): Promise<ChecksQueryResult> => {
|
||||
const { conditions, values, paramIndex } = this.buildCheckFilters({ monitorId, dateRange, filter, status }, "monitor_id");
|
||||
|
||||
const direction = sortOrder === "asc" ? "ASC" : "DESC";
|
||||
const offset = page && rowsPerPage ? page * rowsPerPage : 0;
|
||||
|
||||
const [countResult, checksResult] = await Promise.all([
|
||||
this.pool.query(`SELECT COUNT(*)::int AS count FROM checks WHERE ${conditions.join(" AND ")}`, values),
|
||||
this.pool.query(
|
||||
`SELECT * FROM checks WHERE ${conditions.join(" AND ")} ORDER BY created_at ${direction} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
||||
[...values, rowsPerPage, offset]
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
checksCount: countResult.rows[0].count,
|
||||
checks: await this.populateChildData(checksResult.rows),
|
||||
};
|
||||
};
|
||||
|
||||
findByTeamId = async (
|
||||
sortOrder: string,
|
||||
dateRange: string,
|
||||
filter: string | undefined,
|
||||
page: number,
|
||||
rowsPerPage: number,
|
||||
teamId: string
|
||||
): Promise<ChecksQueryResult> => {
|
||||
const { conditions, values, paramIndex } = this.buildCheckFilters({ teamId, dateRange, filter }, "team_id");
|
||||
|
||||
const direction = sortOrder === "asc" ? "ASC" : "DESC";
|
||||
const offset = page && rowsPerPage ? page * rowsPerPage : 0;
|
||||
|
||||
const [countResult, checksResult] = await Promise.all([
|
||||
this.pool.query(`SELECT COUNT(*)::int AS count FROM checks WHERE ${conditions.join(" AND ")}`, values),
|
||||
this.pool.query(
|
||||
`SELECT * FROM checks WHERE ${conditions.join(" AND ")} ORDER BY created_at ${direction} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
||||
[...values, rowsPerPage, offset]
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
checksCount: countResult.rows[0].count,
|
||||
checks: await this.populateChildData(checksResult.rows),
|
||||
};
|
||||
};
|
||||
|
||||
findLatestByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap> => {
|
||||
if (!monitorIds.length) {
|
||||
return {};
|
||||
}
|
||||
const limitPerMonitor = options?.limitPerMonitor ?? 25;
|
||||
const dateFilter = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const allResult = await this.pool.query(
|
||||
`SELECT * FROM (
|
||||
SELECT *, ROW_NUMBER() OVER (PARTITION BY monitor_id ORDER BY created_at DESC) AS rn
|
||||
FROM checks
|
||||
WHERE monitor_id = ANY($1) AND created_at >= $2
|
||||
) sub
|
||||
WHERE rn <= $3
|
||||
ORDER BY monitor_id, created_at DESC`,
|
||||
[monitorIds, dateFilter, limitPerMonitor]
|
||||
);
|
||||
|
||||
const checks = await this.populateChildData(allResult.rows);
|
||||
|
||||
const mapped: LatestChecksMap = {};
|
||||
for (let i = 0; i < allResult.rows.length; i++) {
|
||||
const mid = allResult.rows[i].monitor_id;
|
||||
if (!mapped[mid]) {
|
||||
mapped[mid] = [];
|
||||
}
|
||||
mapped[mid].push(checks[i]);
|
||||
}
|
||||
// Ensure all requested IDs have an entry
|
||||
for (const id of monitorIds) {
|
||||
if (!mapped[id]) {
|
||||
mapped[id] = [];
|
||||
}
|
||||
}
|
||||
return mapped;
|
||||
};
|
||||
|
||||
findByDateRangeAndMonitorId = async (
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string,
|
||||
options?: { type?: MonitorType }
|
||||
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult> => {
|
||||
if (options?.type === "hardware") {
|
||||
return this.findHardwareDateRangeChecks(monitorId, startDate, endDate, dateString);
|
||||
}
|
||||
if (options?.type === "pagespeed") {
|
||||
return this.findPageSpeedDateRangeChecks(monitorId, startDate, endDate, dateString);
|
||||
}
|
||||
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorId, startDate, endDate, dateString);
|
||||
};
|
||||
|
||||
findSummaryByTeamId = async (teamId: string, dateRange: string): Promise<ChecksSummary> => {
|
||||
const rangeDate = getDateForRange(dateRange);
|
||||
const conditions: string[] = ["team_id = $1"];
|
||||
const values: unknown[] = [teamId];
|
||||
|
||||
if (rangeDate) {
|
||||
conditions.push("created_at >= $2");
|
||||
values.push(rangeDate);
|
||||
}
|
||||
|
||||
const result = await this.pool.query(
|
||||
`SELECT
|
||||
COUNT(*)::int AS "totalChecks",
|
||||
COUNT(*) FILTER (WHERE status = FALSE)::int AS "downChecks"
|
||||
FROM checks WHERE ${conditions.join(" AND ")}`,
|
||||
values
|
||||
);
|
||||
|
||||
return result.rows[0] ?? { totalChecks: 0, downChecks: 0 };
|
||||
};
|
||||
|
||||
deleteByMonitorId = async (monitorId: string): Promise<number> => {
|
||||
const result = await this.pool.query(`DELETE FROM checks WHERE monitor_id = $1`, [monitorId]);
|
||||
return result.rowCount ?? 0;
|
||||
};
|
||||
|
||||
deleteByTeamId = async (teamId: string): Promise<number> => {
|
||||
const result = await this.pool.query(`DELETE FROM checks WHERE team_id = $1`, [teamId]);
|
||||
return result.rowCount ?? 0;
|
||||
};
|
||||
|
||||
deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise<number> => {
|
||||
if (!monitorIds.length) {
|
||||
const result = await this.pool.query(`DELETE FROM checks`);
|
||||
return result.rowCount ?? 0;
|
||||
}
|
||||
const result = await this.pool.query(`DELETE FROM checks WHERE monitor_id != ALL($1)`, [monitorIds]);
|
||||
return result.rowCount ?? 0;
|
||||
};
|
||||
|
||||
deleteOlderThan = async (date: Date): Promise<number> => {
|
||||
// TimescaleDB drop_chunks is O(1) per chunk, much faster than row-by-row DELETE
|
||||
const result = await this.pool.query(`DELETE FROM checks WHERE created_at < $1`, [date]);
|
||||
return result.rowCount ?? 0;
|
||||
};
|
||||
|
||||
// --- Private helpers ---
|
||||
|
||||
private populateChildData = async (checkRows: Record<string, unknown>[]): Promise<Check[]> => {
|
||||
if (!checkRows.length) return [];
|
||||
|
||||
const checkIds = checkRows.map((r) => r.id as string);
|
||||
|
||||
const [disksResult, netsResult, errsResult] = await Promise.all([
|
||||
this.pool.query(
|
||||
`SELECT check_id, device, mountpoint, total_bytes, free_bytes, used_bytes, usage_percent,
|
||||
total_inodes, free_inodes, used_inodes, inodes_usage_percent,
|
||||
read_bytes, write_bytes, read_time, write_time
|
||||
FROM check_disks WHERE check_id = ANY($1)`,
|
||||
[checkIds]
|
||||
),
|
||||
this.pool.query(
|
||||
`SELECT check_id, name, bytes_sent, bytes_recv, packets_sent, packets_recv,
|
||||
err_in, err_out, drop_in, drop_out, fifo_in, fifo_out
|
||||
FROM check_network_interfaces WHERE check_id = ANY($1)`,
|
||||
[checkIds]
|
||||
),
|
||||
this.pool.query(
|
||||
`SELECT check_id, metrics, error
|
||||
FROM check_errors WHERE check_id = ANY($1)`,
|
||||
[checkIds]
|
||||
),
|
||||
]);
|
||||
|
||||
const diskMap = new Map<string, CheckDiskInfo[]>();
|
||||
for (const d of disksResult.rows) {
|
||||
const key = d.check_id as string;
|
||||
if (!diskMap.has(key)) diskMap.set(key, []);
|
||||
diskMap.get(key)!.push({
|
||||
device: d.device,
|
||||
mountpoint: d.mountpoint,
|
||||
total_bytes: Number(d.total_bytes),
|
||||
free_bytes: Number(d.free_bytes),
|
||||
used_bytes: Number(d.used_bytes),
|
||||
usage_percent: d.usage_percent,
|
||||
total_inodes: Number(d.total_inodes),
|
||||
free_inodes: Number(d.free_inodes),
|
||||
used_inodes: Number(d.used_inodes),
|
||||
inodes_usage_percent: d.inodes_usage_percent,
|
||||
read_bytes: Number(d.read_bytes),
|
||||
write_bytes: Number(d.write_bytes),
|
||||
read_time: Number(d.read_time),
|
||||
write_time: Number(d.write_time),
|
||||
});
|
||||
}
|
||||
|
||||
const netMap = new Map<string, CheckNetworkInterfaceInfo[]>();
|
||||
for (const n of netsResult.rows) {
|
||||
const key = n.check_id as string;
|
||||
if (!netMap.has(key)) netMap.set(key, []);
|
||||
netMap.get(key)!.push({
|
||||
name: n.name,
|
||||
bytes_sent: Number(n.bytes_sent),
|
||||
bytes_recv: Number(n.bytes_recv),
|
||||
packets_sent: Number(n.packets_sent),
|
||||
packets_recv: Number(n.packets_recv),
|
||||
err_in: Number(n.err_in),
|
||||
err_out: Number(n.err_out),
|
||||
drop_in: Number(n.drop_in),
|
||||
drop_out: Number(n.drop_out),
|
||||
fifo_in: Number(n.fifo_in),
|
||||
fifo_out: Number(n.fifo_out),
|
||||
});
|
||||
}
|
||||
|
||||
const errMap = new Map<string, CheckErrorInfo[]>();
|
||||
for (const e of errsResult.rows) {
|
||||
const key = e.check_id as string;
|
||||
if (!errMap.has(key)) errMap.set(key, []);
|
||||
errMap.get(key)!.push({ metric: e.metrics ?? [], err: e.error ?? "" });
|
||||
}
|
||||
|
||||
return checkRows.map((row) => {
|
||||
const id = row.id as string;
|
||||
return this.toEntity(row, diskMap.get(id) ?? [], errMap.get(id) ?? [], netMap.get(id) ?? []);
|
||||
});
|
||||
};
|
||||
|
||||
private insertCheck = async (check: Check) => {
|
||||
const result = await this.pool.query(
|
||||
`INSERT INTO checks (
|
||||
monitor_id, team_id, monitor_type, status, response_time, status_code, message,
|
||||
timing_start, timing_socket, timing_lookup, timing_connect, timing_secure_connect,
|
||||
timing_upload, timing_response, timing_end,
|
||||
phase_wait, phase_dns, phase_tcp, phase_tls, phase_request, phase_first_byte, phase_download, phase_total,
|
||||
cpu_physical_core, cpu_logical_core, cpu_frequency, cpu_current_frequency, cpu_temperature,
|
||||
cpu_free_percent, cpu_usage_percent,
|
||||
mem_total_bytes, mem_available_bytes, mem_used_bytes, mem_usage_percent,
|
||||
host_os, host_platform, host_kernel_version, host_pretty_name,
|
||||
capture_version, capture_mode,
|
||||
lighthouse_performance, lighthouse_accessibility, lighthouse_best_practices, lighthouse_seo,
|
||||
audit_cls_score, audit_cls_value, audit_cls_display,
|
||||
audit_si_score, audit_si_value, audit_si_display,
|
||||
audit_fcp_score, audit_fcp_value, audit_fcp_display,
|
||||
audit_lcp_score, audit_lcp_value, audit_lcp_display,
|
||||
audit_tbt_score, audit_tbt_value, audit_tbt_display
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,
|
||||
$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,
|
||||
$45,$46,$47,$48,$49,$50,$51,$52,$53,$54,$55,$56,$57,$58,$59
|
||||
) RETURNING *`,
|
||||
[
|
||||
check.metadata.monitorId,
|
||||
check.metadata.teamId,
|
||||
check.metadata.type,
|
||||
check.status,
|
||||
check.responseTime,
|
||||
check.statusCode,
|
||||
check.message,
|
||||
check.timings?.start ?? null,
|
||||
check.timings?.socket ?? null,
|
||||
check.timings?.lookup ?? null,
|
||||
check.timings?.connect ?? null,
|
||||
check.timings?.secureConnect ?? null,
|
||||
check.timings?.upload ?? null,
|
||||
check.timings?.response ?? null,
|
||||
check.timings?.end ?? null,
|
||||
check.timings?.phases?.wait ?? null,
|
||||
check.timings?.phases?.dns ?? null,
|
||||
check.timings?.phases?.tcp ?? null,
|
||||
check.timings?.phases?.tls ?? null,
|
||||
check.timings?.phases?.request ?? null,
|
||||
check.timings?.phases?.firstByte ?? null,
|
||||
check.timings?.phases?.download ?? null,
|
||||
check.timings?.phases?.total ?? null,
|
||||
check.cpu?.physical_core ?? null,
|
||||
check.cpu?.logical_core ?? null,
|
||||
check.cpu?.frequency ?? null,
|
||||
check.cpu?.current_frequency ?? null,
|
||||
check.cpu?.temperature ?? null,
|
||||
check.cpu?.free_percent ?? null,
|
||||
check.cpu?.usage_percent ?? null,
|
||||
check.memory?.total_bytes ?? null,
|
||||
check.memory?.available_bytes ?? null,
|
||||
check.memory?.used_bytes ?? null,
|
||||
check.memory?.usage_percent ?? null,
|
||||
check.host?.os ?? null,
|
||||
check.host?.platform ?? null,
|
||||
check.host?.kernel_version ?? null,
|
||||
check.host?.pretty_name ?? null,
|
||||
check.capture?.version ?? null,
|
||||
check.capture?.mode ?? null,
|
||||
check.performance ?? null,
|
||||
check.accessibility ?? null,
|
||||
check.bestPractices ?? null,
|
||||
check.seo ?? null,
|
||||
check.audits?.cls?.score ?? null,
|
||||
check.audits?.cls?.numericValue ?? null,
|
||||
check.audits?.cls?.displayValue ?? null,
|
||||
check.audits?.si?.score ?? null,
|
||||
check.audits?.si?.numericValue ?? null,
|
||||
check.audits?.si?.displayValue ?? null,
|
||||
check.audits?.fcp?.score ?? null,
|
||||
check.audits?.fcp?.numericValue ?? null,
|
||||
check.audits?.fcp?.displayValue ?? null,
|
||||
check.audits?.lcp?.score ?? null,
|
||||
check.audits?.lcp?.numericValue ?? null,
|
||||
check.audits?.lcp?.displayValue ?? null,
|
||||
check.audits?.tbt?.score ?? null,
|
||||
check.audits?.tbt?.numericValue ?? null,
|
||||
check.audits?.tbt?.displayValue ?? null,
|
||||
]
|
||||
);
|
||||
return result.rows[0];
|
||||
};
|
||||
|
||||
private insertDisks = async (checkId: string, checkCreatedAt: Date, disks: CheckDiskInfo[]) => {
|
||||
for (const disk of disks) {
|
||||
await this.pool.query(
|
||||
`INSERT INTO check_disks (check_id, check_created_at, device, mountpoint, total_bytes, free_bytes, used_bytes, usage_percent,
|
||||
total_inodes, free_inodes, used_inodes, inodes_usage_percent, read_bytes, write_bytes, read_time, write_time)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`,
|
||||
[
|
||||
checkId,
|
||||
checkCreatedAt,
|
||||
disk.device,
|
||||
disk.mountpoint,
|
||||
disk.total_bytes,
|
||||
disk.free_bytes,
|
||||
disk.used_bytes,
|
||||
disk.usage_percent,
|
||||
disk.total_inodes,
|
||||
disk.free_inodes,
|
||||
disk.used_inodes,
|
||||
disk.inodes_usage_percent,
|
||||
disk.read_bytes,
|
||||
disk.write_bytes,
|
||||
disk.read_time,
|
||||
disk.write_time,
|
||||
]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private insertNetworkInterfaces = async (checkId: string, checkCreatedAt: Date, nets: CheckNetworkInterfaceInfo[]) => {
|
||||
for (const net of nets) {
|
||||
await this.pool.query(
|
||||
`INSERT INTO check_network_interfaces (check_id, check_created_at, name, bytes_sent, bytes_recv, packets_sent, packets_recv,
|
||||
err_in, err_out, drop_in, drop_out, fifo_in, fifo_out)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`,
|
||||
[
|
||||
checkId,
|
||||
checkCreatedAt,
|
||||
net.name,
|
||||
net.bytes_sent,
|
||||
net.bytes_recv,
|
||||
net.packets_sent,
|
||||
net.packets_recv,
|
||||
net.err_in,
|
||||
net.err_out,
|
||||
net.drop_in,
|
||||
net.drop_out,
|
||||
net.fifo_in,
|
||||
net.fifo_out,
|
||||
]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private insertErrors = async (checkId: string, checkCreatedAt: Date, errors: CheckErrorInfo[]) => {
|
||||
for (const err of errors) {
|
||||
await this.pool.query(
|
||||
`INSERT INTO check_errors (check_id, check_created_at, metrics, error)
|
||||
VALUES ($1,$2,$3,$4)`,
|
||||
[checkId, checkCreatedAt, err.metric, err.err]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
private buildCheckFilters = (
|
||||
opts: { monitorId?: string; teamId?: string; dateRange?: string; filter?: string; status?: boolean },
|
||||
idColumn: string
|
||||
) => {
|
||||
const conditions: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (opts.monitorId) {
|
||||
conditions.push(`${idColumn} = $${paramIndex++}`);
|
||||
values.push(opts.monitorId);
|
||||
} else if (opts.teamId) {
|
||||
conditions.push(`${idColumn} = $${paramIndex++}`);
|
||||
values.push(opts.teamId);
|
||||
}
|
||||
|
||||
if (opts.dateRange) {
|
||||
const rangeDate = getDateForRange(opts.dateRange);
|
||||
if (rangeDate) {
|
||||
conditions.push(`created_at >= $${paramIndex++}`);
|
||||
values.push(rangeDate);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter overwrites status (matching Mongo behavior where filter sets matchStage.status)
|
||||
let statusApplied = false;
|
||||
if (opts.filter !== undefined) {
|
||||
switch (opts.filter) {
|
||||
case "all":
|
||||
break;
|
||||
case "up":
|
||||
conditions.push(`status = TRUE`);
|
||||
statusApplied = true;
|
||||
break;
|
||||
case "down":
|
||||
conditions.push(`status = FALSE`);
|
||||
statusApplied = true;
|
||||
break;
|
||||
case "resolve":
|
||||
conditions.push(`status = FALSE`);
|
||||
conditions.push(`status_code = 5000`);
|
||||
statusApplied = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!statusApplied && opts.status !== undefined) {
|
||||
conditions.push(`status = $${paramIndex++}`);
|
||||
values.push(opts.status);
|
||||
}
|
||||
|
||||
return { conditions, values, paramIndex };
|
||||
};
|
||||
|
||||
private findUptimeDateRangeChecks = async (
|
||||
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
): Promise<UptimeChecksResult> => {
|
||||
const bucket = dateStringToBucket(dateString);
|
||||
|
||||
const [uptimeResult, groupedResult, upResult, downResult] = await Promise.all([
|
||||
this.pool.query(
|
||||
`SELECT
|
||||
COUNT(*) FILTER (WHERE status = TRUE) AS up_checks,
|
||||
COUNT(*) AS total_checks,
|
||||
AVG(response_time) AS avg_response_time
|
||||
FROM checks WHERE monitor_id = $1 AND created_at >= $2 AND created_at <= $3`,
|
||||
[monitorId, startDate, endDate]
|
||||
),
|
||||
this.pool.query(
|
||||
`SELECT
|
||||
time_bucket($1::interval, created_at) AS bucket_date,
|
||||
AVG(response_time) AS "avgResponseTime",
|
||||
COUNT(*)::int AS "totalChecks"
|
||||
FROM checks WHERE monitor_id = $2 AND created_at >= $3 AND created_at <= $4
|
||||
GROUP BY bucket_date ORDER BY bucket_date`,
|
||||
[bucket, monitorId, startDate, endDate]
|
||||
),
|
||||
this.pool.query(
|
||||
`SELECT
|
||||
time_bucket($1::interval, created_at) AS bucket_date,
|
||||
AVG(response_time) AS "avgResponseTime",
|
||||
COUNT(*)::int AS "totalChecks"
|
||||
FROM checks WHERE monitor_id = $2 AND created_at >= $3 AND created_at <= $4 AND status = TRUE
|
||||
GROUP BY bucket_date ORDER BY bucket_date`,
|
||||
[bucket, monitorId, startDate, endDate]
|
||||
),
|
||||
this.pool.query(
|
||||
`SELECT
|
||||
time_bucket($1::interval, created_at) AS bucket_date,
|
||||
AVG(response_time) AS "avgResponseTime",
|
||||
COUNT(*)::int AS "totalChecks"
|
||||
FROM checks WHERE monitor_id = $2 AND created_at >= $3 AND created_at <= $4 AND status = FALSE
|
||||
GROUP BY bucket_date ORDER BY bucket_date`,
|
||||
[bucket, monitorId, startDate, endDate]
|
||||
),
|
||||
]);
|
||||
|
||||
const stats = uptimeResult.rows[0];
|
||||
const totalChecks = Number(stats?.total_checks ?? 0);
|
||||
const upChecks = Number(stats?.up_checks ?? 0);
|
||||
|
||||
const formatGrouped = (rows: Array<Record<string, unknown>>) =>
|
||||
rows.map((row) => ({
|
||||
bucketDate: (row.bucket_date as Date).toISOString(),
|
||||
avgResponseTime: Number(row.avgResponseTime ?? 0),
|
||||
totalChecks: Number(row.totalChecks ?? 0),
|
||||
}));
|
||||
|
||||
return {
|
||||
monitorType,
|
||||
groupedChecks: formatGrouped(groupedResult.rows),
|
||||
groupedUpChecks: formatGrouped(upResult.rows),
|
||||
groupedDownChecks: formatGrouped(downResult.rows),
|
||||
uptimePercentage: totalChecks > 0 ? upChecks / totalChecks : 0,
|
||||
avgResponseTime: Number(stats?.avg_response_time ?? 0),
|
||||
};
|
||||
};
|
||||
|
||||
private findHardwareDateRangeChecks = async (
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
): Promise<HardwareChecksResult> => {
|
||||
const bucket = dateStringToBucket(dateString);
|
||||
|
||||
const [totalResult, upResult, metricsResult] = await Promise.all([
|
||||
this.pool.query(
|
||||
`SELECT COUNT(*)::int AS count FROM checks
|
||||
WHERE monitor_id = $1 AND monitor_type = 'hardware' AND created_at >= $2 AND created_at <= $3`,
|
||||
[monitorId, startDate, endDate]
|
||||
),
|
||||
this.pool.query(
|
||||
`SELECT COUNT(*)::int AS count FROM checks
|
||||
WHERE monitor_id = $1 AND monitor_type = 'hardware' AND created_at >= $2 AND created_at <= $3 AND status = TRUE`,
|
||||
[monitorId, startDate, endDate]
|
||||
),
|
||||
this.pool.query(
|
||||
`WITH bucketed AS (
|
||||
SELECT
|
||||
time_bucket($1::interval, c.created_at) AS bucket_date,
|
||||
c.cpu_usage_percent,
|
||||
c.mem_usage_percent,
|
||||
c.cpu_temperature
|
||||
FROM checks c
|
||||
WHERE c.monitor_id = $2 AND c.monitor_type = 'hardware' AND c.created_at >= $3 AND c.created_at <= $4
|
||||
),
|
||||
temp_avg AS (
|
||||
SELECT bucket_date, idx, AVG(val) AS avg_val
|
||||
FROM bucketed, unnest(cpu_temperature) WITH ORDINALITY AS t(val, idx)
|
||||
GROUP BY bucket_date, idx
|
||||
)
|
||||
SELECT
|
||||
b.bucket_date,
|
||||
AVG(b.cpu_usage_percent) AS avg_cpu,
|
||||
AVG(b.mem_usage_percent) AS avg_memory,
|
||||
COALESCE(
|
||||
(SELECT array_agg(ta.avg_val ORDER BY ta.idx) FROM temp_avg ta WHERE ta.bucket_date = b.bucket_date),
|
||||
ARRAY[]::double precision[]
|
||||
) AS avg_temperature
|
||||
FROM bucketed b
|
||||
GROUP BY b.bucket_date
|
||||
ORDER BY b.bucket_date`,
|
||||
[bucket, monitorId, startDate, endDate]
|
||||
),
|
||||
]);
|
||||
|
||||
// Fetch disk and net stats per bucket
|
||||
const checks = await Promise.all(
|
||||
metricsResult.rows.map(async (row) => {
|
||||
const bucketStart = row.bucket_date as Date;
|
||||
const bucketInterval = bucket;
|
||||
|
||||
const [diskResult, netResult] = await Promise.all([
|
||||
this.pool.query(
|
||||
`SELECT d.device AS name,
|
||||
AVG(d.read_bytes) AS "readSpeed",
|
||||
AVG(d.write_bytes) AS "writeSpeed",
|
||||
AVG(d.total_bytes) AS "totalBytes",
|
||||
AVG(d.free_bytes) AS "freeBytes",
|
||||
AVG(d.usage_percent) AS "usagePercent"
|
||||
FROM check_disks d
|
||||
WHERE d.check_created_at >= $1 AND d.check_created_at < $1::timestamptz + $2::interval
|
||||
AND d.check_id IN (SELECT id FROM checks WHERE monitor_id = $3 AND monitor_type = 'hardware' AND created_at >= $1 AND created_at < $1::timestamptz + $2::interval)
|
||||
GROUP BY d.device`,
|
||||
[bucketStart, bucketInterval, monitorId]
|
||||
),
|
||||
this.pool.query(
|
||||
`WITH bounded AS (
|
||||
SELECT n.*, c.created_at AS check_time,
|
||||
FIRST_VALUE(n.bytes_sent) OVER w AS first_bytes_sent,
|
||||
LAST_VALUE(n.bytes_sent) OVER w AS last_bytes_sent,
|
||||
FIRST_VALUE(n.bytes_recv) OVER w AS first_bytes_recv,
|
||||
LAST_VALUE(n.bytes_recv) OVER w AS last_bytes_recv,
|
||||
FIRST_VALUE(n.packets_sent) OVER w AS first_packets_sent,
|
||||
LAST_VALUE(n.packets_sent) OVER w AS last_packets_sent,
|
||||
FIRST_VALUE(n.packets_recv) OVER w AS first_packets_recv,
|
||||
LAST_VALUE(n.packets_recv) OVER w AS last_packets_recv,
|
||||
FIRST_VALUE(n.err_in) OVER w AS first_err_in,
|
||||
LAST_VALUE(n.err_in) OVER w AS last_err_in,
|
||||
FIRST_VALUE(n.err_out) OVER w AS first_err_out,
|
||||
LAST_VALUE(n.err_out) OVER w AS last_err_out,
|
||||
FIRST_VALUE(n.drop_in) OVER w AS first_drop_in,
|
||||
LAST_VALUE(n.drop_in) OVER w AS last_drop_in,
|
||||
FIRST_VALUE(n.drop_out) OVER w AS first_drop_out,
|
||||
LAST_VALUE(n.drop_out) OVER w AS last_drop_out,
|
||||
FIRST_VALUE(n.fifo_in) OVER w AS first_fifo_in,
|
||||
LAST_VALUE(n.fifo_in) OVER w AS last_fifo_in,
|
||||
FIRST_VALUE(n.fifo_out) OVER w AS first_fifo_out,
|
||||
LAST_VALUE(n.fifo_out) OVER w AS last_fifo_out,
|
||||
FIRST_VALUE(c.created_at) OVER w AS first_time,
|
||||
LAST_VALUE(c.created_at) OVER w AS last_time
|
||||
FROM check_network_interfaces n
|
||||
JOIN checks c ON c.id = n.check_id
|
||||
WHERE n.check_created_at >= $1 AND n.check_created_at < $1::timestamptz + $2::interval
|
||||
AND c.monitor_id = $3 AND c.monitor_type = 'hardware'
|
||||
WINDOW w AS (PARTITION BY n.name ORDER BY c.created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
|
||||
)
|
||||
SELECT DISTINCT ON (name) name,
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_bytes_sent - first_bytes_sent) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "bytesSentPerSecond",
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_bytes_recv - first_bytes_recv) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "deltaBytesRecv",
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_packets_sent - first_packets_sent) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "deltaPacketsSent",
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_packets_recv - first_packets_recv) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "deltaPacketsRecv",
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_err_in - first_err_in) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "deltaErrIn",
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_err_out - first_err_out) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "deltaErrOut",
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_drop_in - first_drop_in) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "deltaDropIn",
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_drop_out - first_drop_out) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "deltaDropOut",
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_fifo_in - first_fifo_in) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "deltaFifoIn",
|
||||
CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
|
||||
THEN (last_fifo_out - first_fifo_out) / EXTRACT(EPOCH FROM (last_time - first_time))
|
||||
ELSE 0 END AS "deltaFifoOut"
|
||||
FROM bounded`,
|
||||
[bucketStart, bucketInterval, monitorId]
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
bucketDate: bucketStart.toISOString(),
|
||||
avgCpuUsage: Number(row.avg_cpu ?? 0),
|
||||
avgMemoryUsage: Number(row.avg_memory ?? 0),
|
||||
avgTemperature: Array.isArray(row.avg_temperature) ? row.avg_temperature.map(Number) : [],
|
||||
disks: diskResult.rows.map((d) => ({
|
||||
name: d.name ?? "",
|
||||
readSpeed: Number(d.readSpeed ?? 0),
|
||||
writeSpeed: Number(d.writeSpeed ?? 0),
|
||||
totalBytes: Number(d.totalBytes ?? 0),
|
||||
freeBytes: Number(d.freeBytes ?? 0),
|
||||
usagePercent: Number(d.usagePercent ?? 0),
|
||||
})),
|
||||
net: netResult.rows.map((n) => ({
|
||||
name: n.name ?? "",
|
||||
bytesSentPerSecond: Number(n.bytesSentPerSecond ?? 0),
|
||||
deltaBytesRecv: Number(n.deltaBytesRecv ?? 0),
|
||||
deltaPacketsSent: Number(n.deltaPacketsSent ?? 0),
|
||||
deltaPacketsRecv: Number(n.deltaPacketsRecv ?? 0),
|
||||
deltaErrIn: Number(n.deltaErrIn ?? 0),
|
||||
deltaErrOut: Number(n.deltaErrOut ?? 0),
|
||||
deltaDropIn: Number(n.deltaDropIn ?? 0),
|
||||
deltaDropOut: Number(n.deltaDropOut ?? 0),
|
||||
deltaFifoIn: Number(n.deltaFifoIn ?? 0),
|
||||
deltaFifoOut: Number(n.deltaFifoOut ?? 0),
|
||||
})),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
monitorType: "hardware" as const,
|
||||
aggregateData: { totalChecks: totalResult.rows[0].count },
|
||||
upChecks: { totalChecks: upResult.rows[0].count },
|
||||
checks,
|
||||
};
|
||||
};
|
||||
|
||||
private findPageSpeedDateRangeChecks = async (
|
||||
monitorId: string,
|
||||
startDate: Date,
|
||||
endDate: Date,
|
||||
dateString: string
|
||||
): Promise<PageSpeedChecksResult> => {
|
||||
const bucket = dateStringToBucket(dateString);
|
||||
|
||||
const result = await this.pool.query(
|
||||
`SELECT
|
||||
time_bucket($1::interval, created_at) AS bucket_date,
|
||||
AVG(lighthouse_performance) AS performance,
|
||||
AVG(lighthouse_accessibility) AS accessibility,
|
||||
AVG(lighthouse_best_practices) AS "bestPractices",
|
||||
AVG(lighthouse_seo) AS seo,
|
||||
COUNT(*)::int AS "totalChecks"
|
||||
FROM checks
|
||||
WHERE monitor_id = $2 AND monitor_type = 'pagespeed' AND created_at >= $3 AND created_at <= $4
|
||||
GROUP BY bucket_date ORDER BY bucket_date`,
|
||||
[bucket, monitorId, startDate, endDate]
|
||||
);
|
||||
|
||||
return {
|
||||
monitorType: "pagespeed" as const,
|
||||
groupedChecks: result.rows.map((row) => ({
|
||||
bucketDate: (row.bucket_date as Date).toISOString(),
|
||||
performance: Number(row.performance ?? 0),
|
||||
accessibility: Number(row.accessibility ?? 0),
|
||||
bestPractices: Number(row.bestPractices ?? 0),
|
||||
seo: Number(row.seo ?? 0),
|
||||
totalChecks: Number(row.totalChecks ?? 0),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private toEntity = (row: any, disk?: CheckDiskInfo[], errors?: CheckErrorInfo[], net?: CheckNetworkInterfaceInfo[]): Check => {
|
||||
const timings: GotTimings = {
|
||||
start: row.timing_start ?? 0,
|
||||
socket: row.timing_socket ?? 0,
|
||||
lookup: row.timing_lookup ?? 0,
|
||||
connect: row.timing_connect ?? 0,
|
||||
secureConnect: row.timing_secure_connect ?? 0,
|
||||
upload: row.timing_upload ?? 0,
|
||||
response: row.timing_response ?? 0,
|
||||
end: row.timing_end ?? 0,
|
||||
phases: {
|
||||
wait: row.phase_wait ?? 0,
|
||||
dns: row.phase_dns ?? 0,
|
||||
tcp: row.phase_tcp ?? 0,
|
||||
tls: row.phase_tls ?? 0,
|
||||
request: row.phase_request ?? 0,
|
||||
firstByte: row.phase_first_byte ?? 0,
|
||||
download: row.phase_download ?? 0,
|
||||
total: row.phase_total ?? 0,
|
||||
},
|
||||
};
|
||||
|
||||
const cpu: CheckCpuInfo = {
|
||||
physical_core: row.cpu_physical_core ?? 0,
|
||||
logical_core: row.cpu_logical_core ?? 0,
|
||||
frequency: row.cpu_frequency ?? 0,
|
||||
current_frequency: row.cpu_current_frequency ?? 0,
|
||||
temperature: row.cpu_temperature ?? [],
|
||||
free_percent: row.cpu_free_percent ?? 0,
|
||||
usage_percent: row.cpu_usage_percent ?? 0,
|
||||
};
|
||||
|
||||
const memory: CheckMemoryInfo = {
|
||||
total_bytes: Number(row.mem_total_bytes ?? 0),
|
||||
available_bytes: Number(row.mem_available_bytes ?? 0),
|
||||
used_bytes: Number(row.mem_used_bytes ?? 0),
|
||||
usage_percent: row.mem_usage_percent ?? 0,
|
||||
};
|
||||
|
||||
const host: CheckHostInfo = {
|
||||
os: row.host_os ?? "",
|
||||
platform: row.host_platform ?? "",
|
||||
kernel_version: row.host_kernel_version ?? "",
|
||||
pretty_name: row.host_pretty_name ?? "",
|
||||
};
|
||||
|
||||
const capture: CheckCaptureInfo = {
|
||||
version: row.capture_version ?? "",
|
||||
mode: row.capture_mode ?? "",
|
||||
};
|
||||
|
||||
const audits: CheckAudits | undefined =
|
||||
row.audit_cls_score !== null
|
||||
? {
|
||||
cls: { score: row.audit_cls_score, numericValue: row.audit_cls_value, displayValue: row.audit_cls_display },
|
||||
si: { score: row.audit_si_score, numericValue: row.audit_si_value, displayValue: row.audit_si_display },
|
||||
fcp: { score: row.audit_fcp_score, numericValue: row.audit_fcp_value, displayValue: row.audit_fcp_display },
|
||||
lcp: { score: row.audit_lcp_score, numericValue: row.audit_lcp_value, displayValue: row.audit_lcp_display },
|
||||
tbt: { score: row.audit_tbt_score, numericValue: row.audit_tbt_value, displayValue: row.audit_tbt_display },
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
metadata: {
|
||||
monitorId: row.monitor_id,
|
||||
teamId: row.team_id,
|
||||
type: row.monitor_type,
|
||||
},
|
||||
status: row.status ?? false,
|
||||
responseTime: row.response_time ?? 0,
|
||||
timings,
|
||||
statusCode: row.status_code ?? 0,
|
||||
message: row.message ?? "",
|
||||
cpu,
|
||||
memory,
|
||||
disk: disk ?? [],
|
||||
host,
|
||||
errors: errors ?? [],
|
||||
capture,
|
||||
net: net ?? [],
|
||||
accessibility: row.lighthouse_accessibility ?? undefined,
|
||||
bestPractices: row.lighthouse_best_practices ?? undefined,
|
||||
seo: row.lighthouse_seo ?? undefined,
|
||||
performance: row.lighthouse_performance ?? undefined,
|
||||
audits,
|
||||
createdAt: row.created_at.toISOString(),
|
||||
updatedAt: row.updated_at.toISOString(),
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,38 +1,51 @@
|
||||
export * from "@/repositories/monitors/IMonitorsRepository.js";
|
||||
export { default as MongoMonitorsRepository } from "@/repositories/monitors/MongoMonitorsRepository.js";
|
||||
export { TimescaleMonitorsRepository } from "@/repositories/monitors/TimescaleMonitorsRepository.js";
|
||||
|
||||
export * from "@/repositories/checks/IChecksRepository.js";
|
||||
export { default as MongoChecksRepository } from "@/repositories/checks/MongoChecksRepistory.js";
|
||||
export { TimescaleChecksRepository } from "@/repositories/checks/TimescaleChecksRepository.js";
|
||||
|
||||
export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js";
|
||||
export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js";
|
||||
export { TimescaleMonitorStatsRepository } from "@/repositories/monitor-stats/TimescaleMonitorStatsRepository.js";
|
||||
|
||||
export * from "@/repositories/status-pages/IStatusPagesRepository.js";
|
||||
export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js";
|
||||
export { TimescaleStatusPagesRepository } from "@/repositories/status-pages/TimescaleStatusPagesRepository.js";
|
||||
|
||||
export * from "@/repositories/users/IUsersRepository.js";
|
||||
export { default as MongoUsersRepository } from "@/repositories/users/MongoUsersRepository.js";
|
||||
export { TimescaleUsersRepository } from "@/repositories/users/TimescaleUsersRepository.js";
|
||||
|
||||
export * from "@/repositories/invites/IInvitesRepository.js";
|
||||
export { default as MongoInvitesRepository } from "@/repositories/invites/MongoInviteRepository.js";
|
||||
export { TimescaleInvitesRepository } from "@/repositories/invites/TimescaleInvitesRepository.js";
|
||||
|
||||
export * from "@/repositories/recovery-tokens/IRecoveryTokensRepository.js";
|
||||
export { default as MongoRecoveryTokensRepository } from "@/repositories/recovery-tokens/MongoRecoveryTokensRepository.js";
|
||||
export { TimescaleRecoveryTokensRepository } from "@/repositories/recovery-tokens/TimescaleRecoveryTokensRepository.js";
|
||||
|
||||
export * from "@/repositories/settings/ISettingsRepository.js";
|
||||
export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js";
|
||||
export { TimescaleSettingsRepository } from "@/repositories/settings/TimescaleSettingsRepository.js";
|
||||
|
||||
export * from "@/repositories/notifications/INotificationsRepository.js";
|
||||
export { default as MongoNotificationsRepository } from "@/repositories/notifications/MongoNotificationsRepository.js";
|
||||
export { TimescaleNotificationsRepository } from "@/repositories/notifications/TimescaleNotificationsRepository.js";
|
||||
|
||||
export * from "@/repositories/incidents/IIncidentsRepository.js";
|
||||
export { default as MongoIncidentRepository } from "@/repositories/incidents/MongoIncidentRepository.js";
|
||||
export { default as MongoIncidentsRepository } from "@/repositories/incidents/MongoIncidentsRepository.js";
|
||||
export { TimescaleIncidentsRepository } from "@/repositories/incidents/TimescaleIncidentsRepository.js";
|
||||
|
||||
export * from "@/repositories/teams/ITeamsRepository.js";
|
||||
export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeamsRepository.js";
|
||||
export { TimescaleTeamsRepository } from "@/repositories/teams/TimescaleTeamsRepository.js";
|
||||
|
||||
export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js";
|
||||
export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js";
|
||||
export { TimescaleMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/TimescaleMaintenanceWindowsRepository.js";
|
||||
|
||||
export * from "@/repositories/geo-checks/IGeoChecksRepository.js";
|
||||
export { default as MongoGeoChecksRepository } from "@/repositories/geo-checks/MongoGeoChecksRepository.js";
|
||||
export { TimescaleGeoChecksRepository } from "@/repositories/geo-checks/TimescaleGeoChecksRepository.js";
|
||||
Reference in new issue
Block a user