chore(ops): split /api/health (liveness) from /api/ready (readiness)
Previously /api/health did deep dependency probes (postgres + redis + minio) and 503'd on any failure. That's readiness behavior, not liveness — a transient Redis/MinIO blip would tell the orchestrator to restart the pod when it should only be dropped from the load balancer. Make /api/health a thin liveness check (returns 200 unconditionally if the process is responding) and move the deep checks to a new /api/ready endpoint with the canonical Kubernetes-style 200/503 contract. Docker-compose healthchecks keep pointing at /api/health, which is now more conservative (no false-positive container restarts). Documenso/SMTP are intentionally not probed in /api/ready: each tenant configures its own credentials and a tenant misconfiguration shouldn't deadline the entire shared CRM. Also tighten the gdpr-bundle-builder casts: replace the scattered `as unknown as Record<string, unknown>` double-casts with a small `toJsonRow<T>()` helper that does the widen narrow→wide in one place with one cast hop instead of two. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
82
src/app/api/ready/route.ts
Normal file
82
src/app/api/ready/route.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { redis } from '@/lib/redis';
|
||||
import { minioClient } from '@/lib/minio';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
type CheckStatus = 'ok' | 'error';
|
||||
|
||||
interface ReadyChecks {
|
||||
postgres: CheckStatus;
|
||||
redis: CheckStatus;
|
||||
minio: CheckStatus;
|
||||
}
|
||||
|
||||
interface ReadyResponse {
|
||||
status: 'ready' | 'degraded';
|
||||
checks: ReadyChecks;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Readiness probe — verifies that every backing service this process
|
||||
* needs to serve traffic is reachable. A 503 should drop the pod from the
|
||||
* load balancer until the next probe succeeds; it should not trigger a
|
||||
* pod restart (that's what `/api/health` is for).
|
||||
*
|
||||
* Checks:
|
||||
* - postgres: `SELECT 1` against the primary
|
||||
* - redis: `PING`
|
||||
* - minio: `bucketExists(<configured-bucket>)`
|
||||
*
|
||||
* Documenso + SMTP are intentionally not probed here: they're optional
|
||||
* integrations, and each tenant configures its own credentials. A
|
||||
* tenant-misconfigured Documenso instance shouldn't deadline the entire
|
||||
* shared CRM.
|
||||
*/
|
||||
export async function GET(): Promise<NextResponse<ReadyResponse>> {
|
||||
const checks: ReadyChecks = {
|
||||
postgres: 'error',
|
||||
redis: 'error',
|
||||
minio: 'error',
|
||||
};
|
||||
|
||||
await Promise.allSettled([
|
||||
db
|
||||
.execute(sql`SELECT 1`)
|
||||
.then(() => {
|
||||
checks.postgres = 'ok';
|
||||
})
|
||||
.catch(() => {
|
||||
checks.postgres = 'error';
|
||||
}),
|
||||
|
||||
redis
|
||||
.ping()
|
||||
.then(() => {
|
||||
checks.redis = 'ok';
|
||||
})
|
||||
.catch(() => {
|
||||
checks.redis = 'error';
|
||||
}),
|
||||
|
||||
minioClient
|
||||
.bucketExists(env.MINIO_BUCKET)
|
||||
.then(() => {
|
||||
checks.minio = 'ok';
|
||||
})
|
||||
.catch(() => {
|
||||
checks.minio = 'error';
|
||||
}),
|
||||
]);
|
||||
|
||||
const allReady = Object.values(checks).every((s) => s === 'ok');
|
||||
const status: ReadyResponse['status'] = allReady ? 'ready' : 'degraded';
|
||||
|
||||
return NextResponse.json(
|
||||
{ status, checks, timestamp: new Date().toISOString() },
|
||||
{ status: allReady ? 200 : 503 },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user