From 07c6e58d2ec58dae4b91b5876d1dfc363ed4d626 Mon Sep 17 00:00:00 2001 From: Luis Helder Date: Tue, 5 Dec 2023 15:50:26 -0300 Subject: [PATCH] feat: new healthcheck endpoint (#348) --- __tests__/__fixtures__/http-fixtures.js | 3 + __tests__/__fixtures__/settings-fixture.js | 1 + __tests__/healthcheck.test.js | 310 ++++++++++++++++++ __tests__/test-utils.js | 7 + src/api-docs.js | 137 ++++++++ .../healthcheck/healthcheck.controller.js | 147 +++++++++ src/helpers/constants.js | 1 + src/helpers/healthcheck.helper.js | 68 ++++ src/routes/healthcheck/healthcheck.routes.js | 20 ++ src/routes/index.routes.js | 3 + src/services/healthcheck.service.js | 112 +++++++ 11 files changed, 809 insertions(+) create mode 100644 __tests__/healthcheck.test.js create mode 100644 src/controllers/healthcheck/healthcheck.controller.js create mode 100644 src/helpers/healthcheck.helper.js create mode 100644 src/routes/healthcheck/healthcheck.routes.js create mode 100644 src/services/healthcheck.service.js diff --git a/__tests__/__fixtures__/http-fixtures.js b/__tests__/__fixtures__/http-fixtures.js index 126a8841..83f94679 100644 --- a/__tests__/__fixtures__/http-fixtures.js +++ b/__tests__/__fixtures__/http-fixtures.js @@ -1,4 +1,7 @@ export default { + 'http://fake.txmining:8084/health': { + status: 'pass' + }, '/v1a/version': { version: '0.38.4', network: 'testnet-foxtrot', diff --git a/__tests__/__fixtures__/settings-fixture.js b/__tests__/__fixtures__/settings-fixture.js index d0096b8d..adc97da0 100644 --- a/__tests__/__fixtures__/settings-fixture.js +++ b/__tests__/__fixtures__/settings-fixture.js @@ -5,6 +5,7 @@ const defaultConfig = { http_port: 8001, network: 'testnet', server: 'http://fakehost:8083/v1a/', + txMiningUrl: 'http://fake.txmining:8084/', seeds: { stub_seed: 'upon tennis increase embark dismiss diamond monitor face magnet jungle scout salute rural master shoulder cry juice jeans radar present close meat antenna mind', diff --git a/__tests__/healthcheck.test.js b/__tests__/healthcheck.test.js new file mode 100644 index 00000000..bf49aec3 --- /dev/null +++ b/__tests__/healthcheck.test.js @@ -0,0 +1,310 @@ +import { HathorWallet } from '@hathor/wallet-lib'; +import TestUtils from './test-utils'; + +const { initializedWallets } = require('../src/services/wallets.service'); + +const walletId = 'health_wallet'; +const anotherWalletId = 'another_health_wallet'; + +describe('healthcheck api', () => { + beforeAll(async () => { + await TestUtils.startWallet({ walletId, preCalculatedAddresses: TestUtils.addresses }); + await TestUtils.startWallet({ + walletId: anotherWalletId, + preCalculatedAddresses: TestUtils.addresses + }); + }); + + afterAll(async () => { + await TestUtils.stopWallet({ walletId }); + await TestUtils.stopWallet({ walletId: anotherWalletId }); + }); + + afterEach(async () => { + await TestUtils.stopMocks(); + TestUtils.startMocks(); + TestUtils.resetRequest(); + }); + + describe('/health', () => { + it('should return 400 when the x-wallet-id is invalid', async () => { + const response = await TestUtils.request + .query({ wallet_ids: 'invalid' }) + .get('/health'); + + expect(response.status).toBe(400); + expect(response.body).toStrictEqual({ + success: false, + message: 'Invalid wallet id parameter: invalid', + }); + }); + + it('should return 400 when no component is included', async () => { + const response = await TestUtils.request.get('/health'); + + expect(response.status).toBe(400); + expect(response.body).toStrictEqual({ + success: false, + message: 'At least one component must be included in the health check', + }); + }); + + it('should return 200 when all components are healthy', async () => { + const response = await TestUtils.request + .query({ + include_tx_mining: true, + include_fullnode: true, + wallet_ids: `${walletId},${anotherWalletId}` + }) + .get('/health'); + expect(response.status).toBe(200); + + expect(response.body).toStrictEqual({ + status: 'pass', + description: 'Wallet-headless health', + checks: { + 'Wallet health_wallet': [ + { + componentName: 'Wallet health_wallet', + componentType: 'internal', + status: 'pass', + output: 'Wallet is ready', + time: expect.any(String), + }, + ], + 'Wallet another_health_wallet': [ + { + componentName: 'Wallet another_health_wallet', + componentType: 'internal', + status: 'pass', + output: 'Wallet is ready', + time: expect.any(String), + }, + ], + 'Fullnode http://fakehost:8083/v1a/': [ + { + componentName: 'Fullnode http://fakehost:8083/v1a/', + componentType: 'fullnode', + status: 'pass', + output: 'Fullnode is responding', + time: expect.any(String), + }, + ], + 'TxMiningService http://fake.txmining:8084/': [ + { + componentName: 'TxMiningService http://fake.txmining:8084/', + componentType: 'service', + status: 'pass', + output: 'Tx Mining Service is healthy', + time: expect.any(String), + }, + ], + } + }); + }); + + it('should return 503 when the wallet is not ready', async () => { + const wallet = initializedWallets.get(walletId); + const originalIsReady = wallet.isReady; + const originalState = wallet.state; + + wallet.isReady = () => false; + wallet.state = HathorWallet.SYNCING; + + const response = await TestUtils.request + .query({ include_tx_mining: true, include_fullnode: true, wallet_ids: walletId }) + .get('/health'); + expect(response.status).toBe(503); + + expect(response.body).toStrictEqual({ + status: 'fail', + description: 'Wallet-headless health', + checks: { + 'Wallet health_wallet': [ + { + componentName: 'Wallet health_wallet', + componentType: 'internal', + status: 'fail', + output: 'Wallet is not ready. Current state: Syncing', + time: expect.any(String), + }, + ], + 'Fullnode http://fakehost:8083/v1a/': [ + { + componentName: 'Fullnode http://fakehost:8083/v1a/', + componentType: 'fullnode', + status: 'pass', + output: 'Fullnode is responding', + time: expect.any(String), + }, + ], + 'TxMiningService http://fake.txmining:8084/': [ + { + componentName: 'TxMiningService http://fake.txmining:8084/', + componentType: 'service', + status: 'pass', + output: 'Tx Mining Service is healthy', + time: expect.any(String), + }, + ], + } + }); + + wallet.isReady = originalIsReady; + wallet.state = originalState; + }); + + it('should return 503 when the fullnode is not healthy', async () => { + TestUtils.httpMock.onGet('/version').reply(503, { status: 'fail' }); + + const response = await TestUtils.request + .query({ include_tx_mining: true, include_fullnode: true, wallet_ids: walletId }) + .get('/health'); + expect(response.status).toBe(503); + + expect(response.body).toStrictEqual({ + status: 'fail', + description: 'Wallet-headless health', + checks: { + 'Wallet health_wallet': [ + { + componentName: 'Wallet health_wallet', + componentType: 'internal', + status: 'pass', + output: 'Wallet is ready', + time: expect.any(String), + }, + ], + 'Fullnode http://fakehost:8083/v1a/': [ + { + componentName: 'Fullnode http://fakehost:8083/v1a/', + componentType: 'fullnode', + status: 'fail', + output: 'Fullnode reported as unhealthy: {"status":"fail"}', + time: expect.any(String), + }, + ], + 'TxMiningService http://fake.txmining:8084/': [ + { + componentName: 'TxMiningService http://fake.txmining:8084/', + componentType: 'service', + status: 'pass', + output: 'Tx Mining Service is healthy', + time: expect.any(String), + }, + ], + } + }); + }); + + it('should return 503 when the tx mining service is not healthy', async () => { + TestUtils.httpMock.onGet('http://fake.txmining:8084/health').reply( + 503, + { status: 'fail' } + ); + + const response = await TestUtils.request + .query({ include_tx_mining: true, include_fullnode: true, wallet_ids: walletId }) + .get('/health'); + expect(response.status).toBe(503); + + expect(response.body).toStrictEqual({ + status: 'fail', + description: 'Wallet-headless health', + checks: { + 'Wallet health_wallet': [ + { + componentName: 'Wallet health_wallet', + componentType: 'internal', + status: 'pass', + output: 'Wallet is ready', + time: expect.any(String), + }, + ], + 'Fullnode http://fakehost:8083/v1a/': [ + { + componentName: 'Fullnode http://fakehost:8083/v1a/', + componentType: 'fullnode', + status: 'pass', + output: 'Fullnode is responding', + time: expect.any(String), + }, + ], + 'TxMiningService http://fake.txmining:8084/': [ + { + componentName: 'TxMiningService http://fake.txmining:8084/', + componentType: 'service', + status: 'fail', + output: 'Tx Mining Service reported as unhealthy: {"status":"fail"}', + time: expect.any(String), + }, + ], + } + }); + }); + + it('should not include the fullnode when the parameter is missing', async () => { + const response = await TestUtils.request + .query({ include_tx_mining: true, wallet_ids: walletId }) + .get('/health'); + expect(response.status).toBe(200); + + expect(response.body).toStrictEqual({ + status: 'pass', + description: 'Wallet-headless health', + checks: { + 'Wallet health_wallet': [ + { + componentName: 'Wallet health_wallet', + componentType: 'internal', + status: 'pass', + output: 'Wallet is ready', + time: expect.any(String), + }, + ], + 'TxMiningService http://fake.txmining:8084/': [ + { + componentName: 'TxMiningService http://fake.txmining:8084/', + componentType: 'service', + status: 'pass', + output: 'Tx Mining Service is healthy', + time: expect.any(String), + }, + ], + } + }); + }); + + it('should not include the tx mining service when the parameter is missing', async () => { + const response = await TestUtils.request + .query({ include_fullnode: true, wallet_ids: walletId }) + .get('/health'); + expect(response.status).toBe(200); + + expect(response.body).toStrictEqual({ + status: 'pass', + description: 'Wallet-headless health', + checks: { + 'Wallet health_wallet': [ + { + componentName: 'Wallet health_wallet', + componentType: 'internal', + status: 'pass', + output: 'Wallet is ready', + time: expect.any(String), + }, + ], + 'Fullnode http://fakehost:8083/v1a/': [ + { + componentName: 'Fullnode http://fakehost:8083/v1a/', + componentType: 'fullnode', + status: 'pass', + output: 'Fullnode is responding', + time: expect.any(String), + }, + ], + } + }); + }); + }); +}); diff --git a/__tests__/test-utils.js b/__tests__/test-utils.js index 67914c1f..91189554 100644 --- a/__tests__/test-utils.js +++ b/__tests__/test-utils.js @@ -208,6 +208,7 @@ class TestUtils { httpMock.onGet('/thin_wallet/token').reply(200, httpFixtures['/thin_wallet/token']); httpMock.onGet('/transaction').reply(200, httpFixtures['/transaction']); httpMock.onGet('/getmininginfo').reply(200, httpFixtures['/getmininginfo']); + httpMock.onGet('http://fake.txmining:8084/health').reply(200, httpFixtures['http://fake.txmining:8084/health']); // websocket mocks wsMock.on('connection', socket => { @@ -245,6 +246,12 @@ class TestUtils { }); } + static resetRequest() { + // This can be used to reset the supertest agent and avoid interferences between tests, + // since everything is a singleton in this file + request = supertest.agent(server); + } + static stopMocks() { httpMock.reset(); server.close(); diff --git a/src/api-docs.js b/src/api-docs.js index 1871a468..b4977627 100644 --- a/src/api-docs.js +++ b/src/api-docs.js @@ -3836,6 +3836,143 @@ const defaultApiDocs = { }, }, }, + '/health': { + get: { + summary: 'Return the health of the wallet headless.', + parameters: [ + { + name: 'wallet_ids', + in: 'query', + description: 'Wallet ids to check, comma-separated. If not provided, will not check any wallet.', + required: false, + schema: { + type: 'string', + }, + }, + { + name: 'include_fullnode', + in: 'query', + description: 'Whether fullnode health should be checked and included in the response.', + required: false, + schema: { + type: 'boolean', + }, + }, + { + name: 'include_tx_mining', + in: 'query', + description: 'Whether tx mining service health should be checked and included in the response.', + required: false, + schema: { + type: 'boolean', + }, + } + ], + responses: { + 200: { + description: 'A JSON with the health object. It will contain info about all components that were enabled and provided wallet ids.', + content: { + 'application/json': { + examples: { + success: { + summary: 'Success', + value: { + status: 'pass', + description: 'Wallet-headless health', + checks: { + 'Wallet ': [{ + status: 'pass', + componentType: 'internal', + componentName: 'Wallet ', + output: 'Wallet is ready', + }], + 'Wallet ': [{ + status: 'pass', + componentType: 'internal', + componentName: 'Wallet ', + output: 'Wallet is ready', + }], + fullnode: [{ + status: 'pass', + componentType: 'fullnode', + componentName: 'Fullnode ', + output: 'Fullnode is responding', + }], + txMining: [{ + status: 'pass', + componentType: 'service', + componentName: 'TxMiningService ', + output: 'Tx Mining Service is healthy', + }] + } + }, + }, + }, + }, + }, + }, + 400: { + description: 'A JSON object with the reason for the error.', + content: { + 'application/json': { + examples: { + 'invalid-wallet-ids': { + summary: 'Invalid wallet id', + value: { success: false, message: 'Invalid wallet id parameter.' } + }, + 'no-component-included': { + summary: 'No component was included in the request', + value: { success: false, message: 'At least one component must be included in the health check' } + }, + }, + }, + }, + }, + 503: { + description: 'A JSON with the health object. It will contain info about all components that were checked.', + content: { + 'application/json': { + examples: { + unhealthy: { + summary: 'Unhealthy wallet headless', + value: { + status: 'fail', + description: 'Wallet-headless health', + checks: { + 'Wallet ': [{ + status: 'pass', + componentType: 'internal', + componentName: 'Wallet ', + output: 'Wallet is ready', + }], + 'Wallet ': [{ + status: 'pass', + componentType: 'internal', + componentName: 'Wallet ', + output: 'Wallet is ready', + }], + fullnode: [{ + status: 'fail', + componentType: 'fullnode', + componentName: 'Fullnode ', + output: 'Fullnode reported as unhealthy: ', + }], + txMining: [{ + status: 'pass', + componentType: 'service', + componentName: 'TxMiningService ', + output: 'Tx Mining Service is healthy', + }] + } + }, + }, + }, + }, + }, + }, + }, + }, + }, }, }; diff --git a/src/controllers/healthcheck/healthcheck.controller.js b/src/controllers/healthcheck/healthcheck.controller.js new file mode 100644 index 00000000..a1cc5161 --- /dev/null +++ b/src/controllers/healthcheck/healthcheck.controller.js @@ -0,0 +1,147 @@ +import { buildServiceHealthCheck } from '../../helpers/healthcheck.helper'; +import { initializedWallets } from '../../services/wallets.service'; +import healthService from '../../services/healthcheck.service'; +import { parametersValidation } from '../../helpers/validations.helper'; + +/** + * + * @param {string[]} walletIds + * @param {boolean} includeFullnode + * @param {boolean} includeTxMiningService + * @returns {Promise<{checks: Object, httpStatus: number, status: string}>} + * @private + * @description Returns the health checks for the given wallet ids. The fullnode and the + * tx-mining-service are optionally included in the checks, depending on the parameters. + * + * Also returns the http status code and the overall status of the health check. + * + * Returned object format: + * { + * httpStatus: 200, + * status: 'pass', + * checks: { + * 'Wallet health_wallet': [ + * { + * componentName: 'Wallet health_wallet', + * componentType: 'internal', + * status: 'fail', + * output: 'Wallet is not ready. Current state: Syncing', + * time: expect.any(String), + * }, + * ], + * 'Fullnode http://fakehost:8083/v1a/': [ + * { + * componentName: 'Fullnode http://fakehost:8083/v1a/', + * componentType: 'fullnode', + * status: 'pass', + * output: 'Fullnode is responding', + * time: expect.any(String), + * }, + * ], + * 'TxMiningService http://fake.txmining:8084/': [ + * { + * componentName: 'TxMiningService http://fake.txmining:8084/', + * componentType: 'service', + * status: 'pass', + * output: 'Tx Mining Service is healthy', + * time: expect.any(String), + * }, + * ], + * } + * } + * + */ +async function getWalletsHealthChecks( + walletIds, + includeFullnode = false, + includeTxMiningService = false +) { + const promises = Array.from(walletIds).map( + walletId => healthService.getWalletHealth(initializedWallets.get(walletId), walletId) + ); + + if (includeFullnode) { + promises.push(healthService.getFullnodeHealth()); + } + if (includeTxMiningService) { + promises.push(healthService.getTxMiningServiceHealth()); + } + + const resolvedPromises = await Promise.all(promises); + + 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 httpStatus = resolvedPromises.every(healthData => healthData.status === 'pass') ? 200 : 503; + // If any of the checks failed, the status is fail. If all checks passed, the status is pass. + // Otherwise, the status is warn. + let status = httpStatus === 200 ? 'pass' : 'warn'; + status = resolvedPromises.some(healthData => healthData.status === 'fail') + ? 'fail' + : status; + + return { checks, httpStatus, status }; +} + +/** + * Controller for the /health endpoint that returns the health + * of the wallet-headless service, including all started wallets, + * the connected fullnode and the tx-mining-service + */ +async function getGlobalHealth(req, res) { + const validationResult = parametersValidation(req); + if (!validationResult.success) { + res.status(400).json(validationResult); + return; + } + + const sendError = message => { + res.status(400).send({ + success: false, + message, + }); + }; + + let walletIds = []; + + if ('wallet_ids' in req.query) { + walletIds = req.query.wallet_ids.split(','); + } + + for (const walletId of walletIds) { + if (!initializedWallets.has(walletId)) { + sendError(`Invalid wallet id parameter: ${walletId}`); + return; + } + } + + const includeFullnode = req.query.include_fullnode === 'true'; + const includeTxMiningService = req.query.include_tx_mining === 'true'; + + // Check whether at least one component is included + if (!includeFullnode && !includeTxMiningService && walletIds.length === 0) { + sendError('At least one component must be included in the health check'); + return; + } + + const response = await getWalletsHealthChecks(walletIds, includeFullnode, includeTxMiningService); + + const { checks, httpStatus, status } = response; + + const serviceHealth = buildServiceHealthCheck( + status, + 'Wallet-headless health', + checks, + ); + + res.status(httpStatus).send(serviceHealth); +} + +export { + getGlobalHealth, +}; diff --git a/src/helpers/constants.js b/src/helpers/constants.js index 20da20e1..a8f2ec4c 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.js @@ -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 diff --git a/src/helpers/healthcheck.helper.js b/src/helpers/healthcheck.helper.js new file mode 100644 index 00000000..84c65849 --- /dev/null +++ b/src/helpers/healthcheck.helper.js @@ -0,0 +1,68 @@ +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' || componentName.length === 0) { + throw new Error('Component name must be a non-empty 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' || output.length === 0) { + throw new Error('Component output must be a non-empty 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} status + * @param {string} description + * @param {Object} checks + * @returns {Object} + */ +export function buildServiceHealthCheck(status, description, checks) { + // Assert the description is a string + if (typeof description !== 'string' || description.length === 0) { + throw new Error('Service description must be a non-empty string'); + } + + // Assert the status is one of the allowed values + if (!ALLOWED_STATUSES.includes(status)) { + throw new Error(`Service status must be one of: ${ALLOWED_STATUSES.join(', ')}`); + } + + return { + status, + description, + checks, + }; +} diff --git a/src/routes/healthcheck/healthcheck.routes.js b/src/routes/healthcheck/healthcheck.routes.js new file mode 100644 index 00000000..dfa58e6d --- /dev/null +++ b/src/routes/healthcheck/healthcheck.routes.js @@ -0,0 +1,20 @@ +const { Router } = require('express'); +const { query } = require('express-validator'); +const { patchExpressRouter } = require('../../patch'); +const { 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( + '/', + query('wallet_ids').isString().optional(), + query('include_fullnode').isBoolean().optional(), + query('include_tx_mining').isBoolean().optional(), + getGlobalHealth +); + +module.exports = healthcheckRouter; diff --git a/src/routes/index.routes.js b/src/routes/index.routes.js index af08c213..5004eb88 100644 --- a/src/routes/index.routes.js +++ b/src/routes/index.routes.js @@ -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); @@ -38,4 +39,6 @@ mainRouter.post('/reload-config', rootControllers.reloadConfig); mainRouter.use('/wallet', walletRouter); +mainRouter.use('/health', healthcheckRouter); + module.exports = mainRouter; diff --git a/src/services/healthcheck.service.js b/src/services/healthcheck.service.js new file mode 100644 index 00000000..13f710b1 --- /dev/null +++ b/src/services/healthcheck.service.js @@ -0,0 +1,112 @@ +import { config as hathorLibConfig, healthApi, txMiningApi } from '@hathor/wallet-lib'; + +const { buildComponentHealthCheck } = require('../helpers/healthcheck.helper'); +const { friendlyWalletState } = require('../helpers/constants'); + +const healthService = { + /** + * Returns the health object for a specific wallet + * + * @param {HathorWallet} wallet + * @param {string} walletId + * @returns {Object} + */ + 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; + }, + + /** + * Returns the health object for the connected fullnode + * + * @returns {Object} + */ + 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; + }, + + /** + * Returns the health object for the connected tx-mining-service + * + * @returns {Object} + */ + 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;