Skip to content

Commit

Permalink
feat: new healthcheck endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
luislhl committed Oct 9, 2023
1 parent c39dfd3 commit a56fb31
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 1 deletion.
99 changes: 99 additions & 0 deletions src/controllers/healthcheck/healthcheck.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { buildServiceHealthCheck } from '../../helpers/healthcheck.helper';
import { initializedWallets } from '../../services/wallets.service';
import healthService from '../../services/healthcheck.service';

async function getGlobalHealth(req, res) {
console.log("received request");

const promises = [];

for (const wallet of initializedWallets.values()) {
promises.push(healthService.getWalletHealth(wallet));
}

promises.push(healthService.getFullnodeHealth());
promises.push(healthService.getTxMiningServiceHealth());

// Use Promise.all to run all checks in parallel and replace the promises with the results
const resolvedPromises = await Promise.all(promises);

let httpStatus = 200;
let status = 'pass';

for (const healthData of resolvedPromises) {
if (healthData['status'] === 'fail') {
httpStatus = 503;
status = 'fail';
break;
}

if (healthData['status'] === 'warn') {
httpStatus = 503;
status = 'warn';
}
}

const checks = {};

for (const healthData of resolvedPromises) {
// We use an array as the value to stick to our current format,
// which allows us to add more checks to a component if needed
checks[healthData['componentName']] = [healthData];
}

const serviceHealth = buildServiceHealthCheck(
status,
"Wallet-headless health",
checks,
)
console.log('serviceHealth', serviceHealth);

return res.status(httpStatus).send(serviceHealth);
}

async function getWalletHealth(req, res) {
const sendError = (message) => {
res.status(400).send({
success: false,
message,
});
};

if (!('x-wallet-id' in req.headers)) {
sendError('Header \'X-Wallet-Id\' is required.');
return;
}

const walletId = req.headers['x-wallet-id'];
if (!initializedWallets.has(walletId)) {
sendError('Invalid wallet id parameter.');
return;
}
const wallet = initializedWallets.get(walletId);
const walletHealthData = await healthService.getWalletHealth(wallet);

const status = walletHealthData['status'] === 'pass' ? 200 : 503;

return res.status(status).send(walletHealthData);
}

async function getFullnodeHealth(req, res) {
const fullnodeHealthData = await healthService.getFullnodeHealth();
const status = fullnodeHealthData['status'] === 'pass' ? 200 : 503;

return res.status(status).send(fullnodeHealthData);
}

async function getTxMiningServiceHealth(req, res) {
const txMiningServiceHealthData = await healthService.getTxMiningServiceHealth();
const status = txMiningServiceHealthData['status'] === 'pass' ? 200 : 503;

return res.status(status).send(txMiningServiceHealthData);
}

export {
getGlobalHealth,
getWalletHealth,
getFullnodeHealth,
getTxMiningServiceHealth
}
2 changes: 1 addition & 1 deletion src/controllers/wallet/wallet.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/

const { constants: { HATHOR_TOKEN_CONFIG, TOKEN_INDEX_MASK } } = require('@hathor/wallet-lib');
const { constants: { HATHOR_TOKEN_CONFIG, TOKEN_INDEX_MASK }, HathorWallet } = require('@hathor/wallet-lib');
const { txApi, walletApi, WalletType, constants: hathorLibConstants, helpersUtils, errors, tokensUtils, transactionUtils, PartialTx } = require('@hathor/wallet-lib');
const { matchedData } = require('express-validator');
// import is used because there is an issue with winston logger when using require ref: #262
Expand Down
62 changes: 62 additions & 0 deletions src/helpers/healthcheck.helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const ALLOWED_COMPONENT_TYPES = ['datastore', 'fullnode', 'internal', 'service'];
const ALLOWED_STATUSES = ['fail', 'pass', 'warn'];

/**
* Builds a health check object for a component
*
* @param {string} componentName
* @param {string} status
* @param {string} componentType
* @param {string} output
* @returns {Object}
*/
export function buildComponentHealthCheck(componentName, status, componentType, output) {
// Assert the component name is a string
if (typeof componentName !== 'string') {
throw new Error('Component name must be a string');
}

// Assert the component type is one of the allowed values
if (!ALLOWED_COMPONENT_TYPES.includes(componentType)) {
throw new Error("Component status must be one of: " + ALLOWED_COMPONENT_TYPES.join(', '));
}

// Assert the status is one of the allowed values
if (!ALLOWED_STATUSES.includes(status)) {
throw new Error("Component status must be one of: " + ALLOWED_STATUSES.join(', '));
}

// Assert the output is a string
if (typeof output !== 'string') {
throw new Error('Component output must be a string');
}

// Build the health check object
return {
componentName,
status,
componentType,
output,
time: new Date().toISOString(),
};
}

