update rollup
This commit is contained in:
parent
ae5a8f4597
commit
1732a9d055
|
|
@ -2,17 +2,16 @@ import sql from "../db";
|
|||
|
||||
// Aggregates raw pings into monitor_uptime_rollup so status pages and dashboard
|
||||
// widgets can compute uptime % over arbitrary windows without ever scanning the
|
||||
// pings table at read time. Three resolutions: hourly, daily, weekly.
|
||||
// pings table at read time. Two resolutions: hourly and daily.
|
||||
//
|
||||
// Each pass aggregates the *current* bucket only. The query is bounded by the
|
||||
// bucket size, not the table size, so it's cheap regardless of history depth.
|
||||
|
||||
type BucketType = "hourly" | "daily" | "weekly";
|
||||
type BucketType = "hourly" | "daily";
|
||||
|
||||
const BUCKET_TRUNC: Record<BucketType, string> = {
|
||||
hourly: "hour",
|
||||
daily: "day",
|
||||
weekly: "week",
|
||||
};
|
||||
|
||||
async function rollupCurrent(bucket: BucketType): Promise<number> {
|
||||
|
|
@ -83,12 +82,11 @@ export async function startRollupJob() {
|
|||
// we throw so a broken rollup query trips the api process and shows in the
|
||||
// service logs immediately, instead of leaving the table mysteriously empty.
|
||||
try {
|
||||
const [h, d, w] = await Promise.all([
|
||||
const [h, d] = await Promise.all([
|
||||
backfillRecent("hourly", 48),
|
||||
backfillRecent("daily", 90),
|
||||
backfillRecent("weekly", 26),
|
||||
]);
|
||||
console.log(`[rollup] backfilled rows: hourly=${h} daily=${d} weekly=${w}`);
|
||||
console.log(`[rollup] backfilled rows: hourly=${h} daily=${d}`);
|
||||
} catch (e) {
|
||||
console.error("[rollup] backfill FAILED — rollup table will be empty until fixed:", e);
|
||||
}
|
||||
|
|
@ -97,7 +95,7 @@ export async function startRollupJob() {
|
|||
// run rollupCurrent immediately so we have at least one row per type. This
|
||||
// covers the "fresh deploy with very recent pings only" case.
|
||||
try {
|
||||
for (const b of ["hourly", "daily", "weekly"] as BucketType[]) {
|
||||
for (const b of ["hourly", "daily"] as BucketType[]) {
|
||||
if (await rollupIsEmpty(b)) {
|
||||
console.log(`[rollup] ${b} still empty — forcing current-bucket aggregation`);
|
||||
await rollupCurrent(b);
|
||||
|
|
@ -110,5 +108,4 @@ export async function startRollupJob() {
|
|||
// Periodic refreshes for the *current* bucket of each resolution.
|
||||
setInterval(() => { rollupCurrent("hourly").catch((e) => console.warn("[rollup] hourly failed:", e)); }, 5 * 60 * 1000);
|
||||
setInterval(() => { rollupCurrent("daily").catch((e) => console.warn("[rollup] daily failed:", e)); }, 30 * 60 * 1000);
|
||||
setInterval(() => { rollupCurrent("weekly").catch((e) => console.warn("[rollup] weekly failed:", e)); }, 6 * 60 * 60 * 1000);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
import sql from "./db";
|
||||
|
||||
export type Window = "24h" | "7d" | "30d" | "90d";
|
||||
export type BucketType = "hourly" | "daily" | "weekly";
|
||||
export type BucketType = "hourly" | "daily";
|
||||
|
||||
const WINDOW_TO_BUCKET: Record<Window, { bucket: BucketType; count: number }> = {
|
||||
"24h": { bucket: "hourly", count: 24 },
|
||||
"7d": { bucket: "daily", count: 7 },
|
||||
"30d": { bucket: "daily", count: 30 },
|
||||
"90d": { bucket: "weekly", count: 13 },
|
||||
"90d": { bucket: "daily", count: 90 }, // 90 daily bars; 90 rows per monitor, cached.
|
||||
};
|
||||
|
||||
export interface StatusPageRow {
|
||||
|
|
@ -53,7 +53,7 @@ export interface MonitorRow {
|
|||
region_states: Array<{ region: string; state: "up" | "down" | "unknown"; updated_at: string | null }>;
|
||||
uptime_pct: number | null; // for the page's default_window
|
||||
uptime: MultiWindowUptime; // 24h / 7d / 30d / 90d row
|
||||
buckets: Array<{ start: string; total: number; up: number }>; // bar chart input
|
||||
buckets: Array<{ start: string; total: number; up: number; avg_latency: number | null }>; // bar chart input
|
||||
avg_latency: number | null;
|
||||
latency_history: Array<{ region: string; latency_ms: number | null; ts: string }>;
|
||||
}
|
||||
|
|
@ -126,8 +126,8 @@ export async function loadMultiWindowUptime(monitorIds: string[]): Promise<Recor
|
|||
/ NULLIF(sum(total) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '7 days'), 0) AS pct_7d,
|
||||
(sum(up_count) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '30 days'))::float
|
||||
/ NULLIF(sum(total) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '30 days'), 0) AS pct_30d,
|
||||
(sum(up_count) FILTER (WHERE bucket_type='weekly' AND bucket_start > now() - interval '90 days'))::float
|
||||
/ NULLIF(sum(total) FILTER (WHERE bucket_type='weekly' AND bucket_start > now() - interval '90 days'), 0) AS pct_90d
|
||||
(sum(up_count) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '90 days'))::float
|
||||
/ NULLIF(sum(total) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '90 days'), 0) AS pct_90d
|
||||
FROM monitor_uptime_rollup
|
||||
WHERE monitor_id = ANY(${ids}::text[])
|
||||
GROUP BY 1
|
||||
|
|
@ -240,7 +240,7 @@ export async function loadMonitors(pageId: string, window: Window, pageDisplayMo
|
|||
|
||||
// Step 3: uptime rollup buckets covering the requested window.
|
||||
const { bucket, count } = WINDOW_TO_BUCKET[window];
|
||||
const truncUnit = bucket === "hourly" ? "hour" : bucket === "daily" ? "day" : "week";
|
||||
const truncUnit = bucket === "hourly" ? "hour" : "day";
|
||||
const intervalLiteral = `${count} ${truncUnit}s`;
|
||||
let rollupRows = await sql<any[]>`
|
||||
SELECT monitor_id, bucket_start, sum(total)::int AS total, sum(up_count)::int AS up_count, avg(avg_latency)::real AS avg_latency
|
||||
|
|
@ -288,16 +288,11 @@ export async function loadMonitors(pageId: string, window: Window, pageDisplayMo
|
|||
// Generate the full sequence of expected bucket timestamps so empty bars
|
||||
// render as "no data" instead of disappearing entirely. Truncate `now()` to
|
||||
// the unit so the slot boundaries line up with what the rollup writes.
|
||||
const bucketMs = bucket === "hourly" ? 3600_000 : bucket === "daily" ? 86_400_000 : 604_800_000;
|
||||
const bucketMs = bucket === "hourly" ? 3600_000 : 86_400_000;
|
||||
const truncate = (d: Date): Date => {
|
||||
const t = new Date(d);
|
||||
if (bucket === "hourly") { t.setUTCMinutes(0, 0, 0); }
|
||||
else { t.setUTCHours(0, 0, 0, 0); }
|
||||
if (bucket === "weekly") {
|
||||
// ISO week starts Monday.
|
||||
const day = (t.getUTCDay() + 6) % 7;
|
||||
t.setUTCDate(t.getUTCDate() - day);
|
||||
}
|
||||
if (bucket === "hourly") t.setUTCMinutes(0, 0, 0);
|
||||
else t.setUTCHours(0, 0, 0, 0);
|
||||
return t;
|
||||
};
|
||||
const nowTrunc = truncate(new Date()).getTime();
|
||||
|
|
@ -310,7 +305,9 @@ export async function loadMonitors(pageId: string, window: Window, pageDisplayMo
|
|||
const slotMap = indexed[id] ?? {};
|
||||
bucketsByMonitor[id] = slotIsos.map((iso) => {
|
||||
const hit = slotMap[iso];
|
||||
return hit ? { start: iso, total: hit.total, up: hit.up } : { start: iso, total: 0, up: 0 };
|
||||
return hit
|
||||
? { start: iso, total: hit.total, up: hit.up, avg_latency: hit.avg_latency != null ? Math.round(hit.avg_latency) : null }
|
||||
: { start: iso, total: 0, up: 0, avg_latency: null };
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,14 +14,10 @@
|
|||
// ── Bar tooltips ──────────────────────────────────────────────────────
|
||||
var tooltip = document.getElementById("bar-tooltip");
|
||||
if (tooltip) {
|
||||
var bucketSpanMs = (defaultWindow === "24h") ? 3600 * 1000
|
||||
: (defaultWindow === "7d") ? 86400 * 1000
|
||||
: (defaultWindow === "30d") ? 86400 * 1000
|
||||
: 7 * 86400 * 1000;
|
||||
var bucketSpanMs = (defaultWindow === "24h") ? 3600 * 1000 : 86400 * 1000;
|
||||
function fmtBucketRange(startIso) {
|
||||
var start = new Date(startIso);
|
||||
var end = new Date(start.getTime() + bucketSpanMs);
|
||||
var sameDay = start.toDateString() === new Date(end.getTime() - 1).toDateString();
|
||||
var dateOpts = { month: "short", day: "numeric" };
|
||||
var timeOpts = { hour: "2-digit", minute: "2-digit" };
|
||||
if (defaultWindow === "24h") {
|
||||
|
|
@ -29,13 +25,9 @@
|
|||
start.toLocaleTimeString(undefined, timeOpts) + " — " +
|
||||
end.toLocaleTimeString(undefined, timeOpts);
|
||||
}
|
||||
if (defaultWindow === "7d" || defaultWindow === "30d") {
|
||||
// 7d / 30d / 90d → daily buckets
|
||||
return start.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" });
|
||||
}
|
||||
// 90d → weekly buckets
|
||||
var endLabel = new Date(end.getTime() - 1).toLocaleDateString(undefined, dateOpts);
|
||||
return "Week of " + start.toLocaleDateString(undefined, dateOpts) + " — " + endLabel;
|
||||
}
|
||||
function uptimeBand(p) {
|
||||
if (p == null) return "";
|
||||
if (p >= 99.9) return "good";
|
||||
|
|
@ -47,6 +39,8 @@
|
|||
var total = parseInt(bar.getAttribute("data-total") || "0", 10);
|
||||
var up = parseInt(bar.getAttribute("data-up") || "0", 10);
|
||||
var start = bar.getAttribute("data-start");
|
||||
var latRaw = bar.getAttribute("data-latency");
|
||||
var lat = latRaw == null ? null : parseInt(latRaw, 10);
|
||||
if (!start) return;
|
||||
var pct = total > 0 ? Math.round(1000 * up / total) / 10 : null;
|
||||
var pctText = pct == null ? "—" : (pct === 100 ? "100%" : pct.toFixed(1) + "%");
|
||||
|
|
@ -55,6 +49,9 @@
|
|||
html += '<div class="row"><span>Checks</span><span>' + total + "</span></div>";
|
||||
html += '<div class="row"><span>Successful</span><span>' + up + "</span></div>";
|
||||
html += '<div class="row"><span>Uptime</span><span class="pct ' + uptimeBand(pct) + '">' + pctText + "</span></div>";
|
||||
if (lat != null) {
|
||||
html += '<div class="row"><span>Avg ping</span><span>' + lat + "ms</span></div>";
|
||||
}
|
||||
} else {
|
||||
html += '<div class="row"><span>No data</span><span>—</span></div>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@
|
|||
</div>
|
||||
<div class="bars" aria-label="<%= statusLabel(m.current_state) %>">
|
||||
<% buckets.forEach(function(b) { %>
|
||||
<div class="bar" style="background: <%= bucketColor(b) %>;" data-start="<%= b.start %>" data-total="<%= b.total %>" data-up="<%= b.up %>"></div>
|
||||
<div class="bar" style="background: <%= bucketColor(b) %>;" data-start="<%= b.start %>" data-total="<%= b.total %>" data-up="<%= b.up %>"<% if (b.avg_latency != null) { %> data-latency="<%= b.avg_latency %>"<% } %>></div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<div class="bars-meta">
|
||||
|
|
|
|||
Loading…
Reference in New Issue