UNPKG

express-service-readiness-middleware

Version:

This module provides express middleware for determining whether routes are exposed based on service critical dependency health.

197 lines 8.44 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.stopCheckingReadiness = exports.criticalDependenciesReady = exports.setLogger = exports.checkDependenciesHealth = exports.createReadinessMiddleware = void 0; const DefaultConfig = { retryIntervalInMilliseconds: 2000, maximumWaitTimeForServiceReadinessInMilliseconds: 30000, whitelistedPaths: [], logOutDependenciesDataOnFailure: false }; const dependencyStateItems = []; let informationLogger; let ready = false; let maximumWaitTimeTimeout; let serviceConfiguration = undefined; /** * Creates the service readiness middleware * @param dependencies - Array of {IDependency} objects * @param config - Optional configuration for the middleware. If not defined the DefaultConfig will be used */ const createReadinessMiddleware = (dependencies, config) => { const configuration = config ?? DefaultConfig; Object.keys(DefaultConfig).forEach(key => { if (configuration[key] === null || configuration[key] === undefined) { configuration[key] = DefaultConfig[key]; } }); serviceConfiguration = configuration; checkServiceReadiness(dependencies, configuration); let pathsToWhitelist = []; if (configuration.whitelistedPaths && configuration.whitelistedPaths.length > 0) pathsToWhitelist = configuration.whitelistedPaths.map(x => x.toLowerCase()); return (req, res, next) => { if (!ready) { const path = req.originalUrl.replace(/\?.*$/, '').toLowerCase(); if (pathsToWhitelist.length > 0 && pathsToWhitelist.includes(path)) return next(); informationLogger?.log('Service is not yet ready to handle requests'); res.sendStatus(502); return; } return next(); }; }; exports.createReadinessMiddleware = createReadinessMiddleware; /** * Checks the health of all dependencies * @param dependencies - Array of {IDependency} objects */ const checkDependenciesHealth = async (dependencies) => { const dependenciesHealth = []; const promises = []; let allDependenciesHealthy = true; let allCriticalDependenciesHealthy = true; for (const dependency of dependencies) { promises.push(checkDependencyHealth(dependency)); } const promiseResults = await Promise.allSettled(promises); for (let i = 0; i < dependencies.length; i++) { const dependency = dependencies[i]; const promiseResult = promiseResults[i]; const healthy = promiseResult.status === 'fulfilled' && promiseResult.value; dependenciesHealth.push({ name: dependency.name, data: dependency.data, healthy, critical: dependency.critical }); if (!healthy) { allDependenciesHealthy = false; if (dependency.critical) allCriticalDependenciesHealthy = false; } } return { allDependenciesHealthy, allCriticalDependenciesHealthy, dependencies: dependenciesHealth }; }; exports.checkDependenciesHealth = checkDependenciesHealth; /** * Set a logger * @param logger - {ILogger} */ const setLogger = (logger) => { informationLogger = logger; }; exports.setLogger = setLogger; /** * Returns a boolean indicating whether all critical dependencies are ready */ const criticalDependenciesReady = () => ready; exports.criticalDependenciesReady = criticalDependenciesReady; /** * Removes any NodeJS.Timeout instances created by the middleware */ const stopCheckingReadiness = () => { if (maximumWaitTimeTimeout) { clearTimeout(maximumWaitTimeTimeout); } for (const dependencyStateItem of dependencyStateItems) { if (dependencyStateItem.timeoutId) clearTimeout(dependencyStateItem.timeoutId); } }; exports.stopCheckingReadiness = stopCheckingReadiness; const checkDependencyHealth = async (dependency) => { let healthy = false; try { const healthyFunc = dependency.isHealthy ? dependency.isHealthy : dependency.isReady; healthy = await healthyFunc(); informationLogger?.log(`dependency '${dependency.name}' is ${healthy ? 'healthy' : 'not healthy'}${!healthy && serviceConfiguration?.logOutDependenciesDataOnFailure ? ', data: ' + JSON.stringify(dependency.data) : ''}`); } catch (err) { // @ts-ignore informationLogger?.log(`An error occurred while checking health for dependency '${dependency.name}'${serviceConfiguration?.logOutDependenciesDataOnFailure ? ', data: ' + JSON.stringify(dependency.data) : ''}, error: ${err.message || err}`); } return healthy; }; const maximumWaitTimeExceeded = (config) => { return () => { (0, exports.stopCheckingReadiness)(); const items = []; dependencyStateItems.forEach(item => { items.push({ name: item.name, data: item.data, ready: item.ready }); }); if (informationLogger) { const suffix = config.logOutDependenciesDataOnFailure === true ? ` Critical dependencies: ${JSON.stringify(items)}` : ''; informationLogger?.log(`All critical dependencies did not become healthy.${suffix}`); } // @ts-ignore if (global.ExpressServiceReadinessTests === undefined) process.exit(1); }; }; const checkServiceReadiness = (dependencies, config) => { const criticalDependencies = getCriticalDependencies(dependencies); if (criticalDependencies.length === 0) { ready = true; return; } const maximumWaitTimeForServiceReadinessInMilliseconds = config.maximumWaitTimeForServiceReadinessInMilliseconds ?? DefaultConfig.maximumWaitTimeForServiceReadinessInMilliseconds; maximumWaitTimeTimeout = setTimeout(maximumWaitTimeExceeded(config), maximumWaitTimeForServiceReadinessInMilliseconds); criticalDependencies.forEach(criticalDependencies => { const { name, data, isReady } = criticalDependencies; let retryIntervalInMilliseconds = getRetryIntervalInMilliseconds(config, criticalDependencies); const dependencyStateItem = { name, data, ready: false, isReady, retryIntervalInMilliseconds }; dependencyStateItems.push(dependencyStateItem); // noinspection JSIgnoredPromiseFromCall checkDependencyReadiness(dependencyStateItem); }); }; const checkCriticalReadiness = () => { for (const dependencyStateItem of dependencyStateItems) { if (!dependencyStateItem.ready) return; } clearTimeout(maximumWaitTimeTimeout); informationLogger?.log('All critical dependencies are now ready'); ready = true; }; const checkDependencyReadiness = async (dependencyStateItem) => { try { const ready = await dependencyStateItem.isReady(); if (ready) { dependencyStateItem.ready = true; informationLogger?.log(`critical dependency '${dependencyStateItem.name}' is ready`); checkCriticalReadiness(); return; } informationLogger?.log(`critical dependency '${dependencyStateItem.name}' is not ready yet${serviceConfiguration.logOutDependenciesDataOnFailure ? ', data: ' + JSON.stringify(dependencyStateItem.data) : ''}`); } catch (err) { // @ts-ignore informationLogger?.log(`An error occurred while checking health for critical dependency '${dependencyStateItem.name}'${serviceConfiguration.logOutDependenciesDataOnFailure ? ', data: ' + JSON.stringify(dependencyStateItem.data) : ''}, error: ${err.message || err}`); } const checkHealthAgain = () => checkDependencyReadiness(dependencyStateItem); dependencyStateItem.timeoutId = setTimeout(checkHealthAgain, dependencyStateItem.retryIntervalInMilliseconds); }; const getCriticalDependencies = (dependencies) => dependencies.filter(x => x.critical); const getRetryIntervalInMilliseconds = (config, criticalDependency) => { let retryIntervalInMilliseconds = config.retryIntervalInMilliseconds ?? DefaultConfig.retryIntervalInMilliseconds; if (criticalDependency.retryIntervalInMilliseconds) retryIntervalInMilliseconds = criticalDependency.retryIntervalInMilliseconds; return retryIntervalInMilliseconds; }; //# sourceMappingURL=index.js.map