-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
288 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = []; | ||
|
||
for (const [walletId, wallet] of initializedWallets) { | ||
promises.push(healthService.getWalletHealth(wallet, walletId)); | ||
} | ||
|
||
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, | ||
); | ||
|
||
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, walletId); | ||
|
||
const status = walletHealthData.status === 'pass' ? 200 : 503; | ||
|
||
res.status(status).send(walletHealthData); | ||
} | ||
|
||
async function getFullnodeHealth(req, res) { | ||
const fullnodeHealthData = await healthService.getFullnodeHealth(); | ||
const status = fullnodeHealthData.status === 'pass' ? 200 : 503; | ||
|
||
res.status(status).send(fullnodeHealthData); | ||
} | ||
|
||
async function getTxMiningServiceHealth(req, res) { | ||
const txMiningServiceHealthData = await healthService.getTxMiningServiceHealth(); | ||
const status = txMiningServiceHealthData.status === 'pass' ? 200 : 503; | ||
|
||
res.status(status).send(txMiningServiceHealthData); | ||
} | ||
|
||
export { | ||
getGlobalHealth, | ||
getWalletHealth, | ||
getFullnodeHealth, | ||
getTxMiningServiceHealth | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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( | ||
`Wallet ${walletId}`, | ||
'fail', | ||
'internal', | ||
`Wallet is not ready. Current state: ${friendlyWalletState[wallet.state]}` | ||
); | ||
} 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 { | ||
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; |