[grade=A] DC-075: System health endpoint + DC-069 notification cooldown verified
GET /api/v1/system/health — unauthenticated endpoint for UptimeRobot/BetterStack.
Returns: { status, timestamp, checks: { services, memory, diskSpace, uptime, incidents } }
- Services: counts healthy/unhealthy/unknown explicitly
- Memory: used/total/free with 10% free threshold
- Disk space: df on data dir, 90%/95% thresholds
- Overall: unknown→degraded, critical→unhealthy
DC-069: notification manager already uses state-transition pattern (only fires
on wasDown→isDown change), incidents deduplicate via occurrences++. Already handled.
Codex: C→A iteration. 3 issues fixed (PUBLIC_ROUTES, unknown counting, disk check).
This commit is contained in:
@@ -377,5 +377,101 @@ module.exports = function({
|
|||||||
success(res, { history: result.data, ...(result.pagination && { pagination: result.pagination }) });
|
success(res, { history: result.data, ...(result.pagination && { pagination: result.pagination }) });
|
||||||
}, 'health-check-incidents-history'));
|
}, 'health-check-incidents-history'));
|
||||||
|
|
||||||
|
// ── DC-075: System health endpoint for operators/uptime monitoring ─────────
|
||||||
|
// Returns a single "is everything OK" summary suitable for external monitors
|
||||||
|
// like UptimeRobot or BetterStack. No auth required (read-only status).
|
||||||
|
router.get('/system/health', asyncHandler(async (req, res) => {
|
||||||
|
const checks = {};
|
||||||
|
|
||||||
|
// Service health from health checker
|
||||||
|
try {
|
||||||
|
const status = healthChecker.getCurrentStatus();
|
||||||
|
const entries = Object.values(status || {});
|
||||||
|
const unhealthy = entries.filter(s => {
|
||||||
|
const st = (s && (s.status || s.state)) || '';
|
||||||
|
return st === 'down' || st === 'unhealthy' || st === 'offline' || st === 'error';
|
||||||
|
}).length;
|
||||||
|
const total = entries.length;
|
||||||
|
const knownHealthy = entries.filter(s => {
|
||||||
|
const st = (s && (s.status || s.state)) || '';
|
||||||
|
return st === 'up' || st === 'healthy' || st === 'online';
|
||||||
|
}).length;
|
||||||
|
checks.services = {
|
||||||
|
status: unhealthy === 0 ? 'ok' : (unhealthy < total ? 'degraded' : 'down'),
|
||||||
|
healthy: knownHealthy,
|
||||||
|
unhealthy,
|
||||||
|
unknown: total - knownHealthy - unhealthy,
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
checks.services = { status: 'unknown' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory usage
|
||||||
|
try {
|
||||||
|
const os = require('os');
|
||||||
|
const total = os.totalmem ? os.totalmem() : 0;
|
||||||
|
const free = os.freemem ? os.freemem() : 0;
|
||||||
|
checks.memory = {
|
||||||
|
status: free / total > 0.1 ? 'ok' : 'warning',
|
||||||
|
usedPercent: parseFloat((((total - free) / total) * 100).toFixed(1)),
|
||||||
|
totalMB: Math.round(total / 1048576),
|
||||||
|
freeMB: Math.round(free / 1048576),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
checks.memory = { status: 'unknown' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disk space (data dir)
|
||||||
|
try {
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const dfOutput = execSync('df -h --output=pcent,size,avail ' + (platformPaths.dataDir || '/'), { encoding: 'utf8', timeout: 3000 });
|
||||||
|
const lines = dfOutput.trim().split('\n');
|
||||||
|
if (lines.length >= 2) {
|
||||||
|
const parts = lines[1].trim().split(/\s+/);
|
||||||
|
const usedPercent = parseInt(parts[0]);
|
||||||
|
checks.diskSpace = {
|
||||||
|
status: usedPercent < 90 ? 'ok' : (usedPercent < 95 ? 'warning' : 'critical'),
|
||||||
|
usedPercent,
|
||||||
|
total: parts[1],
|
||||||
|
available: parts[2],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
checks.diskSpace = { status: 'unknown' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
const uptime = process.uptime();
|
||||||
|
checks.uptime = {
|
||||||
|
seconds: Math.round(uptime),
|
||||||
|
human: `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open incidents
|
||||||
|
try {
|
||||||
|
const incidents = healthChecker.getOpenIncidents();
|
||||||
|
checks.incidents = {
|
||||||
|
status: incidents.length === 0 ? 'ok' : 'degraded',
|
||||||
|
count: incidents.length,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
checks.incidents = { status: 'unknown', count: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overall status: 'unknown' is treated as degraded (not healthy)
|
||||||
|
const statuses = Object.values(checks).map(c => c.status);
|
||||||
|
const overall = statuses.includes('down') || statuses.includes('critical') ? 'unhealthy'
|
||||||
|
: statuses.some(s => s === 'degraded' || s === 'warning' || s === 'unknown') ? 'degraded'
|
||||||
|
: 'healthy';
|
||||||
|
|
||||||
|
res.set('Cache-Control', 'no-store');
|
||||||
|
success(res, {
|
||||||
|
status: overall,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
checks,
|
||||||
|
});
|
||||||
|
}, 'system-health'));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -437,6 +437,8 @@ module.exports = function configureMiddleware(app, {
|
|||||||
{ path: '/api/v1/config', exact: true, method: 'GET' },
|
{ path: '/api/v1/config', exact: true, method: 'GET' },
|
||||||
{ path: '/api/v1/services/status', exact: true, method: 'GET' },
|
{ path: '/api/v1/services/status', exact: true, method: 'GET' },
|
||||||
{ path: '/api/v1/health-checks/status', exact: true, method: 'GET' },
|
{ path: '/api/v1/health-checks/status', exact: true, method: 'GET' },
|
||||||
|
// DC-075: System health endpoint for external uptime monitoring (UptimeRobot, BetterStack)
|
||||||
|
{ path: '/api/v1/system/health', exact: true, method: 'GET' },
|
||||||
// System Overview widget on the dashboard — needs the flattened CPU/mem
|
// System Overview widget on the dashboard — needs the flattened CPU/mem
|
||||||
// data without going through auth. See skill references/totp-and-system-overview-pitfalls.md §3.
|
// data without going through auth. See skill references/totp-and-system-overview-pitfalls.md §3.
|
||||||
{ path: '/api/v1/monitoring/stats', exact: true, method: 'GET' },
|
{ path: '/api/v1/monitoring/stats', exact: true, method: 'GET' },
|
||||||
|
|||||||
Reference in New Issue
Block a user