bae705aa97
- 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
168 lines
5.4 KiB
JavaScript
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', {});
|
|
}
|