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 a9d6c3a
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 0 deletions.
96 changes: 96 additions & 0 deletions src/controllers/healthcheck/healthcheck.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { buildServiceHealthCheck } from '../../helpers/healthcheck.helper';
import { initializedWallets } from '../../services/wallets.service';
import healthService from '../../services/healthcheck.service';

async function getGlobalHealth(req, res) {
const promises = [];

Check warning on line 6 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L6

Added line #L6 was not covered by tests

for (const [walletId, wallet] of initializedWallets) {
promises.push(healthService.getWalletHealth(wallet, walletId));

Check warning on line 9 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L8-L9

Added lines #L8 - L9 were not covered by tests
}

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

Check warning on line 13 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L12-L13

Added lines #L12 - L13 were not covered by tests

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

Check warning on line 16 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L16

Added line #L16 was not covered by tests

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

Check warning on line 19 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L18-L19

Added lines #L18 - L19 were not covered by tests

for (const healthData of resolvedPromises) {

Check warning on line 21 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L21

Added line #L21 was not covered by tests
if (healthData.status === 'fail') {
httpStatus = 503;
status = 'fail';
break;

Check warning on line 25 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L23-L25

Added lines #L23 - L25 were not covered by tests
}

if (healthData.status === 'warn') {
httpStatus = 503;
status = 'warn';

Check warning on line 30 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L29-L30

Added lines #L29 - L30 were not covered by tests
}
}

const checks = {};

Check warning on line 34 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L34

Added line #L34 was not covered by tests

for (const healthData of resolvedPromises) {

Check warning on line 36 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L36

Added line #L36 was not covered by tests
// 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];

Check warning on line 39 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L39

Added line #L39 was not covered by tests
}

const serviceHealth = buildServiceHealthCheck(

Check warning on line 42 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L42

Added line #L42 was not covered by tests
status,
'Wallet-headless health',
checks,
);

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

Check warning on line 48 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L48

Added line #L48 was not covered by tests
}

async function getWalletHealth(req, res) {
const sendError = message => {
res.status(400).send({

Check warning on line 53 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L52-L53

Added lines #L52 - L53 were not covered by tests
success: false,
message,
});
};

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

Check warning on line 61 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L60-L61

Added lines #L60 - L61 were not covered by tests
}

const walletId = req.headers['x-wallet-id'];

Check warning on line 64 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L64

Added line #L64 was not covered by tests
if (!initializedWallets.has(walletId)) {
sendError('Invalid wallet id parameter.');
return;

Check warning on line 67 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L66-L67

Added lines #L66 - L67 were not covered by tests
}
const wallet = initializedWallets.get(walletId);
const walletHealthData = await healthService.getWalletHealth(wallet, walletId);

Check warning on line 70 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L69-L70

Added lines #L69 - L70 were not covered by tests

const status = walletHealthData.status === 'pass' ? 200 : 503;

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

Check warning on line 74 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L74

Added line #L74 was not covered by tests
}

async function getFullnodeHealth(req, res) {
const fullnodeHealthData = await healthService.getFullnodeHealth();

Check warning on line 78 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L78

Added line #L78 was not covered by tests
const status = fullnodeHealthData.status === 'pass' ? 200 : 503;

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

Check warning on line 81 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L81

Added line #L81 was not covered by tests
}

async function getTxMiningServiceHealth(req, res) {
const txMiningServiceHealthData = await healthService.getTxMiningServiceHealth();

Check warning on line 85 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L85

Added line #L85 was not covered by tests
const status = txMiningServiceHealthData.status === 'pass' ? 200 : 503;

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

Check warning on line 88 in src/controllers/healthcheck/healthcheck.controller.js

View check run for this annotation

Codecov / codecov/patch

src/controllers/healthcheck/healthcheck.controller.js#L88

Added line #L88 was not covered by tests
}

export {
getGlobalHealth,
getWalletHealth,
getFullnodeHealth,
getTxMiningServiceHealth
};
1 change: 1 addition & 0 deletions src/helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module.exports = {
[HathorWallet.SYNCING]: 'Syncing',
[HathorWallet.READY]: 'Ready',
[HathorWallet.ERROR]: 'Error',
[HathorWallet.PROCESSING]: 'Processing',
},

// Error message when the user tries to send a transaction while the lock is active
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');

Check warning on line 16 in src/helpers/healthcheck.helper.js

View check run for this annotation

Codecov / codecov/patch

src/helpers/healthcheck.helper.js#L16

Added line #L16 was not covered by tests
}

// 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(', ')}`);

Check warning on line 21 in src/helpers/healthcheck.helper.js

View check run for this annotation

Codecov / codecov/patch

src/helpers/healthcheck.helper.js#L21

Added line #L21 was not covered by tests
}

// 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(', ')}`);

