/*********************************************************************************************************************************************************************** * File Name: health.service.js * Type of Program: Service * Description: Health check registry — runs all dependency checks and builds two response formats. * * runDashboard() → rich human-readable payload (for GET /api/health) * runReadiness() → compact machine payload (for GET /api/health/ready) * * Both call runChecks() internally and share the same check results. * * Check registry (CHECKS array): * Each entry is self-contained — name, criticality, enable condition, metadata, and runner. * To add a new infrastructure check: push one entry here. Nothing else changes. * * Criticality model: * critical: true → failure → overall "unhealthy" → HTTP 503 * critical: false → failure → overall "degraded" → HTTP 200 * * Checks included: * ✓ database — PostgreSQL via Sequelize (CRITICAL) * ✓ cache — Redis PING; skipped in memory mode (optional) * ✓ storage — S3/Garage HeadBucket (optional) * ✓ email — Gmail API profile fetch (OAuth2) (optional) * * Checks intentionally excluded: * ✗ chibisafe — third-party CDN; not owned infrastructure * ✗ paypal — third-party payment gateway; not owned infrastructure * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 20, 2026 ***********************************************************************************************************************************************************************/ "use strict"; const os = require('os'); const sequelize = require('../config/db.config'); const redis = require('../config/redis.config'); const { ping: pingS3 } = require('./s3.service'); const { ping: pingEmail } = require('./email.service'); const PKG = require('../package.json'); const TIMEOUT_MS = 3000; // ─── Connection status labels (internal status → human label) ───────────────── const CONNECTION_LABEL = { healthy: 'established', unhealthy: 'unavailable', timeout: 'timeout', skipped: 'not configured', }; // ─── Check registry ─────────────────────────────────────────────────────────── // // Fields: // name {string} — key used in response JSON // critical {boolean} — true: 503 on failure; false: degraded (200) // enabled {boolean} — false: skipped entirely // skipNote {string} — shown in dashboard when enabled = false // meta {object} — extra context shown in the dashboard (provider, host, port, etc.) // run {async fn} — throws on failure, resolves on success const CHECKS = [ { name: 'database', critical: true, enabled: true, meta: { dialect: 'postgresql', host: process.env.DB_HOST, port: Number(process.env.DB_PORT) || 5432 }, run: () => sequelize.authenticate(), }, { name: 'cache', critical: false, enabled: !!redis, skipNote: 'CACHE_DRIVER=memory — Redis is not used in this environment', meta: { driver: 'redis', url: process.env.REDIS_URL || 'redis://127.0.0.1:6379' }, run: () => redis.ping(), }, { name: 'storage', critical: false, enabled: !!(process.env.S3_ENDPOINT || process.env.S3_PUBLIC_URL), skipNote: 'S3_ENDPOINT is not configured', meta: { provider: 's3', endpoint: process.env.S3_PUBLIC_URL ?? process.env.S3_ENDPOINT, bucket: process.env.S3_BUCKET }, run: pingS3, }, { name: 'email', critical: false, enabled: !!(process.env.GOOGLE_CLIENT_ID && process.env.GMAIL_REFRESH_TOKEN), skipNote: 'GMAIL_REFRESH_TOKEN is not configured', meta: { provider: 'gmail-api', account: process.env.EMAIL_FROM }, run: pingEmail, }, ]; // ─── Internal helpers ───────────────────────────────────────────────────────── const mb = (bytes) => Math.round(bytes / 1024 / 1024 * 10) / 10; function formatUptime(seconds) { if (seconds < 60) return `${seconds} secs`; if (seconds < 3600) return `${Math.floor(seconds / 60)} mins`; if (seconds < 86400) return `${Math.floor(seconds / 3600)} hours`; if (seconds < 2592000) return `${Math.floor(seconds / 86400)} days`; if (seconds < 31536000) return `${Math.floor(seconds / 2592000)} months`; return `${Math.floor(seconds / 31536000)} years`; } function memoryInfo() { const m = process.memoryUsage(); return { heap_used_mb: mb(m.heapUsed), heap_total_mb: mb(m.heapTotal), rss_mb: mb(m.rss), external_mb: mb(m.external), }; } function systemInfo() { const load = os.loadavg(); return { platform: process.platform, arch: process.arch, cpus: os.cpus().length, load_avg: { '1m': Math.round(load[0] * 100) / 100, '5m': Math.round(load[1] * 100) / 100, '15m': Math.round(load[2] * 100) / 100, }, memory: memoryInfo(), }; } function appInfo() { return { name: PKG.name, version: PKG.version, environment: process.env.NODE_ENV || 'development', node_version: process.version, pid: process.pid, }; } // ─── Check runner ───────────────────────────────────────────────────────────── async function runCheck(check) { const { name, critical, enabled, skipNote, run } = check; if (!enabled) { return [name, { status: 'skipped', note: skipNote, critical }]; } const start = Date.now(); try { await Promise.race([ run(), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), TIMEOUT_MS) ), ]); return [name, { status: 'healthy', latency_ms: Date.now() - start, critical }]; } catch (err) { return [name, { status: err.message === 'timeout' ? 'timeout' : 'unhealthy', latency_ms: Date.now() - start, critical, }]; } } async function runChecks() { const results = await Promise.all(CHECKS.map(runCheck)); const checks = Object.fromEntries(results); const criticalFailed = results.some( ([, r]) => r.critical && r.status !== 'healthy' && r.status !== 'skipped' ); const anyDegraded = results.some( ([, r]) => !r.critical && r.status !== 'healthy' && r.status !== 'skipped' ); const overallStatus = criticalFailed ? 'unhealthy' : anyDegraded ? 'degraded' : 'healthy'; return { checks, criticalFailed, overallStatus }; } // ─── Dashboard payload (GET /api/health) ───────────────────────────────────── // // Human-readable — full context: app info, system info, service connection details. // Uses "established / unavailable / timeout / not configured" language. async function runDashboard() { const { checks, criticalFailed, overallStatus } = await runChecks(); const services = {}; for (const check of CHECKS) { const result = checks[check.name]; services[check.name] = { connection: CONNECTION_LABEL[result.status] ?? result.status, ...(result.latency_ms !== undefined && { latency_ms: result.latency_ms }), ...(result.note !== undefined && { note: result.note }), ...(check.meta !== undefined && check.meta), critical: result.critical, }; } return { httpStatus: criticalFailed ? 503 : 200, body: { status: overallStatus, app: appInfo(), uptime: formatUptime(Math.floor(process.uptime())), timestamp: new Date().toISOString(), system: systemInfo(), services, }, }; } // ─── Readiness payload (GET /api/health/ready) ─────────────────────────────── // // Compact — intended for machines (Kubernetes probe, deployment scripts, CI gates). // Uses "healthy / unhealthy / timeout / skipped" language. async function runReadiness() { const { checks, criticalFailed, overallStatus } = await runChecks(); return { httpStatus: criticalFailed ? 503 : 200, body: { status: overallStatus, version: PKG.version, uptime: formatUptime(Math.floor(process.uptime())), timestamp: new Date().toISOString(), checks, memory: memoryInfo(), }, }; } module.exports = { runDashboard, runReadiness };