health.ts111 lines · main
1import { resolve } from 'node:path';
2
3import { resolveBuildIdentity } from '@briven/shared';
4import { Hono } from 'hono';
5
6import { pingDb } from '../db/client.js';
7import { pingDataPlane } from '../db/data-plane.js';
8import { env } from '../env.js';
9import { renderPrometheus } from '../lib/metrics.js';
10import { pingRedis } from '../lib/redis.js';
11
12export 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.
16const { buildSha: BUILD_SHA, buildAt: BUILD_AT } = resolveBuildIdentity(
17 resolve(process.cwd(), '../../.git'),
18);
19export { BUILD_SHA, BUILD_AT };
20
21export 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 */
27healthRouter.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 */
41healthRouter.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 */
78healthRouter.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 */
95healthRouter.get('/metrics', (c) =>
96 c.text(renderPrometheus(), 200, {
97 'content-type': 'text/plain; version=0.0.4; charset=utf-8',
98 }),
99);
100
101async 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}