checks repo, barrel export

This commit is contained in:
Alex Holliday committed 2026-03-25 13:52:18 -07:00
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(),
};
};
}
+14 -1
View File
@@ -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";