/**
* Builds a health check object for a service.
*
* @param {string} description
* @param {Object} checks
* @returns {Object}
*/
export function buildServiceHealthCheck(status, description, checks) {
// Assert the description is a string
if (typeof description !== 'string') {
throw new Error('Service description must be a string');
}

return {
status,
description,
checks,
};
}
33 changes: 33 additions & 0 deletions src/routes/healthcheck/healthcheck.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const { Router } = require('express');
const { patchExpressRouter } = require('../../patch');
const {
getWalletHealth, getFullnodeHealth, getTxMiningServiceHealth, getGlobalHealth
} = require('../../controllers/healthcheck/healthcheck.controller');

const healthcheckRouter = patchExpressRouter(Router({ mergeParams: true }));

/**
* GET request to get the health of the wallet-headless
* For the docs, see api-docs.js
*/
healthcheckRouter.get('/', getGlobalHealth);

/**
* GET request to get the health of a wallet
* For the docs, see api-docs.js
*/
healthcheckRouter.get('/wallet', getWalletHealth);

/**
* GET request to get the health of the fullnode
* For the docs, see api-docs.js
*/
healthcheckRouter.get('/fullnode', getFullnodeHealth);

/**
* GET request to get the health of the tx-mining-service
* For the docs, see api-docs.js
*/
healthcheckRouter.get('/tx-mining', getTxMiningServiceHealth);

module.exports = healthcheckRouter;
3 changes: 3 additions & 0 deletions src/routes/index.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const { patchExpressRouter } = require('../patch');

const mainRouter = patchExpressRouter(Router({ mergeParams: true }));
const walletRouter = require('./wallet/wallet.routes');
const healthcheckRouter = require('./healthcheck/healthcheck.routes');

mainRouter.get('/', rootControllers.welcome);
mainRouter.get('/docs', rootControllers.docs);
Expand All @@ -38,4 +39,6 @@ mainRouter.post('/reload-config', rootControllers.reloadConfig);

mainRouter.use('/wallet', walletRouter);

mainRouter.use('/health', healthcheckRouter);

module.exports = mainRouter;
94 changes: 94 additions & 0 deletions src/services/healthcheck.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { config as hathorLibConfig, healthApi, txMiningApi } from '@hathor/wallet-lib';
const { buildComponentHealthCheck } = require('../helpers/healthcheck.helper');

const healthService = {
async getWalletHealth(wallet) {
let healthData;

// TODO: Can we get a better reason for not being ready?
if (!wallet.isReady()) {
healthData = buildComponentHealthCheck(
'Wallet ' + walletId,
'fail',
'internal',
'Wallet is not ready'
)
} else {
healthData = buildComponentHealthCheck(
'Wallet ' + walletId,
'pass',
'internal',
'Wallet is ready'
)
}

return healthData;
},

async getFullnodeHealth() {
let output;
let healthStatus;

// TODO: We will need to parse the healthData to get the status,
// but hathor-core hasn't this implemented yet
try {
const healthData = await healthApi.getHealth();

output = 'Fullnode is responding'
healthStatus = 'pass'
} catch (e) {
if (e.response && e.response.data) {
output = 'Fullnode reported as unhealthy: ' + JSON.stringify(e.response.data)
healthStatus = e.response.data['status']
} else {
output = 'Error getting fullnode health: ' + e.message
healthStatus = 'fail'
}
}

const fullnodeHealthData = buildComponentHealthCheck(
'Fullnode ' + hathorLibConfig.getServerUrl(),
healthStatus,
'fullnode',
output
)

return fullnodeHealthData;
},

async getTxMiningServiceHealth() {
let output;
let healthStatus;

try {
const healthData = await txMiningApi.getHealth();

healthStatus = healthData['status'];

if (healthStatus === 'fail') {
output = 'Tx Mining Service reported as unhealthy: ' + JSON.stringify(healthData)
} else {
output = 'Tx Mining Service is healthy'
}
} catch (e) {
if (e.response && e.response.data) {
output = 'Tx Mining Service reported as unhealthy: ' + JSON.stringify(e.response.data)
healthStatus = e.response.data['status']
} else {
output = 'Error getting tx-mining-service health: ' + e.message
healthStatus = 'fail'
}
}

const txMiningServiceHealthData = buildComponentHealthCheck(
'TxMiningService ' + hathorLibConfig.getTxMiningUrl(),
healthStatus,
'service',
output
)

return txMiningServiceHealthData
}
};

export default healthService;

0 comments on commit a56fb31

Please sign in to comment.