Files
boc/landvex-finance-sprint3/health.mjs
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- Add NFC ePassport roadmap (ICAO 9303, eIDAS)
- Add TensorFlow.js edge face detection (BlazeFace)
- Add structured audit logger (GDPR-compliant)
- Risk scoring support

Part of KYC Apple Native UX v1.1.0
2026-06-29 16:24:48 +00:00

168 lines
5.4 KiB
JavaScript

/**
* LandveX Finance — Förbättrad Health Check
* Sprint 3: Observability
*
* Kontrollerar: PostgreSQL, Redis, Disk, Ledger-tjänst, Revolut API
*/
import { financeInfo, financeError } from './logger.mjs';
import { healthCheckDuration, diskGauge } from './metrics.mjs';
let _cache = null;
let _cacheTime = 0;
const CACHE_TTL = 30000; // 30 sekunder
export async function financeHealthHandler(req, res) {
const start = Date.now();
// Returnera cache om giltig
if (_cache && Date.now() - _cacheTime < CACHE_TTL) {
return res.status(_cache.status === 'healthy' ? 200 : _cache.status === 'degraded' ? 200 : 503).json(_cache);
}
const checks = {};
let status = 'healthy';
// ── 1. PostgreSQL (Ledger DB) ──
try {
const { Pool } = await import('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL || process.env.LEDGER_DB_URL,
ssl: { rejectUnauthorized: false },
max: 2,
connectionTimeoutMillis: 3000,
});
const dbStart = Date.now();
await pool.query('SELECT 1');
const dbDur = Date.now() - dbStart;
checks.database = { status: 'healthy', response_ms: dbDur };
healthCheckDuration.observe({ check: 'database' }, dbDur);
await pool.end();
} catch (e) {
checks.database = { status: 'unhealthy', error: e.message };
status = 'unhealthy';
financeError('health_check_failed', { error: e, context: { check: 'database' } });
}
// ── 2. Redis ──
try {
const { default: Redis } = await import('ioredis');
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
connectTimeout: 3000,
maxRetriesPerRequest: 1,
});
const redisStart = Date.now();
await redis.ping();
const redisDur = Date.now() - redisStart;
checks.redis = { status: 'healthy', response_ms: redisDur };
healthCheckDuration.observe({ check: 'redis' }, redisDur);
redis.disconnect();
} catch (e) {
checks.redis = { status: 'unhealthy', error: e.message };
if (status === 'healthy') status = 'degraded';
financeError('health_check_failed', { error: e, context: { check: 'redis' } });
}
// ── 3. Disk ──
try {
const { statfs } = await import('fs');
const stats = await statfs('/opt/amos/data');
const freeGB = (stats.bavail * stats.bsize) / (1024 ** 3);
const totalGB = (stats.blocks * stats.bsize) / (1024 ** 3);
const usedPct = parseFloat(((totalGB - freeGB) / totalGB * 100).toFixed(1));
const diskStatus = freeGB < 1 ? 'critical' : freeGB < 5 ? 'warning' : 'healthy';
checks.disk = {
status: diskStatus,
free_gb: Math.round(freeGB * 100) / 100,
total_gb: Math.round(totalGB * 100) / 100,
used_percent: usedPct,
};
diskGauge.set({ path: '/opt/amos/data' }, usedPct);
if (diskStatus === 'critical') status = 'unhealthy';
else if (diskStatus === 'warning' && status === 'healthy') status = 'degraded';
} catch (e) {
checks.disk = { status: 'unknown', error: e.message };
}
// ── 4. Ledger Service (intern) ──
try {
const ledgerStart = Date.now();
const ledgerBase = process.env.LEDGER_BASE || 'http://localhost:3250';
const r = await fetch(`${ledgerBase}/health`, {
signal: AbortSignal.timeout(3000),
});
const ledgerDur = Date.now() - ledgerStart;
checks.ledger = {
status: r.ok ? 'healthy' : 'unhealthy',
response_ms: ledgerDur,
};
healthCheckDuration.observe({ check: 'ledger' }, ledgerDur);
if (!r.ok && status === 'healthy') status = 'degraded';
} catch (e) {
checks.ledger = { status: 'unhealthy', error: e.message };
if (status === 'healthy') status = 'degraded';
financeError('health_check_failed', { error: e, context: { check: 'ledger' } });
}
// ── 5. Revolut API (extern) ──
try {
const revStart = Date.now();
// Försök hämta token (från ledger-proxy.mjs logik)
const revClientId = process.env.REVOLUT_CLIENT_ID;
const revRefreshToken = process.env.REVOLUT_REFRESH_TOKEN;
const revPrivKeyPath = process.env.REVOLUT_PRIVATE_KEY_PATH;
let revStatus = 'unknown';
let revAuth = false;
if (revClientId && revRefreshToken && revPrivKeyPath) {
// Vi försöker inte faktiskt autentisera här (tar för lång tid)
// Utan kollar bara att konfiguration finns
revStatus = 'configured';
revAuth = true;
} else {
revStatus = 'not_configured';
}
checks.revolut = {
status: revStatus === 'configured' ? 'healthy' : 'degraded',
response_ms: Date.now() - revStart,
authenticated: revAuth,
};
if (revStatus === 'not_configured' && status === 'healthy') {
status = 'degraded';
}
} catch (e) {
checks.revolut = { status: 'unhealthy', error: e.message };
}
const result = {
service: 'finance',
status,
timestamp: new Date().toISOString(),
response_ms: Date.now() - start,
checks,
};
// Cachea resultatet
_cache = result;
_cacheTime = Date.now();
financeInfo('health_check_completed', {
duration: result.response_ms,
context: { status, checks_count: Object.keys(checks).length },
});
res.status(status === 'healthy' ? 200 : status === 'degraded' ? 200 : 503).json(result);
}
// Manuell cache-invalidering (t.ex. efter deployment)
export function invalidateHealthCache() {
_cache = null;
_cacheTime = 0;
financeInfo('health_cache_invalidated', {});
}