/** * Comprehensive Health Check System * * Validates that the deployed tRPC and batch processing architecture * is working correctly and all components are healthy. */ import { PrismaClient } from "@prisma/client"; import { migrationLogger } from "./migration-logger"; interface HealthCheckResult { name: string; success: boolean; duration: number; details?: Record; error?: Error; } interface SystemHealthResult { success: boolean; checks: HealthCheckResult[]; totalDuration: number; failedChecks: number; score: number; // 0-100 } export class HealthChecker { private prisma: PrismaClient; constructor() { this.prisma = new PrismaClient(); } /** * Run comprehensive health checks */ async runHealthChecks(): Promise { const startTime = Date.now(); const checks: HealthCheckResult[] = []; try { migrationLogger.startStep( "HEALTH_CHECKS", "Running comprehensive health checks" ); // Define all health checks const healthChecks = [ { name: "Database Connection", fn: () => this.checkDatabaseConnection(), }, { name: "Database Schema", fn: () => this.checkDatabaseSchema() }, { name: "tRPC Endpoints", fn: () => this.checkTRPCEndpoints() }, { name: "Batch Processing System", fn: () => this.checkBatchProcessingSystem(), }, { name: "OpenAI API Access", fn: () => this.checkOpenAIAccess() }, { name: "Environment Configuration", fn: () => this.checkEnvironmentConfiguration(), }, { name: "File System Access", fn: () => this.checkFileSystemAccess() }, { name: "Memory Usage", fn: () => this.checkMemoryUsage() }, { name: "CPU Usage", fn: () => this.checkCPUUsage() }, { name: "Application Performance", fn: () => this.checkApplicationPerformance(), }, { name: "Security Configuration", fn: () => this.checkSecurityConfiguration(), }, { name: "Logging System", fn: () => this.checkLoggingSystem() }, ]; // Run all checks for (const check of healthChecks) { const result = await this.runSingleHealthCheck(check.name, check.fn); checks.push(result); } const totalDuration = Date.now() - startTime; const failedChecks = checks.filter((c) => !c.success).length; const score = Math.round( ((checks.length - failedChecks) / checks.length) * 100 ); const result: SystemHealthResult = { success: failedChecks === 0, checks, totalDuration, failedChecks, score, }; if (result.success) { migrationLogger.completeStep("HEALTH_CHECKS"); } else { migrationLogger.failStep( "HEALTH_CHECKS", new Error(`${failedChecks} health checks failed`) ); } return result; } catch (error) { migrationLogger.error( "HEALTH_CHECKS", "Health check system failed", error as Error ); throw error; } finally { await this.prisma.$disconnect(); } } private async runSingleHealthCheck( name: string, checkFn: () => Promise<{ success: boolean; details?: Record; error?: Error; }> ): Promise { const startTime = Date.now(); try { migrationLogger.debug("HEALTH_CHECK", `Running: ${name}`); const result = await checkFn(); const duration = Date.now() - startTime; const healthResult: HealthCheckResult = { name, success: result.success, duration, details: result.details, error: result.error, }; if (result.success) { migrationLogger.debug("HEALTH_CHECK", `āœ… ${name} passed`, { duration, details: result.details, }); } else { migrationLogger.warn("HEALTH_CHECK", `āŒ ${name} failed`, { duration, error: result.error?.message, }); } return healthResult; } catch (error) { const duration = Date.now() - startTime; migrationLogger.error( "HEALTH_CHECK", `šŸ’„ ${name} crashed`, error as Error, { duration } ); return { name, success: false, duration, error: error as Error, }; } } private async checkDatabaseConnection(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { const startTime = Date.now(); await this.prisma.$queryRaw`SELECT 1`; const queryTime = Date.now() - startTime; // Test multiple connections const connectionTests = await Promise.all([ this.prisma.$queryRaw`SELECT 1`, this.prisma.$queryRaw`SELECT 1`, this.prisma.$queryRaw`SELECT 1`, ]); return { success: connectionTests.length === 3, details: { queryTime, connectionPoolTest: "passed", }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkDatabaseSchema(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { // Check critical tables const tableChecks = await Promise.allSettled([ this.prisma.company.findFirst(), this.prisma.user.findFirst(), this.prisma.session.findFirst(), this.prisma.aIBatchRequest.findFirst(), this.prisma.aIProcessingRequest.findFirst(), ]); const failedTables = tableChecks.filter( (result) => result.status === "rejected" ).length; // Check for critical indexes const indexCheck = await this.prisma.$queryRaw<{ count: string }[]>` SELECT COUNT(*) as count FROM pg_indexes WHERE tablename IN ('Session', 'AIProcessingRequest', 'AIBatchRequest') `; const indexCount = parseInt(indexCheck[0]?.count || "0"); return { success: failedTables === 0, details: { accessibleTables: tableChecks.length - failedTables, totalTables: tableChecks.length, indexes: indexCount, }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkTRPCEndpoints(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { const baseUrl = process.env.NEXTAUTH_URL || "http://localhost:3000"; // Test tRPC endpoint accessibility const endpoints = [ `${baseUrl}/api/trpc/auth.getSession`, `${baseUrl}/api/trpc/dashboard.getMetrics`, ]; const results = await Promise.allSettled( endpoints.map(async (url) => { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ json: null }), }); return { url, status: response.status }; }) ); const successfulEndpoints = results.filter( (result) => result.status === "fulfilled" && (result.value.status === 200 || result.value.status === 401 || result.value.status === 403) ).length; return { success: successfulEndpoints > 0, details: { testedEndpoints: endpoints.length, successfulEndpoints, endpoints: results.map((r) => r.status === "fulfilled" ? r.value : { error: r.reason.message } ), }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkBatchProcessingSystem(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { // Check batch processing components const batchEnabled = process.env.BATCH_PROCESSING_ENABLED === "true"; // Test database components const batchRequestsCount = await this.prisma.aIBatchRequest.count(); const processingRequestsCount = await this.prisma.aIProcessingRequest.count(); // Check if batch processor can be imported let batchProcessorAvailable = false; try { await import("../../lib/batchProcessor"); batchProcessorAvailable = true; } catch { // Batch processor not available } // Check batch status distribution const batchStatuses = await this.prisma.aIBatchRequest.groupBy({ by: ["status"], _count: { status: true }, }); return { success: batchEnabled && batchProcessorAvailable, details: { enabled: batchEnabled, processorAvailable: batchProcessorAvailable, batchRequests: batchRequestsCount, processingRequests: processingRequestsCount, statusDistribution: Object.fromEntries( batchStatuses.map((s) => [s.status, s._count.status]) ), }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkOpenAIAccess(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { const apiKey = process.env.OPENAI_API_KEY; const mockMode = process.env.OPENAI_MOCK_MODE === "true"; if (mockMode) { return { success: true, details: { mode: "mock", available: true }, }; } if (!apiKey) { return { success: false, error: new Error("OPENAI_API_KEY not configured"), }; } // Test API with a simple request const response = await fetch("https://api.openai.com/v1/models", { headers: { Authorization: `Bearer ${apiKey}`, }, }); const responseTime = Date.now(); return { success: response.ok, details: { mode: "live", available: response.ok, status: response.status, responseTime: responseTime, }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkEnvironmentConfiguration(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { const requiredVars = ["DATABASE_URL", "NEXTAUTH_SECRET", "NEXTAUTH_URL"]; const missingVars = requiredVars.filter( (varName) => !process.env[varName] ); const newVars = [ "BATCH_PROCESSING_ENABLED", "TRPC_ENDPOINT_URL", "BATCH_CREATE_INTERVAL", ]; const missingNewVars = newVars.filter((varName) => !process.env[varName]); return { success: missingVars.length === 0, details: { requiredVarsPresent: requiredVars.length - missingVars.length, totalRequiredVars: requiredVars.length, newVarsPresent: newVars.length - missingNewVars.length, totalNewVars: newVars.length, missingRequired: missingVars, missingNew: missingNewVars, }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkFileSystemAccess(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { const fs = await import("node:fs/promises"); const path = await import("node:path"); // Test write access to logs directory const logsDir = path.join(process.cwd(), "logs"); const testFile = path.join(logsDir, "health-check.tmp"); try { await fs.mkdir(logsDir, { recursive: true }); await fs.writeFile(testFile, "health check"); await fs.unlink(testFile); } catch (error) { return { success: false, error: new Error( `Cannot write to logs directory: ${(error as Error).message}` ), }; } // Test read access to package.json try { await fs.access(path.join(process.cwd(), "package.json")); } catch (error) { return { success: false, error: new Error("Cannot access package.json"), }; } return { success: true, details: { logsWritable: true, packageJsonReadable: true, }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkMemoryUsage(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { const memUsage = process.memoryUsage(); const usedMB = Math.round(memUsage.heapUsed / 1024 / 1024); const totalMB = Math.round(memUsage.heapTotal / 1024 / 1024); const externalMB = Math.round(memUsage.external / 1024 / 1024); // Consider memory healthy if heap usage is under 80% of total const usagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100; const healthy = usagePercent < 80; return { success: healthy, details: { heapUsed: usedMB, heapTotal: totalMB, external: externalMB, usagePercent: Math.round(usagePercent), }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkCPUUsage(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { const cpuUsage = process.cpuUsage(); const userTime = cpuUsage.user / 1000; // Convert to milliseconds const systemTime = cpuUsage.system / 1000; // Simple CPU health check - process should be responsive const startTime = Date.now(); await new Promise((resolve) => setTimeout(resolve, 10)); const responseTime = Date.now() - startTime; return { success: responseTime < 50, // Should respond within 50ms details: { userTime, systemTime, responseTime, }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkApplicationPerformance(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { // Test database query performance const dbStartTime = Date.now(); await this.prisma.company.findFirst(); const dbQueryTime = Date.now() - dbStartTime; // Test complex query performance const complexStartTime = Date.now(); await this.prisma.session.findMany({ include: { messages: { take: 5 }, processingStatus: true, }, take: 10, }); const complexQueryTime = Date.now() - complexStartTime; return { success: dbQueryTime < 100 && complexQueryTime < 500, details: { simpleQueryTime: dbQueryTime, complexQueryTime: complexQueryTime, performanceGood: dbQueryTime < 100 && complexQueryTime < 500, }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkSecurityConfiguration(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { const securityIssues: string[] = []; // Check NEXTAUTH_SECRET strength const secret = process.env.NEXTAUTH_SECRET; if (!secret || secret.length < 32) { securityIssues.push("Weak NEXTAUTH_SECRET"); } // Check if using secure URLs in production if (process.env.NODE_ENV === "production") { const url = process.env.NEXTAUTH_URL; if (url && !url.startsWith("https://")) { securityIssues.push("Non-HTTPS URL in production"); } } // Check rate limiting configuration if (!process.env.RATE_LIMIT_WINDOW_MS) { securityIssues.push("Rate limiting not configured"); } return { success: securityIssues.length === 0, details: { securityIssues, hasSecret: !!secret, rateLimitConfigured: !!process.env.RATE_LIMIT_WINDOW_MS, }, }; } catch (error) { return { success: false, error: error as Error, }; } } private async checkLoggingSystem(): Promise<{ success: boolean; details?: Record; error?: Error; }> { try { // Test if logging works const testMessage = `Health check test ${Date.now()}`; migrationLogger.debug("HEALTH_TEST", testMessage); // Check if log directory exists and is writable const fs = await import("node:fs"); const path = await import("node:path"); const logsDir = path.join(process.cwd(), "logs"); const logsDirExists = fs.existsSync(logsDir); return { success: logsDirExists, details: { logsDirExists, testMessageLogged: true, }, }; } catch (error) { return { success: false, error: error as Error, }; } } /** * Generate health report */ generateHealthReport(result: SystemHealthResult): string { const report = ` # System Health Report **Overall Status**: ${result.success ? "āœ… Healthy" : "āŒ Unhealthy"} **Health Score**: ${result.score}/100 **Total Duration**: ${result.totalDuration}ms **Failed Checks**: ${result.failedChecks}/${result.checks.length} ## Health Check Results ${result.checks .map( (check) => ` ### ${check.name} - **Status**: ${check.success ? "āœ… Pass" : "āŒ Fail"} - **Duration**: ${check.duration}ms ${check.details ? `- **Details**: ${JSON.stringify(check.details, null, 2)}` : ""} ${check.error ? `- **Error**: ${check.error.message}` : ""} ` ) .join("")} ## Summary ${ result.success ? "šŸŽ‰ All health checks passed! The system is operating normally." : `āš ļø ${result.failedChecks} health check(s) failed. Please review and address the issues above.` } --- *Generated at ${new Date().toISOString()}* `; return report; } } // CLI interface if (import.meta.url === `file://${process.argv[1]}`) { const healthChecker = new HealthChecker(); const generateReport = process.argv.includes("--report"); healthChecker .runHealthChecks() .then((result) => { console.log("\n=== SYSTEM HEALTH CHECK RESULTS ==="); console.log( `Overall Health: ${result.success ? "āœ… Healthy" : "āŒ Unhealthy"}` ); console.log(`Health Score: ${result.score}/100`); console.log(`Total Duration: ${result.totalDuration}ms`); console.log( `Failed Checks: ${result.failedChecks}/${result.checks.length}` ); console.log("\n=== INDIVIDUAL CHECKS ==="); for (const check of result.checks) { const status = check.success ? "āœ…" : "āŒ"; console.log(`${status} ${check.name} (${check.duration}ms)`); if (check.details) { console.log(` Details:`, check.details); } if (check.error) { console.log(` Error: ${check.error.message}`); } } if (generateReport) { const report = healthChecker.generateHealthReport(result); const fs = require("node:fs"); const reportPath = `health-report-${Date.now()}.md`; fs.writeFileSync(reportPath, report); console.log(`\nšŸ“‹ Health report saved to: ${reportPath}`); } process.exit(result.success ? 0 : 1); }) .catch((error) => { console.error("Health checks failed:", error); process.exit(1); }); }