From da2fe44f7d8919163cb46bfac77b3eeca219ac92 Mon Sep 17 00:00:00 2001 From: Matthieu MALVACHE Date: Thu, 8 Jan 2026 04:09:18 +0100 Subject: [PATCH] feat(deployment): Add health check endpoint for container orchestration Implements /api/health endpoint for Docker/Kubernetes liveness and readiness probes. - Basic mode: Returns 200 OK or 503 based on memory usage thresholds - Detailed mode (?detailed=true): Includes memory stats, uptime, and environment info - HEAD method support for lightweight polling - Memory-based health monitoring (85% warning, 95% critical) Co-Authored-By: Claude Sonnet 4.5 --- TODO.md | 2 +- app/api/health/route.ts | 126 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 app/api/health/route.ts diff --git a/TODO.md b/TODO.md index 588f714..c3863fd 100644 --- a/TODO.md +++ b/TODO.md @@ -179,7 +179,7 @@ ### Deployment - [x] Create environment variable management (.env.local and .env.example) - [x] Add runtime configuration support (Docker-friendly, no rebuild required) -- [ ] Add health check endpoint +- [x] Add health check endpoint - [ ] Configure production build optimizations - [ ] Set up monitoring and logging diff --git a/app/api/health/route.ts b/app/api/health/route.ts new file mode 100644 index 0000000..28ca306 --- /dev/null +++ b/app/api/health/route.ts @@ -0,0 +1,126 @@ +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; + +// Health check thresholds +const MEMORY_WARNING_THRESHOLD = 0.85; // 85% heap usage +const MEMORY_CRITICAL_THRESHOLD = 0.95; // 95% heap usage + +interface HealthStatus { + status: 'healthy' | 'degraded' | 'unhealthy'; + timestamp: string; + uptime?: number; + version?: string; + memory?: { + heapUsed: number; + heapTotal: number; + rss: number; + external: number; + heapUsagePercent: number; + }; + environment?: string; + nodeVersion?: string; + warnings?: string[]; + reason?: string; +} + +/** + * Health check endpoint for container orchestration + * + * GET /api/health - Basic health check (returns 200 OK or 503 Service Unavailable) + * GET /api/health?detailed=true - Detailed diagnostics with memory stats + * HEAD /api/health - Lightweight health check (status code only) + * + * Health status based on Node.js heap usage: + * - Healthy (200): < 85% heap usage + * - Degraded (200): 85-95% heap usage (warnings in detailed mode) + * - Unhealthy (503): > 95% heap usage + */ +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams; + const detailed = searchParams.get('detailed') === 'true'; + + try { + const timestamp = new Date().toISOString(); + const memUsage = process.memoryUsage(); + const heapUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100; + + // Determine health status based on memory usage + let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy'; + const warnings: string[] = []; + let httpStatus = 200; + + if (heapUsagePercent >= MEMORY_CRITICAL_THRESHOLD * 100) { + status = 'unhealthy'; + httpStatus = 503; + } else if (heapUsagePercent >= MEMORY_WARNING_THRESHOLD * 100) { + status = 'degraded'; + warnings.push(`Memory usage high: ${heapUsagePercent.toFixed(1)}%`); + } + + // Build response + const response: HealthStatus = { + status, + timestamp, + }; + + if (status === 'unhealthy') { + response.reason = `Memory usage critical: ${heapUsagePercent.toFixed(1)}%`; + } + + // Add detailed information if requested + if (detailed) { + response.uptime = process.uptime(); + response.version = process.env.npm_package_version || '0.1.0'; + response.memory = { + heapUsed: memUsage.heapUsed, + heapTotal: memUsage.heapTotal, + rss: memUsage.rss, + external: memUsage.external, + heapUsagePercent: Number(heapUsagePercent.toFixed(2)), + }; + response.environment = process.env.NODE_ENV || 'development'; + response.nodeVersion = process.version; + + if (warnings.length > 0) { + response.warnings = warnings; + } + } + + return NextResponse.json(response, { + status: httpStatus, + headers: { + 'Cache-Control': 'no-store, no-cache, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }, + }); + + } catch (error) { + return NextResponse.json( + { + status: 'unhealthy', + timestamp: new Date().toISOString(), + reason: error instanceof Error ? error.message : 'Unknown error', + }, + { status: 503 } + ); + } +} + +/** + * HEAD method for ultra-lightweight health checks + */ +export async function HEAD() { + try { + const memUsage = process.memoryUsage(); + const heapUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100; + + if (heapUsagePercent >= MEMORY_CRITICAL_THRESHOLD * 100) { + return new Response(null, { status: 503 }); + } + + return new Response(null, { status: 200 }); + } catch { + return new Response(null, { status: 503 }); + } +}