Monitoring: /health for uptime checks, structured error logging, API errors without internal details #90
@ -24,6 +24,25 @@ import { tagRoutes, pageTags } from './tags.ts';
|
|||||||
import { ebookRoutes } from './ebooks.ts';
|
import { ebookRoutes } from './ebooks.ts';
|
||||||
|
|
||||||
const app = Fastify({ logger: { level: process.env.LOG_LEVEL ?? 'info' } });
|
const app = Fastify({ logger: { level: process.env.LOG_LEVEL ?? 'info' } });
|
||||||
|
|
||||||
|
// Errors: readers get a short message, never internal details; the log gets the whole error (one JSON line each,
|
||||||
|
// with the method and address), which is what the server's error report reads (divan-deploy scripts/errors.sh).
|
||||||
|
app.setErrorHandler((err: any, req, reply) => {
|
||||||
|
const status = err.statusCode >= 400 && err.statusCode < 500 ? err.statusCode : 500;
|
||||||
|
if (status === 500) req.log.error({ err, method: req.method, url: req.url }, 'request failed');
|
||||||
|
reply.code(status).send({ error: status === 500 ? 'سرور میں خرابی۔ تھوڑی دیر بعد کوشش کریں۔' : err.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
// for the site's /health (uptime monitoring): the API answers and the database too
|
||||||
|
app.get('/api/health', async (req, reply) => {
|
||||||
|
try {
|
||||||
|
await pool.query('SELECT 1');
|
||||||
|
return { ok: true, db: 'ok' };
|
||||||
|
} catch (err) {
|
||||||
|
req.log.error({ err }, 'health: database unreachable');
|
||||||
|
return reply.code(503).send({ ok: false, db: 'down' });
|
||||||
|
}
|
||||||
|
});
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
app.get('/health', async () => {
|
app.get('/health', async () => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// The signed-in reader (or null) for every page, from the session cookie; and the reader's numerals: pages are
|
// The signed-in reader (or null) for every page, from the session cookie; and the reader's numerals: pages are
|
||||||
// written with Eastern Arabic (Urdu) digits, and a reader who chose Western digits (cookie divan-digits=latn, set in
|
// written with Eastern Arabic (Urdu) digits, and a reader who chose Western digits (cookie divan-digits=latn, set in
|
||||||
// the display settings) gets them in the page as sent, so nothing flashes
|
// the display settings) gets them in the page as sent, so nothing flashes. Errors are logged as one JSON line each.
|
||||||
import { defineMiddleware } from 'astro:middleware';
|
import { defineMiddleware } from 'astro:middleware';
|
||||||
import { auth, COOKIE } from './lib/auth';
|
import { auth, COOKIE } from './lib/auth';
|
||||||
|
|
||||||
@ -12,7 +12,15 @@ export const onRequest = defineMiddleware(async (ctx, next) => {
|
|||||||
if (r.ok) ctx.locals.user = r.data.user;
|
if (r.ok) ctx.locals.user = r.data.user;
|
||||||
else if (r.status === 401) ctx.cookies.delete(COOKIE, { path: '/' }); // expired or signed out elsewhere
|
else if (r.status === 401) ctx.cookies.delete(COOKIE, { path: '/' }); // expired or signed out elsewhere
|
||||||
}
|
}
|
||||||
const res = await next();
|
let res: Response;
|
||||||
|
try {
|
||||||
|
res = await next();
|
||||||
|
} catch (err: any) {
|
||||||
|
// one JSON line per error (the server's error report reads these); the reader sees the Urdu error page (500.astro)
|
||||||
|
console.error(JSON.stringify({ level: 'error', time: new Date().toISOString(), method: ctx.request.method, path: ctx.url.pathname,
|
||||||
|
msg: String(err?.message ?? err), stack: String(err?.stack ?? '').split('\n').slice(0, 8).join('\n') }));
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
if (ctx.cookies.get('divan-digits')?.value !== 'latn' || !res.headers.get('content-type')?.includes('text/html')) return res;
|
if (ctx.cookies.get('divan-digits')?.value !== 'latn' || !res.headers.get('content-type')?.includes('text/html')) return res;
|
||||||
// text only: scripts and styles are left alone (the page's own scripts look for Eastern digits)
|
// text only: scripts and styles are left alone (the page's own scripts look for Eastern digits)
|
||||||
const html = (await res.text()).split(/(<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>)/)
|
const html = (await res.text()).split(/(<script[\s\S]*?<\/script>|<style[\s\S]*?<\/style>)/)
|
||||||
|
|||||||
20
web/src/pages/health.ts
Normal file
20
web/src/pages/health.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// /health for an uptime monitor (divan-deploy README): 200 when the site, the API and the database all answer,
|
||||||
|
// 503 otherwise, with which part is down. Never cached.
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
|
||||||
|
const API = process.env.API_URL ?? 'http://127.0.0.1:4100';
|
||||||
|
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
const started = Date.now();
|
||||||
|
let api = 'down', db = 'down';
|
||||||
|
try {
|
||||||
|
const r = await fetch(`${API}/api/health`, { signal: AbortSignal.timeout(3000) });
|
||||||
|
api = 'ok';
|
||||||
|
db = (await r.json().catch(() => ({}))).db ?? 'down';
|
||||||
|
} catch {}
|
||||||
|
const ok = api === 'ok' && db === 'ok';
|
||||||
|
if (!ok) console.error(JSON.stringify({ level: 'error', time: new Date().toISOString(), msg: 'health check failed', api, db }));
|
||||||
|
return new Response(JSON.stringify({ ok, site: 'ok', api, db, ms: Date.now() - started }), {
|
||||||
|
status: ok ? 200 : 503, headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
||||||
|
});
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user