mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
245 lines
8.7 KiB
JavaScript
245 lines
8.7 KiB
JavaScript
/***********************************************************************************************************************************************************************
|
|
* 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 };
|