Bug fix (agreed pre-existing bug #3 of 3): the .catch() branch of localDb.getLocalDbStats() referenced status/roaAge, both only declared inside the sibling .then() callback's closure -- out of scope in .catch(), so any real DB-stats failure threw ReferenceError with no further catch, hanging the response until client timeout instead of degrading gracefully. Fixed: roaAge computed once before the promise chain (same pattern as the existing aspaAge); the .catch() branch gets its own locally-scoped `status = "degraded"` (a DB-stats fetch failure can't confirm "ok", matching the honest-degradation pattern already used throughout this codebase). Also deduplicated the identical delta_last computation that was repeated verbatim in both branches into one aspaAdoptionDelta() helper. Verified via node --check, eslint no-undef (2 known bugs remain: compare, webhooks), and smoke-test harness (28/28 match). server.js: 3556 -> 3346 lines.
105 lines
4.8 KiB
JavaScript
105 lines
4.8 KiB
JavaScript
const localDb = require("../../local-db-client");
|
|
const { getRpkiAspaLastFetch, rpkiAspaMap } = require("../services/rpki");
|
|
const { pdbSourceCache } = require("../caches/pdb-source-cache");
|
|
const { roaStore } = require("../caches/roa-store");
|
|
const { ripeStatCache } = require("../services/ripe-stat");
|
|
const { responseCache } = require("../caches/response-caches");
|
|
const { aspaAdoptionDailyHistory } = require("../aspa-adoption/tracker");
|
|
|
|
const BGPROUTES_API_KEY = process.env.BGPROUTES_API_KEY || "";
|
|
|
|
function aspaAdoptionDelta() {
|
|
return aspaAdoptionDailyHistory.length >= 2
|
|
? aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1].aspa_objects - aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2].aspa_objects
|
|
: 0;
|
|
}
|
|
|
|
// Health endpoint — extended with cache status, ASPA metrics, and local DB stats
|
|
function handleHealth(req, res) {
|
|
const mem = process.memoryUsage();
|
|
const rpkiAspaLastFetch = getRpkiAspaLastFetch();
|
|
const aspaAge = rpkiAspaLastFetch ? Math.floor((Date.now() - rpkiAspaLastFetch) / 60000) : -1;
|
|
// Bug fix 2026-07-17: previously only declared inside the .then() callback
|
|
// below, so the .catch() branch's reference to it threw ReferenceError
|
|
// (never caught -- the response just hung until client timeout).
|
|
const roaAge = roaStore.lastBuild ? Math.floor((Date.now() - roaStore.lastBuild) / 60000) : -1;
|
|
const pdbTotal = pdbSourceCache.hits + pdbSourceCache.misses;
|
|
|
|
// Query local DB stats (async, but return partial if needed)
|
|
localDb.getLocalDbStats().then(function(dbStats) {
|
|
// Determine health status based on local DB data availability
|
|
const hasLocalBgp = dbStats && dbStats.bgp_routes > 100000; // should have >2M rows normally
|
|
const hasLocalRpki = dbStats && dbStats.rpki_roas > 100000; // should have >500k rows normally
|
|
const status = (hasLocalBgp && hasLocalRpki && aspaAge < 300) ? "ok" : "degraded";
|
|
|
|
const healthResponse = {
|
|
status,
|
|
service: "PeerCortex",
|
|
version: "0.6.9",
|
|
timestamp: new Date().toISOString(),
|
|
uptime_seconds: Math.floor(process.uptime()),
|
|
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
|
bgproutes_configured: !!BGPROUTES_API_KEY,
|
|
caches: {
|
|
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
|
|
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
|
|
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
|
|
pdb_netfac: { entries: pdbSourceCache.netfac.size },
|
|
ripe_stat: { entries: ripeStatCache.size },
|
|
response_cache: { entries: responseCache.size },
|
|
},
|
|
local_db: dbStats ? {
|
|
bgp_routes: dbStats.bgp_routes,
|
|
rpki_roas: dbStats.rpki_roas,
|
|
threat_intel: dbStats.threat_intel,
|
|
rdap_cache_entries: dbStats.rdap_cache_entries,
|
|
source: "PostgreSQL (local)",
|
|
healthy: hasLocalBgp && hasLocalRpki,
|
|
} : null,
|
|
aspa_adoption: {
|
|
total_objects: rpkiAspaMap.size,
|
|
roa_count: roaStore.count,
|
|
history_samples: aspaAdoptionDailyHistory.length,
|
|
delta_last: aspaAdoptionDelta(),
|
|
},
|
|
};
|
|
return res.end(JSON.stringify(healthResponse, null, 2));
|
|
}).catch(function(e) {
|
|
console.error('[/api/health] Local DB stats error:', e.message);
|
|
// Return health without local DB stats on error. Local DB stats failed
|
|
// to fetch, so "ok" can't be confirmed -- report degraded, matching the
|
|
// honest-degradation pattern used throughout this codebase.
|
|
const status = "degraded";
|
|
return res.end(
|
|
JSON.stringify({
|
|
status,
|
|
service: "PeerCortex",
|
|
version: "0.6.9",
|
|
timestamp: new Date().toISOString(),
|
|
uptime_seconds: Math.floor(process.uptime()),
|
|
memory_mb: Math.round(mem.heapUsed / 1024 / 1024),
|
|
bgproutes_configured: !!BGPROUTES_API_KEY,
|
|
caches: {
|
|
roa_store: { entries: roaStore.count, age_minutes: roaAge, ready: roaStore.ready },
|
|
aspa_map: { entries: rpkiAspaMap.size, age_minutes: aspaAge },
|
|
pdb_net: { entries: pdbSourceCache.net.size, hit_rate_pct: pdbTotal > 0 ? Math.round(pdbSourceCache.hits / pdbTotal * 100) : 0 },
|
|
pdb_netixlan: { entries: pdbSourceCache.netixlan.size },
|
|
pdb_netfac: { entries: pdbSourceCache.netfac.size },
|
|
ripe_stat: { entries: ripeStatCache.size },
|
|
response_cache: { entries: responseCache.size },
|
|
},
|
|
local_db: { error: "Could not fetch local DB stats", message: e.message },
|
|
aspa_adoption: {
|
|
total_objects: rpkiAspaMap.size,
|
|
roa_count: roaStore.count,
|
|
history_samples: aspaAdoptionDailyHistory.length,
|
|
delta_last: aspaAdoptionDelta(),
|
|
},
|
|
}, null, 2)
|
|
);
|
|
});
|
|
return;
|
|
}
|
|
|
|
module.exports = { handleHealth };
|