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