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
JavaScript
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;