import { Hono } from 'hono'; import { sql } from 'drizzle-orm'; import { getDb } from '../db/connection.ts'; import { getStorage } from '../services/storage.ts'; export const healthRoute = new Hono(); /** * Liveness — antwortet immer 200, solange der Prozess läuft. * Bewusst KEIN Downstream-Probe, damit ein kurzer DB-/MinIO-Hänger * nicht den Container in eine Restart-Schleife zwingt. */ healthRoute.get('/healthz', (c) => c.json({ status: 'ok' })); /** * Readiness mit Downstream-Probes. Status 200 wenn DB + MinIO grün, * sonst 503 mit Aufschlüsselung welche Probe fehlgeschlagen ist. * Probes timen sich selbst aus über ein 1s-AbortSignal — eine träge * Abhängigkeit darf das Readiness-Signal nicht hängen lassen. */ healthRoute.get('/healthz/details', async (c) => { const [dbProbe, storageProbe] = await Promise.all([ probeDb(), probeStorage(), ]); const allOk = dbProbe.ok && storageProbe.ok; return c.json( { status: allOk ? 'ok' : 'degraded', app: 'cards', version: process.env.CARDS_API_VERSION ?? '0.0.0', uptime_s: Math.floor(process.uptime()), checks: { db: dbProbe, storage: storageProbe, }, }, allOk ? 200 : 503 ); }); healthRoute.get('/version', (c) => c.json({ app: 'cards', version: process.env.CARDS_API_VERSION ?? '0.0.0', build: process.env.CARDS_BUILD_SHA ?? 'dev', }) ); type ProbeResult = { ok: true; latency_ms: number } | { ok: false; error: string }; async function probeDb(): Promise { const t0 = Date.now(); try { const db = getDb(); await db.execute(sql`SELECT 1`); return { ok: true, latency_ms: Date.now() - t0 }; } catch (err) { return { ok: false, error: errorMessage(err) }; } } async function probeStorage(): Promise { const t0 = Date.now(); try { await getStorage().ensureBucket(); return { ok: true, latency_ms: Date.now() - t0 }; } catch (err) { return { ok: false, error: errorMessage(err) }; } } function errorMessage(err: unknown): string { if (err instanceof Error) return err.message; return String(err); }