Pinguje URL podle plánu a vede historii dostupnosti, kterou si projdete.
Tato služba dělá v jednom souboru dvě věci. Každých pět minut naplánovaný běh stáhne vaši TARGET_URL, zaznamená, jestli odpověděla a jak dlouho to trvalo, a výsledek uloží do připojené libsql databáze. Když službu otevřete v prohlížeči, stejný handler místo toho vykreslí poslední historii jako stránku se stavem.
Plán je přímo v kódu: export const crons registruje úlohu probe a každé spuštění volá handler s hlavičkami X-Fb-Trigger a X-Fb-Cron, takže naplánovaný běh pozná od zobrazení stránky. Export run() napojuje tlačítko Run, takže kontrolu zaznamenáte hned po nasazení a nemusíte čekat na další pětiminutový tik. Nasazením vznikne projekt, databáze checks (připojená do služby jako UPTIME_DB) a formulář se zeptá na URL, kterou má hlídat.
import { createClient } from "npm:@libsql/client@0.14.0/web"; // An uptime monitor in a single service. It does two jobs: // - on a schedule it pings a URL and records the result (up/down + latency), // - on a web request it renders the recent history as a status page. // // The schedule is defined right here in the code: the crons export below registers // a cron job named "probe". Each firing calls the default handler with the headers // "X-Fb-Trigger: cron" and "X-Fb-Cron: probe", so we branch on that to decide // which job to do. The run() export powers the Run button for an instant check. export const crons = { "probe": "*/5 * * * *" }; // Read a secret that is only present in some setups. In a normal deploy the linked // database and the TARGET_URL secret are always configured; this just keeps the service // from crashing if you copy it somewhere they are not set up yet. function optionalSecret(name: string): string | undefined { try { return Deno.env.get(name) || undefined; } catch { return undefined; // this service was not granted access to that variable } } // This service was deployed with a libsql database linked as UPTIME_DB. We read that; // if you copy the service without the link it falls back to its built-in database. const db = createClient({ url: optionalSecret("UPTIME_DB") ?? Deno.env.get("DATABASE_URL")!, }); // The URL to watch is provided as the TARGET_URL secret when you deploy. const TARGET_URL = optionalSecret("TARGET_URL") ?? "https://example.com"; // One table holds every probe result. datetime('now') is UTC in SQLite. async function ensureSchema(): Promise<void> { await db.execute( `CREATE TABLE IF NOT EXISTS checks ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, ok INTEGER NOT NULL, status INTEGER, latency_ms INTEGER NOT NULL, error TEXT, at TEXT NOT NULL DEFAULT (datetime('now')) )`, ); } // Probe TARGET_URL once and store how it went. async function runCheck(): Promise<void> { await ensureSchema(); const started = Date.now(); try { const res = await fetch(TARGET_URL); await res.body?.cancel(); // we only need the status, not the body await db.execute({ sql: "INSERT INTO checks (url, ok, status, latency_ms) VALUES (?, ?, ?, ?)", args: [TARGET_URL, res.ok ? 1 : 0, res.status, Date.now() - started], }); } catch (err) { // A thrown fetch means a DNS/TLS/timeout failure: a real outage worth recording. await db.execute({ sql: "INSERT INTO checks (url, ok, status, latency_ms, error) VALUES (?, ?, ?, ?, ?)", args: [TARGET_URL, 0, null, Date.now() - started, String(err)], }); } } function escapeHtml(s: string): string { return s .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } // Render the recent probe history as a small status page. async function renderPage(): Promise<Response> { await ensureSchema(); const summary = await db.execute( "SELECT count(*) AS total, coalesce(sum(ok), 0) AS up FROM checks", ); const total = Number(summary.rows[0].total); const up = Number(summary.rows[0].up); const uptime = total > 0 ? ((up / total) * 100).toFixed(1) : "0.0"; const recent = await db.execute( "SELECT ok, status, latency_ms, error, at FROM checks ORDER BY id DESC LIMIT 50", ); const rows = recent.rows .map((r) => { const ok = Number(r.ok) === 1; const detail = ok ? String(r.status) : escapeHtml(String(r.error ?? r.status ?? "error")); return ` <tr> <td>${escapeHtml(String(r.at))} UTC</td> <td class="${ok ? "up" : "down"}">${ok ? "up" : "down"}</td> <td>${detail}</td> <td>${escapeHtml(String(r.latency_ms))} ms</td> </tr>`; }) .join("\n"); const table = total === 0 ? `<p class="muted">No checks yet. The first one runs on the next tick.</p>` : `<table> <thead><tr><th>Time</th><th>Result</th><th>Detail</th><th>Latency</th></tr></thead> <tbody> ${rows} </tbody> </table>`; const html = `<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Uptime monitor</title> <style> body { font: 15px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; max-width: 720px; margin: 2.5rem auto; padding: 0 1rem; } h1 { font-size: 1.2rem; } table { width: 100%; border-collapse: collapse; margin-top: 1rem; } th, td { text-align: left; padding: 6px 10px; border-bottom: 1px solid #e2e2e2; } th { color: #666; font-weight: 600; } .up { color: #0a7d2c; } .down { color: #c0341d; } .muted { color: #888; } </style> </head> <body> <h1>Uptime monitor</h1> <p class="muted">Watching <strong>${escapeHtml(TARGET_URL)}</strong> every 5 minutes. Uptime ${uptime}% over ${total} checks.</p> ${table} </body> </html> `; return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" }, }); } // The Run button: probe the target right now instead of waiting for the next tick. export async function run(): Promise<string> { await runCheck(); return "check recorded"; } export default async (req: Request): Promise<Response> => { // The scheduled probe and the status page share this one handler. if (req.headers.get("x-fb-trigger") === "cron") { await runCheck(); return new Response("ok\n"); // the scheduled run has no visible page } return renderPage(); };
Go beyond what seems possible.