mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user