UNPKG

express-insights

Version:

Production-ready Express middleware for backend health monitoring with metrics, HTML/JSON output, authentication, and service checks.

218 lines (196 loc) 8.18 kB
const { setupLogger } = require('./logger'); const { validateOptions } = require('./config'); const { getDefaultStats } = require('./stats'); const { renderHTML } = require('./htmlRenderer'); const { applyCorsHeaders } = require('./cors'); const rateLimitStore = new Map(); function healthCheck(app, options = {}) { const config = validateOptions(options); const { route, htmlResponse, style, message, customChecks = [], onError, onSuccess, timeout, retries, version, includeNetwork, includeDisk, authentication, rateLimit, enableMetrics, cors, cacheTTL, autoRefreshInterval, log: logConfig, resourceThresholds, checks: enabledChecks, } = config; const logger = logConfig.customLogger || setupLogger(logConfig); const cleanupInterval = setInterval(() => { const now = Date.now(); if (rateLimit) { const windowStart = now - rateLimit.windowMs; for (const [id, times] of rateLimitStore.entries()) { const active = times.filter((time) => time > windowStart); if (active.length === 0) rateLimitStore.delete(id); else rateLimitStore.set(id, active); } } }, 60000); const metrics = { totalRequests: 0, healthyResponses: 0, unhealthyResponses: 0, averageResponseTime: 0, lastChecked: null, }; const builtInChecks = []; if (enabledChecks.database) { builtInChecks.push({ name: 'database', check: async () => { await new Promise((resolve) => setTimeout(resolve, 100)); return true; }, }); } const preCheckMiddleware = async (req, res, next) => { if (!applyCorsHeaders(req, res, cors, logger)) { return res.status(403).json({ error: 'Forbidden', message: 'Origin not allowed by CORS policy', timestamp: new Date(), }); } if (rateLimit) { const clientId = req.ip || req.connection.remoteAddress; const now = Date.now(); const windowStart = now - rateLimit.windowMs; const requests = (rateLimitStore.get(clientId) || []).filter((t) => t > windowStart); if (requests.length >= rateLimit.max) { logger.warn(`Rate limit exceeded for ${clientId}`, { route }); res.setHeader('Retry-After', Math.ceil(rateLimit.windowMs / 1000)); return res.status(429).json({ error: 'Too Many Requests', message: `Rate limit exceeded. Try again after ${rateLimit.windowMs / 1000} seconds`, }); } requests.push(now); rateLimitStore.set(clientId, requests); } if (authentication) { const authHeader = req.headers.authorization || req.query.apiKey; let isAuthorized = false; if (typeof authentication === 'function') { isAuthorized = await authentication(authHeader); } else if (typeof authentication === 'string') { isAuthorized = authHeader === authentication; } if (!isAuthorized) { logger.warn(`Unauthorized access attempt to ${route}`, { ip: req.ip }); return res.status(401).json({ error: 'Unauthorized', message: 'Invalid authentication credentials', }); } } next(); }; app.get(route, preCheckMiddleware, async (req, res) => { const startTime = Date.now(); logger.info(`Health check request received from ${req.ip}`, { route, timestamp: new Date() }); try { const result = await getDefaultStats({ version, includeNetwork, includeDisk, cacheTTL, resourceThresholds }); const allChecks = [...customChecks, ...builtInChecks]; if (allChecks.length > 0) { result.services = {}; const checkPromises = allChecks.map(async (checkConfig) => { const { name, check, timeout: checkTimeout = timeout, retries: checkRetries = retries } = checkConfig; for (let attempt = 1; attempt <= checkRetries + 1; attempt++) { try { const checkPromise = Promise.resolve(check()); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Check timeout for ${name}`)), checkTimeout) ); await Promise.race([checkPromise, timeoutPromise]); result.services[name] = 'healthy'; logger.info(`Service ${name} check passed`, { attempt }); return; } catch (err) { if (attempt > checkRetries) { result.services[name] = 'unhealthy'; logger.error(`Service ${name} check failed`, { error: err.message, attempt }); if (typeof onError === 'function') onError(err, name, attempt); } } } }); await Promise.all(checkPromises); const serviceStatuses = Object.values(result.services); if (serviceStatuses.every((s) => s === 'unhealthy') && serviceStatuses.length > 0) { result.status = 'unhealthy'; } else if (serviceStatuses.some((s) => s === 'unhealthy')) { result.status = 'degraded'; } } if (enableMetrics) { metrics.totalRequests++; metrics.lastChecked = new Date().toISOString(); result.status.startsWith('un') ? metrics.unhealthyResponses++ : metrics.healthyResponses++; const responseTime = Date.now() - startTime; metrics.averageResponseTime = (metrics.averageResponseTime * (metrics.totalRequests - 1) + responseTime) / metrics.totalRequests; result.metrics = { ...metrics, averageResponseTime: `${metrics.averageResponseTime.toFixed(2)}ms` }; } if (typeof onSuccess === 'function') onSuccess(result); logger.info(`Health check completed`, { status: result.status, responseTime: Date.now() - startTime }); const statusCode = result.status === 'unhealthy' ? 503 : 200; if (htmlResponse || req.query.html === 'true') { res.setHeader('Content-Type', 'text/html'); res.status(statusCode).send(renderHTML(result, message, style, autoRefreshInterval)); } else { res.status(statusCode).json(result); } } catch (error) { logger.error(`Health check failed`, { error: error.message, stack: error.stack }); if (typeof onError === 'function') onError(error, 'system'); res.status(500).json({ status: 'error', message: 'Health check failed', error: error.message, timestamp: new Date(), }); } }); if (enableMetrics) { app.get(`${route}/metrics`, preCheckMiddleware, (req, res) => { logger.info(`Metrics request received`, { ip: req.ip, timestamp: new Date() }); res.json(metrics); }); app.get(`${route}/metrics/prometheus`, preCheckMiddleware, (req, res) => { logger.info(`Prometheus metrics request received`, { ip: req.ip, timestamp: new Date() }); const promMetrics = [ `# HELP health_check_requests_total Total health check requests`, `# TYPE health_check_requests_total counter`, `health_check_requests_total ${metrics.totalRequests}`, `# HELP health_check_healthy_total Total healthy responses`, `# TYPE health_check_healthy_total counter`, `health_check_healthy_total ${metrics.healthyResponses}`, `# HELP health_check_unhealthy_total Total unhealthy responses`, `# TYPE health_check_unhealthy_total counter`, `health_check_unhealthy_total ${metrics.unhealthyResponses}`, `# HELP health_check_response_time_ms Average response time in ms`, `# TYPE health_check_response_time_ms gauge`, `health_check_response_time_ms ${metrics.averageResponseTime.toFixed(2)}`, ].join('\n'); res.setHeader('Content-Type', 'text/plain; version=0.0.4'); res.send(promMetrics); }); } process.on('SIGTERM', () => clearInterval(cleanupInterval)); } module.exports = healthCheck;