health.ts111 lines · main
| 1 | import { resolve } from 'node:path'; |
| 2 | |
| 3 | import { resolveBuildIdentity } from '@briven/shared'; |
| 4 | import { Hono } from 'hono'; |
| 5 | |
| 6 | import { pingDb } from '../db/client.js'; |
| 7 | import { pingDataPlane } from '../db/data-plane.js'; |
| 8 | import { env } from '../env.js'; |
| 9 | import { renderPrometheus } from '../lib/metrics.js'; |
| 10 | import { pingRedis } from '../lib/redis.js'; |
| 11 | |
| 12 | export const BOOT_TIME = new Date().toISOString(); |
| 13 | // apps/api runs from /app/apps/api (Dockerfile WORKDIR), so the repo |
| 14 | // root's .git directory is two levels up. The shared helper handles |
| 15 | // the env→git→"dev" fallback chain identically for every service. |
| 16 | const { buildSha: BUILD_SHA, buildAt: BUILD_AT } = resolveBuildIdentity( |
| 17 | resolve(process.cwd(), '../../.git'), |
| 18 | ); |
| 19 | export { BUILD_SHA, BUILD_AT }; |
| 20 | |
| 21 | export const healthRouter = new Hono(); |
| 22 | |
| 23 | /** |
| 24 | * /health — process liveness. Never depends on anything external. |
| 25 | * Per CLAUDE.md §5.5: health = process alive, ready = deps reachable. |
| 26 | */ |
| 27 | healthRouter.get('/health', (c) => |
| 28 | c.json({ |
| 29 | status: 'ok', |
| 30 | service: 'api', |
| 31 | env: env.BRIVEN_ENV, |
| 32 | bootedAt: BOOT_TIME, |
| 33 | }), |
| 34 | ); |
| 35 | |
| 36 | /** |
| 37 | * /ready — dependency readiness. Returns 200 only when every required |
| 38 | * upstream is reachable (control-plane postgres, data-plane postgres, |
| 39 | * runtime reachable from the swarm network). |
| 40 | */ |
| 41 | healthRouter.get('/ready', async (c) => { |
| 42 | const [controlOk, dataOk, runtimeOk, redisOk] = await Promise.all([ |
| 43 | env.BRIVEN_DATABASE_URL ? pingDb() : Promise.resolve(false), |
| 44 | env.BRIVEN_DATA_PLANE_URL ? pingDataPlane() : Promise.resolve(false), |
| 45 | probeRuntime(), |
| 46 | env.BRIVEN_REDIS_URL ? pingRedis() : Promise.resolve(false), |
| 47 | ]); |
| 48 | |
| 49 | const checks = { |
| 50 | control_postgres: env.BRIVEN_DATABASE_URL |
| 51 | ? controlOk |
| 52 | ? 'ok' |
| 53 | : 'unreachable' |
| 54 | : 'not_configured', |
| 55 | data_plane_postgres: env.BRIVEN_DATA_PLANE_URL |
| 56 | ? dataOk |
| 57 | ? 'ok' |
| 58 | : 'unreachable' |
| 59 | : 'not_configured', |
| 60 | runtime: runtimeOk ? 'ok' : 'unreachable', |
| 61 | redis: env.BRIVEN_REDIS_URL ? (redisOk ? 'ok' : 'unreachable') : 'not_configured', |
| 62 | } as const; |
| 63 | |
| 64 | // Redis powers logs streaming + rate limits. Required when configured; |
| 65 | // unconfigured = dev mode where logs/limits silently no-op. |
| 66 | const redisRequired = !!env.BRIVEN_REDIS_URL; |
| 67 | const ready = controlOk && dataOk && runtimeOk && (!redisRequired || redisOk); |
| 68 | return c.json({ status: ready ? 'ready' : 'not_ready', checks }, ready ? 200 : 503); |
| 69 | }); |
| 70 | |
| 71 | /** |
| 72 | * /info — build + runtime metadata. Public (no auth) so: |
| 73 | * - `briven doctor` can show "running build abc1234 / booted 2h ago" |
| 74 | * - support can verify which commit a customer's deploy is on |
| 75 | * - the post-deploy CI gate can assert a fresh sha is live |
| 76 | * Intentionally narrow — no secrets, no env-key listing. |
| 77 | */ |
| 78 | healthRouter.get('/info', (c) => |
| 79 | c.json({ |
| 80 | service: 'api', |
| 81 | env: env.BRIVEN_ENV, |
| 82 | buildSha: BUILD_SHA, |
| 83 | buildAt: BUILD_AT, |
| 84 | bootedAt: BOOT_TIME, |
| 85 | uptimeSec: Math.floor(process.uptime()), |
| 86 | domain: env.BRIVEN_DOMAIN ?? null, |
| 87 | }), |
| 88 | ); |
| 89 | |
| 90 | /** |
| 91 | * /metrics — Prometheus exposition. Intentionally unauthenticated; the |
| 92 | * scraper runs on the same docker network and the host firewall is the |
| 93 | * trust boundary. Per CLAUDE.md §11 every service exposes /metrics. |
| 94 | */ |
| 95 | healthRouter.get('/metrics', (c) => |
| 96 | c.text(renderPrometheus(), 200, { |
| 97 | 'content-type': 'text/plain; version=0.0.4; charset=utf-8', |
| 98 | }), |
| 99 | ); |
| 100 | |
| 101 | async function probeRuntime(): Promise<boolean> { |
| 102 | if (!env.BRIVEN_RUNTIME_URL) return false; |
| 103 | try { |
| 104 | const res = await fetch(`${env.BRIVEN_RUNTIME_URL}/health`, { |
| 105 | signal: AbortSignal.timeout(2000), |
| 106 | }); |
| 107 | return res.ok; |
| 108 | } catch { |
| 109 | return false; |
| 110 | } |
| 111 | } |