Check warning on line 26 in src/helpers/healthcheck.helper.js

View check run for this annotation

Codecov / codecov/patch

src/helpers/healthcheck.helper.js#L26

Added line #L26 was not covered by tests
}

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

Check warning on line 31 in src/helpers/healthcheck.helper.js

View check run for this annotation

Codecov / codecov/patch

src/helpers/healthcheck.helper.js#L31

Added line #L31 was not covered by tests
}

// Build the health check object
return {

Check warning on line 35 in src/helpers/healthcheck.helper.js

View check run for this annotation

Codecov / codecov/patch

src/helpers/healthcheck.helper.js#L35

Added line #L35 was not covered by tests
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');

Check warning on line 54 in src/helpers/healthcheck.helper.js

View check run for this annotation

Codecov / codecov/patch

src/helpers/healthcheck.helper.js#L54

Added line #L54 was not covered by tests
}

return {

Check warning on line 57 in src/helpers/healthcheck.helper.js

View check run for this annotation

Codecov / codecov/patch

src/helpers/healthcheck.helper.js#L57

Added line #L57 was not covered by tests
status,
description,
checks,
};
}
31 changes: 31 additions & 0 deletions src/routes/healthcheck/healthcheck.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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;
95 changes: 95 additions & 0 deletions src/services/healthcheck.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { config as hathorLibConfig, healthApi, txMiningApi } from '@hathor/wallet-lib';

const { buildComponentHealthCheck } = require('../helpers/healthcheck.helper');
const { friendlyWalletState } = require('../helpers/constants');

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

if (!wallet.isReady()) {
healthData = buildComponentHealthCheck(

Check warning on line 11 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L11

Added line #L11 was not covered by tests
`Wallet ${walletId}`,
'fail',
'internal',
`Wallet is not ready. Current state: ${friendlyWalletState[wallet.state]}`
);
} else {
healthData = buildComponentHealthCheck(

Check warning on line 18 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L18

Added line #L18 was not covered by tests
`Wallet ${walletId}`,
'pass',
'internal',
'Wallet is ready'
);
}

return healthData;

Check warning on line 26 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L26

Added line #L26 was not covered by tests
},

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 {
await healthApi.getHealth();

Check warning on line 36 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L35-L36

Added lines #L35 - L36 were not covered by tests

output = 'Fullnode is responding';
healthStatus = 'pass';

Check warning on line 39 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L38-L39

Added lines #L38 - L39 were not covered by tests
} catch (e) {
if (e.response && e.response.data) {
output = `Fullnode reported as unhealthy: ${JSON.stringify(e.response.data)}`;
healthStatus = e.response.data.status;

Check warning on line 43 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L42-L43

Added lines #L42 - L43 were not covered by tests
} else {
output = `Error getting fullnode health: ${e.message}`;
healthStatus = 'fail';

Check warning on line 46 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L45-L46

Added lines #L45 - L46 were not covered by tests
}
}

const fullnodeHealthData = buildComponentHealthCheck(

Check warning on line 50 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L50

Added line #L50 was not covered by tests
`Fullnode ${hathorLibConfig.getServerUrl()}`,
healthStatus,
'fullnode',
output
);

return fullnodeHealthData;

Check warning on line 57 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L57

Added line #L57 was not covered by tests
},

async getTxMiningServiceHealth() {
let output;
let healthStatus;

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

Check warning on line 65 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L64-L65

Added lines #L64 - L65 were not covered by tests

healthStatus = healthData.status;

Check warning on line 67 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L67

Added line #L67 was not covered by tests

if (healthStatus === 'fail') {
output = `Tx Mining Service reported as unhealthy: ${JSON.stringify(healthData)}`;

Check warning on line 70 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L70

Added line #L70 was not covered by tests
} else {
output = 'Tx Mining Service is healthy';

Check warning on line 72 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L72

Added line #L72 was not covered by tests
}
} 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;

Check warning on line 77 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L76-L77

Added lines #L76 - L77 were not covered by tests
} else {
output = `Error getting tx-mining-service health: ${e.message}`;
healthStatus = 'fail';

Check warning on line 80 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L79-L80

Added lines #L79 - L80 were not covered by tests
}
}

const txMiningServiceHealthData = buildComponentHealthCheck(

Check warning on line 84 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L84

Added line #L84 was not covered by tests
`TxMiningService ${hathorLibConfig.getTxMiningUrl()}`,
healthStatus,
'service',
output
);

return txMiningServiceHealthData;

Check warning on line 91 in src/services/healthcheck.service.js

View check run for this annotation

Codecov / codecov/patch

src/services/healthcheck.service.js#L91

Added line #L91 was not covered by tests
}
};

export default healthService;

0 comments on commit a9d6c3a

Please sign in to comment.