/** * 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', {}); }