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
|
// Aggregates raw pings into monitor_uptime_rollup so status pages and dashboard
|
||||||
// widgets can compute uptime % over arbitrary windows without ever scanning the
|
// 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
|
// 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.
|
// 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> = {
|
const BUCKET_TRUNC: Record<BucketType, string> = {
|
||||||
hourly: "hour",
|
hourly: "hour",
|
||||||
daily: "day",
|
daily: "day",
|
||||||
weekly: "week",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function rollupCurrent(bucket: BucketType): Promise<number> {
|
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
|
// 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.
|
// service logs immediately, instead of leaving the table mysteriously empty.
|
||||||
try {
|
try {
|
||||||
const [h, d, w] = await Promise.all([
|
const [h, d] = await Promise.all([
|
||||||
backfillRecent("hourly", 48),
|
backfillRecent("hourly", 48),
|
||||||
backfillRecent("daily", 90),
|
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) {
|
} catch (e) {
|
||||||
console.error("[rollup] backfill FAILED — rollup table will be empty until fixed:", 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
|
// run rollupCurrent immediately so we have at least one row per type. This
|
||||||
// covers the "fresh deploy with very recent pings only" case.
|
// covers the "fresh deploy with very recent pings only" case.
|
||||||
try {
|
try {
|
||||||
for (const b of ["hourly", "daily", "weekly"] as BucketType[]) {
|
for (const b of ["hourly", "daily"] as BucketType[]) {
|
||||||
if (await rollupIsEmpty(b)) {
|
if (await rollupIsEmpty(b)) {
|
||||||
console.log(`[rollup] ${b} still empty — forcing current-bucket aggregation`);
|
console.log(`[rollup] ${b} still empty — forcing current-bucket aggregation`);
|
||||||
await rollupCurrent(b);
|
await rollupCurrent(b);
|
||||||
|
|
@ -110,5 +108,4 @@ export async function startRollupJob() {
|
||||||
// Periodic refreshes for the *current* bucket of each resolution.
|
// 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("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("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";
|
import sql from "./db";
|
||||||
|
|
||||||
export type Window = "24h" | "7d" | "30d" | "90d";
|
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 }> = {
|
const WINDOW_TO_BUCKET: Record<Window, { bucket: BucketType; count: number }> = {
|
||||||
"24h": { bucket: "hourly", count: 24 },
|
"24h": { bucket: "hourly", count: 24 },
|
||||||
"7d": { bucket: "daily", count: 7 },
|
"7d": { bucket: "daily", count: 7 },
|
||||||
"30d": { bucket: "daily", count: 30 },
|
"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 {
|
export interface StatusPageRow {
|
||||||
|
|
@ -53,7 +53,7 @@ export interface MonitorRow {
|
||||||
region_states: Array<{ region: string; state: "up" | "down" | "unknown"; updated_at: string | null }>;
|
region_states: Array<{ region: string; state: "up" | "down" | "unknown"; updated_at: string | null }>;
|
||||||
uptime_pct: number | null; // for the page's default_window
|
uptime_pct: number | null; // for the page's default_window
|
||||||
uptime: MultiWindowUptime; // 24h / 7d / 30d / 90d row
|
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;
|
avg_latency: number | null;
|
||||||
latency_history: Array<{ region: string; latency_ms: number | null; ts: string }>;
|
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,
|
/ 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
|
(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,
|
/ 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
|
(sum(up_count) FILTER (WHERE bucket_type='daily' 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
|
/ NULLIF(sum(total) FILTER (WHERE bucket_type='daily' AND bucket_start > now() - interval '90 days'), 0) AS pct_90d
|
||||||
FROM monitor_uptime_rollup
|
FROM monitor_uptime_rollup
|
||||||
WHERE monitor_id = ANY(${ids}::text[])
|
WHERE monitor_id = ANY(${ids}::text[])
|
||||||
GROUP BY 1
|
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.
|
// Step 3: uptime rollup buckets covering the requested window.
|
||||||
const { bucket, count } = WINDOW_TO_BUCKET[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`;
|
const intervalLiteral = `${count} ${truncUnit}s`;
|
||||||
let rollupRows = await sql<any[]>`
|
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
|
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
|
// Generate the full sequence of expected bucket timestamps so empty bars
|
||||||
// render as "no data" instead of disappearing entirely. Truncate `now()` to
|
// render as "no data" instead of disappearing entirely. Truncate `now()` to
|
||||||
// the unit so the slot boundaries line up with what the rollup writes.
|
// 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 truncate = (d: Date): Date => {
|
||||||
const t = new Date(d);
|
const t = new Date(d);
|
||||||
if (bucket === "hourly") { t.setUTCMinutes(0, 0, 0); }
|
if (bucket === "hourly") t.setUTCMinutes(0, 0, 0);
|
||||||
else { t.setUTCHours(0, 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);
|
|
||||||
}
|
|
||||||
return t;
|
return t;
|
||||||
};
|
};
|
||||||
const nowTrunc = truncate(new Date()).getTime();
|
const nowTrunc = truncate(new Date()).getTime();
|
||||||
|
|
@ -310,7 +305,9 @@ export async function loadMonitors(pageId: string, window: Window, pageDisplayMo
|
||||||
const slotMap = indexed[id] ?? {};
|
const slotMap = indexed[id] ?? {};
|
||||||
bucketsByMonitor[id] = slotIsos.map((iso) => {
|
bucketsByMonitor[id] = slotIsos.map((iso) => {
|
||||||
const hit = slotMap[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 ──────────────────────────────────────────────────────
|
// ── Bar tooltips ──────────────────────────────────────────────────────
|
||||||
var tooltip = document.getElementById("bar-tooltip");
|
var tooltip = document.getElementById("bar-tooltip");
|
||||||
if (tooltip) {
|
if (tooltip) {
|
||||||
var bucketSpanMs = (defaultWindow === "24h") ? 3600 * 1000
|
var bucketSpanMs = (defaultWindow === "24h") ? 3600 * 1000 : 86400 * 1000;
|
||||||
: (defaultWindow === "7d") ? 86400 * 1000
|
|
||||||
: (defaultWindow === "30d") ? 86400 * 1000
|
|
||||||
: 7 * 86400 * 1000;
|
|
||||||
function fmtBucketRange(startIso) {
|
function fmtBucketRange(startIso) {
|
||||||
var start = new Date(startIso);
|
var start = new Date(startIso);
|
||||||
var end = new Date(start.getTime() + bucketSpanMs);
|
var end = new Date(start.getTime() + bucketSpanMs);
|
||||||
var sameDay = start.toDateString() === new Date(end.getTime() - 1).toDateString();
|
|
||||||
var dateOpts = { month: "short", day: "numeric" };
|
var dateOpts = { month: "short", day: "numeric" };
|
||||||
var timeOpts = { hour: "2-digit", minute: "2-digit" };
|
var timeOpts = { hour: "2-digit", minute: "2-digit" };
|
||||||
if (defaultWindow === "24h") {
|
if (defaultWindow === "24h") {
|
||||||
|
|
@ -29,13 +25,9 @@
|
||||||
start.toLocaleTimeString(undefined, timeOpts) + " — " +
|
start.toLocaleTimeString(undefined, timeOpts) + " — " +
|
||||||
end.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" });
|
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) {
|
function uptimeBand(p) {
|
||||||
if (p == null) return "";
|
if (p == null) return "";
|
||||||
if (p >= 99.9) return "good";
|
if (p >= 99.9) return "good";
|
||||||
|
|
@ -47,6 +39,8 @@
|
||||||
var total = parseInt(bar.getAttribute("data-total") || "0", 10);
|
var total = parseInt(bar.getAttribute("data-total") || "0", 10);
|
||||||
var up = parseInt(bar.getAttribute("data-up") || "0", 10);
|
var up = parseInt(bar.getAttribute("data-up") || "0", 10);
|
||||||
var start = bar.getAttribute("data-start");
|
var start = bar.getAttribute("data-start");
|
||||||
|
var latRaw = bar.getAttribute("data-latency");
|
||||||
|
var lat = latRaw == null ? null : parseInt(latRaw, 10);
|
||||||
if (!start) return;
|
if (!start) return;
|
||||||
var pct = total > 0 ? Math.round(1000 * up / total) / 10 : null;
|
var pct = total > 0 ? Math.round(1000 * up / total) / 10 : null;
|
||||||
var pctText = pct == null ? "—" : (pct === 100 ? "100%" : pct.toFixed(1) + "%");
|
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>Checks</span><span>' + total + "</span></div>";
|
||||||
html += '<div class="row"><span>Successful</span><span>' + up + "</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>";
|
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 {
|
} else {
|
||||||
html += '<div class="row"><span>No data</span><span>—</span></div>';
|
html += '<div class="row"><span>No data</span><span>—</span></div>';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="bars" aria-label="<%= statusLabel(m.current_state) %>">
|
<div class="bars" aria-label="<%= statusLabel(m.current_state) %>">
|
||||||
<% buckets.forEach(function(b) { %>
|
<% 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>
|
||||||
<div class="bars-meta">
|
<div class="bars-meta">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue