diff --git a/rest-api/monitoring/README.md b/rest-api/monitoring/README.md new file mode 100644 index 00000000000..4b680dc53df --- /dev/null +++ b/rest-api/monitoring/README.md @@ -0,0 +1,73 @@ +# Monitoring a live deployment of Hedera Mirror Node + +This code runs on an external server outside of the Hedera Beta MirrorNode deployment, and periodically polls the REST APIs exposed by the Hedera mirror node to ensure that the deployed APIs are working. +It also provides a simple dashboard to monitor the status. + +## Overview + +Hedera mirror nodes REST APIs expose /transactions, /balances and /accounts endpoints. +To monitor a live deployment of a Hedera mirror node, this code consists of monitoring APIs and monitoring dashboard as described below. + +#### Monitoring APIs: +A process runs that periodically polls the APIs exposed by the deployment of a Hedera mirror node. +It then checks the responses using a few simple checks for /transactions, /balances and /accounts APIs. +The results of these checks are exposed as a set of REST APIs exposed by this monitoring service as follows: + +| API | HTTP return code | Description | +|-----| -----------------| ------------| +|/api/v1/status | 200(OK) | Provides a list of results of all tests run on all servers | +|/api/v1/status/{id} | 200 (OK) | If all tests pass for a server, then it returns the results | +| | 4xx | If any tests fail for a server, or if the server is not running, then it returns a 4xx error code to make it easy to integrate with alerting systems | + + +#### Monitoring dashboard: + +A dashboard polls the above-mentioned APIs and displays the results. + +---- + +## Quickstart + +### Requirements + +- [ ] List of addresses of Hedera mirror nodes that you want to monitor +- [ ] An external server where you want to run this code to monitor the mirror node. You will need two TCP ports on the server. +- [ ] npm and pm2 + + +``` +git clone git@github.com:hashgraph/hedera-mirror-node.git +cd hedera-mirror-node/rest-apis/monitoring +``` + +To run the monitor_apis backend: +``` +cd monitor_apis +cp config/sample.serverlist.json config/serverlist.json // Start with the sample configuration file +nano config/serverlist.json // Insert the mirror node deployments you want to monitor +npm install // Install npm dependencies +PORT=xxxx npm start // To start the monitoring server on port xxxx (Note: please enter a number for xxxx) +``` +The server will start polling Hedera mirror nodes specified in the config/serverlist.json file. +The default timeout to populate the data is 2 minutes. After 2 minutes, you can verify the output using `curl :/api/v1/status` command. + + +To run the dashboard (from hedera-mirror-node/rest-apis/monitoring directory): +``` +cd monitor_dashboard +nano js/main.js // Change the server: 'localhost:3000' line to point to the ip-address/name and port of the server where you are running the monitoring backed as described in the above tests. +pm2 serve . yyyy // Serve the dashboard html pages on another port...(Note: please enter a number for yyyy) +``` + +Using your browser, connect to `http::/index.html` + +---- + +## Contributing + +Refer to [CONTRIBUTING.md](CONTRIBUTING.md) + +## License + +Apache License 2.0, see [LICENSE](LICENSE). + diff --git a/rest-api/monitoring/monitor_apis/.gitignore b/rest-api/monitoring/monitor_apis/.gitignore new file mode 100644 index 00000000000..bd2b4f1b67c --- /dev/null +++ b/rest-api/monitoring/monitor_apis/.gitignore @@ -0,0 +1,2 @@ +node_modules +serverlist.json diff --git a/rest-api/monitoring/monitor_apis/accounts.monitor.js b/rest-api/monitoring/monitor_apis/accounts.monitor.js new file mode 100644 index 00000000000..ff3a7797942 --- /dev/null +++ b/rest-api/monitoring/monitor_apis/accounts.monitor.js @@ -0,0 +1,90 @@ +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +'use strict'; + +const fetch = require("node-fetch"); +const common = require('./common.js'); +const config = require('./config/config.js'); + +let testAccounts; + +/** + * Base url for the accounts API + * @param {Object} server The server to run the test against + * @return {String} Base query string for Hedera mirror node REST API + */ +const getBaseUrl = (server) => { + return (`http://${server.ip}:${server.port}/api/v1/accounts`); +} + +/** + * Executes the /accounts API with no parameters + * Expects the response to have config.limits.RESPONSE_ROWS entries, and + * timestamp in the last n minutes as specified in the config.fileUpdateRefreshTimes + * @param {Object} server The server to run the test against + * @return {} None. updates testAccounts variable + */ +const getAccountsNoParams = async (server) => { + const url = getBaseUrl(server); + const response = await fetch(url); + const data = await response.json(); + + common.logResult (server, url, 'getAccountsNoParams', + (data.accounts.length === config.limits.RESPONSE_ROWS) ? + {result: true, msg: `Received ${config.limits.RESPONSE_ROWS} accounts`} : + {result: false, msg: `Received less than ${config.limits.RESPONSE_ROWS} accounts`}); + + const txSec = data.accounts[0].balance.timestamp.split('.')[0]; + const currSec = Math.floor(new Date().getTime() / 1000); + const delta = currSec - txSec; + + common.logResult (server, url, 'getAccountsNoParams', + (delta < (2 * config.fileUpdateRefreshTimes.balances)) ? + {result: true, msg: `Freshness: Received accounts from ${delta} seconds ago`} : + {result: false, msg: `Freshness: Got stale accounts from ${delta} seconds ago`} + ); + + testAccounts = data.accounts; +} + +/** + * Executes the /accounts API for querying one single account + * Expects the account id to match the requested account id + * @param {Object} server The server to run the test against + * @return {} None. + */ +const getOneAccount = async (server) => { + const accId = testAccounts[0].account; + const url = getBaseUrl(server) + '/' + accId; + const response = await fetch(url); + const data = await response.json(); + + common.logResult (server, url, 'getOneAccount', + (data.account === accId) ? + {result: true, msg: 'Received correct account'} : + {result: false, msg: 'Did not receive correct account'}); +} + +module.exports = { + testAccounts: testAccounts, + getAccountsNoParams: getAccountsNoParams, + getOneAccount: getOneAccount +} \ No newline at end of file diff --git a/rest-api/monitoring/monitor_apis/balances.monitor.js b/rest-api/monitoring/monitor_apis/balances.monitor.js new file mode 100644 index 00000000000..241ca80e05b --- /dev/null +++ b/rest-api/monitoring/monitor_apis/balances.monitor.js @@ -0,0 +1,119 @@ +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +'use strict'; + +const fetch = require("node-fetch"); +const common = require('./common.js'); +const config = require('./config/config.js'); + +let testBalances; +let testBalancesTimestamp; + +/** + * Base url for the balances API + * @param {Object} server The server to run the test against + * @return {String} Base query string for Hedera mirror node REST API + */ +const getBaseUrl = (server) => { + return (`http://${server.ip}:${server.port}/api/v1/balances`); +} + +/** + * Executes the /balances API with no parameters + * Expects the response to have config.limits.RESPONSE_ROWS entries, and + * timestamp in the last n minutes as specified in the config.fileUpdateRefreshTimes + * @param {Object} server The server to run the test against + * @return {} None. updates testBalances variable + */ +const getBalancesNoParams = async (server) => { + const url = getBaseUrl(server); + const response = await fetch(url); + const data = await response.json(); + + common.logResult (server, url, 'getBalancesNoParams', + (data.balances.length === config.limits.RESPONSE_ROWS) ? + {result: true, msg: `Received ${config.limits.RESPONSE_ROWS} balances`} : + {result: false, msg: `Received less than ${config.limits.RESPONSE_ROWS} balances`}); + + const balancesSec = data.timestamp.split('.')[0]; + const currSec = Math.floor(new Date().getTime() / 1000); + const delta = currSec - balancesSec; + + common.logResult (server, url, 'getBalancesNoParams', + (delta < (2 * config.fileUpdateRefreshTimes.balances)) ? + {result: true, msg: `Freshness: Received balances from ${delta} seconds ago`} : + {result: false, msg: `Freshness: Got stale balances from ${delta} seconds ago`} + ); + + testBalances = data.balances; + testBalancesTimestamp = data.timestamp; +} + +/** + * Executes the /balances API with timestamp filter + * Expects the response to have config.limits.RESPONSE_ROWS entries, and + * timestamp in the last n minutes as specified in the config.fileUpdateRefreshTimes + * @param {Object} server The server to run the test against + * @return {} None. updates testBalances variable + */ +const checkBalancesWithTimestamp = async (server) => { + const url = getBaseUrl(server) + '?timestamp=lt:' + testBalancesTimestamp; + const response = await fetch(url); + const data = await response.json(); + + common.logResult (server, url, 'checkBalancesWithTimestamp', + (data.balances.length === config.limits.RESPONSE_ROWS) ? + {result: true, msg: `Received ${config.limits.RESPONSE_ROWS} balances`} : + {result: false, msg: `Received less than ${config.limits.RESPONSE_ROWS} balances`}); + + common.logResult (server, url, 'checkBalancesWithTimestamp', + ((data.timestamp < testBalancesTimestamp) && + ((testBalancesTimestamp - data.timestamp) < (2 * config.fileUpdateRefreshTimes.balances))) + ? + {result: true, msg: 'Received older balances correctly'} : + {result: false, msg: 'Did not receive older balances correctly'}); +} + +/** + * Executes the /balances API for querying one single account + * Expects the account id to match the requested account id + * @param {Object} server The server to run the test against + * @return {} None. + */ +const getOneBalance = async (server) => { + const accId = testBalances[0].account; + const url = getBaseUrl(server) + '/?account.id=' + accId; + const response = await fetch(url); + const data = await response.json(); + + common.logResult (server, url, 'getOneBalance', + ((data.balances.length === 1) && + (data.balances[0].account === accId)) ? + {result: true, msg: 'Received correct account balance'} : + {result: false, msg: 'Did not receive correct account balance'}); +} + +module.exports = { + testBalances: testBalances, + getBalancesNoParams: getBalancesNoParams, + checkBalancesWithTimestamp: checkBalancesWithTimestamp, + getOneBalance: getOneBalance +} \ No newline at end of file diff --git a/rest-api/monitoring/monitor_apis/common.js b/rest-api/monitoring/monitor_apis/common.js new file mode 100644 index 00000000000..3e0dc21e125 --- /dev/null +++ b/rest-api/monitoring/monitor_apis/common.js @@ -0,0 +1,175 @@ +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +'use strict'; + +const fs = require('fs'); + +let currentResults = {}; // Results of current tests are stored here +let testResultsSnapshot = {}; // A snapshot of the last results - used for returning values in the API + +/** + * Copies the current results to the results snapshot, and initializes + * the current results object to get ready for next iteration of testing + * @param {Object} server The server under test + * @return {} None. Updates currentResults + */ +const initResults = (server) => { + testResultsSnapshot[server.name] = + currentResults[server.name] === undefined ? {} : + JSON.parse(JSON.stringify(currentResults[server.name])); + currentResults[server.name] = { + ip: server.ip, + port: server.port, + results: [] + }; +} + +/** + * Log results of a single test to the current results + * @param {Object} server The server to run the test against + * @param {String} url The URL of the test + * @param {String} funcName Name of the test function that produced this result + * @param {Object} result Result of the test (pass/fail + message) + * @return {} None. Updates currentResults + */ +const logResult = (server, url, funcName, result) => { + console.log ((result.result ? 'PASSED' : 'FAILED') + + ' (' + JSON.stringify(server) + ')' + + ': ' + url + ":: " + result.msg); + + currentResults[server.name].results.push({ + at: (new Date().getTime() / 1000).toFixed(3), + result: result.result, + url: url, + message: funcName + ': ' + result.msg + }); +} + +/** + * Prints the results + * @param {} None + * @return {} None + */ +const printResults = async function () { + console.log ("Results:"); + + for (let servername in currentResults) { + console.log ("Server: " + servername); + await currentResults[servername].results.forEach ((result) => { + console.log (JSON.stringify(result)); + }); + console.log ("--------------"); + } +} + +/** + * Getter for a snapshot of results + * @param {} None + * @return {Object} Snapshot of results from the latest completed round of tests + */ +const getStatus = () => { + return (testResultsSnapshot); +} + +/** + * Getter for a snapshot of results for a server specified in the HTTP request + * @param {HTTPRequest} req HTTP Request object + * @param {HTTPResponse} res HTTP Response object + * @return {Object} Snapshot of results from the latest completed round of tests for + * the specified server + */ +const getStatusWithId = (req, res) => { + const net = req.params.id; + + if ((net == undefined) || + (net == null)) { + res.status(404) + .send(`Not found. Net: ${net}`); + return; + } + + if (!(testResultsSnapshot.hasOwnProperty(net)) || + (testResultsSnapshot.hasOwnProperty(net) == undefined)) { + res.status(404) + .send(`Test results unavailable for Net: ${net}`); + return; + } + + if (! (testResultsSnapshot[net].hasOwnProperty('results'))) { + res.status(404) + .send(`Test results unavailable for Net: ${net}`); + return; + } + + let cntPass = 0; + let cntFail = 0; + let cntTotal = 0; + + for (let row of testResultsSnapshot[net].results) { + if (row.result) { + cntPass ++; + } else { + cntFail ++; + } + } + cntTotal = cntPass + cntFail; + + if (cntFail > 0) { + res.status(409) + .send(`{pass: ${cntPass}, fail: ${cntFail}, total: ${cntTotal}}`); + return; + } + return (testResultsSnapshot[net]); +} + + +/** + * Read the servers list file + * @param {} None + * @return {Object} config The configuration object + */ +const getServerList = () => { + const SERVERLIST_FILE = './config/serverlist.json'; + + try { + const configtext = fs.readFileSync(SERVERLIST_FILE); + const config = JSON.parse(configtext); + return (config); + } catch (err) { + return ({ + "api": { + "ip": "localhost", + "port": 80 + }, + "servers": [], + "interval": 30 + }); + } +} + +module.exports = { + initResults: initResults, + logResult: logResult, + printResults: printResults, + getStatus: getStatus, + getStatusWithId: getStatusWithId, + getServerList: getServerList +} \ No newline at end of file diff --git a/rest-api/monitoring/monitor_apis/config/config.js b/rest-api/monitoring/monitor_apis/config/config.js new file mode 100644 index 00000000000..fe8623f6db4 --- /dev/null +++ b/rest-api/monitoring/monitor_apis/config/config.js @@ -0,0 +1,37 @@ +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ +const config = { + limits: { + RESPONSE_ROWS: 1000 + }, + + // Refresh times for each type of files (in seconds) + fileUpdateRefreshTimes: { + records: 2 * 60, // Record files are updated this often + balances: 15 * 60, // Balance files are updated this often + events: 5 * 60 // Event files are updated this often + }, + + network: { + numNodes: 39 + } +} + +module.exports = config; diff --git a/rest-api/monitoring/monitor_apis/config/sample.serverlist.json b/rest-api/monitoring/monitor_apis/config/sample.serverlist.json new file mode 100644 index 00000000000..5870f82ea98 --- /dev/null +++ b/rest-api/monitoring/monitor_apis/config/sample.serverlist.json @@ -0,0 +1,6 @@ +{ + "servers": [ + {"ip": "127.0.0.1", "port": 3000, "name": "MyLocalMirrorNode"} + ], + "interval": 60 +} \ No newline at end of file diff --git a/rest-api/monitoring/monitor_apis/monitor.js b/rest-api/monitoring/monitor_apis/monitor.js new file mode 100644 index 00000000000..7dd9cd8fb52 --- /dev/null +++ b/rest-api/monitoring/monitor_apis/monitor.js @@ -0,0 +1,95 @@ +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +'use strict'; + +const transMonitor = require('./transactions.monitor.js'); +const balancesMonitor = require('./balances.monitor.js'); +const accountsMonitor = require('./accounts.monitor.js'); +const common = require('./common.js'); + +// List of test functions +const funcs = [transMonitor.getTransactionsNoParams + , transMonitor.checkTransactionsWithTimestamp + , transMonitor.getOneTransaction + , balancesMonitor.getBalancesNoParams + , balancesMonitor.checkBalancesWithTimestamp + , balancesMonitor.getOneBalance + , accountsMonitor.getAccountsNoParams + , accountsMonitor.getOneAccount]; + +/** + * Recursive: Run all tests in the funcs array for one server sequentially + * @param {Object} server The server to run the tests on + * @param {Integer} testIndex Index of the first test to be run + * @return {} None + */ +const runTests = async function (server, testIndex) { + if (testIndex < (funcs.length - 1)) { + const x = await funcs[testIndex](server) + await runTests(server, testIndex + 1); + } else { + const x = await funcs[testIndex](server); + } +} + +/** + * Run all tests in the funcs array on all the servers + * @param {Array} restservers List of servers to run tests against + * @return {} None + */ +const runOnAllServers = async function (restservers) { + for (let server of restservers) { + common.initResults(server); + try { + const a = await runTests(server, 0); + console.log(`Tests completed for server: ${server.name}`); + } catch (err) { + console.log('Error in runOnAllServers: ' + err); + console.trace(); + } + } +} + +/** + * Main function to run all tests and print results + * @param {} None + * @return {} None + */ +const runEverything = async function () { + try { + const restservers = common.getServerList().servers; + + if (restservers.length === 0) { + return; + } + + await runOnAllServers(restservers); + await common.printResults(); + } catch (err) { + console.log('Error in runEverything: ' + err); + console.log (err.stack) + console.trace(); + } +} + +module.exports = { + runEverything: runEverything +} \ No newline at end of file diff --git a/rest-api/monitoring/monitor_apis/package.json b/rest-api/monitoring/monitor_apis/package.json new file mode 100644 index 00000000000..2a9b82a396b --- /dev/null +++ b/rest-api/monitoring/monitor_apis/package.json @@ -0,0 +1,26 @@ +{ + "name": "backend", + "version": "1.0.0", + "description": "REST API for Hedera mirror node", + "main": "server.js", + "scripts": { + "test": "jest --watchAll", + "dev": "nodemon app.js" + }, + "author": "Atul Mahamuni", + "license": "Apache-2.0", + "dependencies": { + "body-parser": "^1.19.0", + "compression": "^1.7.4", + "cors": "^2.8.5", + "dotenv": "^8.0.0", + "express": "^4.17.1", + "log4js": "^4.3.1", + "node-fetch": "^2.6.0" + }, + "devDependencies": { + "jest": "^24.8.0", + "nodemon": "^1.19.1", + "supertest": "^4.0.2" + } +} diff --git a/rest-api/monitoring/monitor_apis/server.js b/rest-api/monitoring/monitor_apis/server.js new file mode 100644 index 00000000000..779bddaa545 --- /dev/null +++ b/rest-api/monitoring/monitor_apis/server.js @@ -0,0 +1,73 @@ +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +'use strict'; + +require("dotenv").config({ + path: './.env' +}); + +const express = require('express'); +const bodyParser = require('body-parser'); +const cors = require('cors'); +var compression = require('compression'); + +const common = require('./common.js'); +const monitor = require('./monitor.js'); + +const app = express(); + +const port = process.env.PORT; +if (port === undefined || isNaN(Number(port))) { + console.log('Please specify the port'); + process.exit(1); +} + +app.set('trust proxy', true) +app.set('port', port); +app.use(bodyParser.urlencoded({ + extended: false +})); +app.use(bodyParser.json()); +app.use(compression()); +app.use(cors()); + +let apiPrefix = '/api/v1'; + +// routes +app.get(apiPrefix + '/status', (req, res) => {res.json(common.getStatus())}); +app.get(apiPrefix + '/status/:id', (req, res) => {res.json(common.getStatusWithId(req, res))}); + + +if (process.env.NODE_ENV !== 'test') { + app.listen(port, () => { + console.log(`Server running on port: ${port}`); + }); +} + +let interval = common.getServerList().interval; + +// Run all the tests periodically +setInterval(() => { + console.log ("Running the tests at: " + new Date()); + monitor.runEverything(); +}, interval * 1000); + +module.exports = app; diff --git a/rest-api/monitoring/monitor_apis/transactions.monitor.js b/rest-api/monitoring/monitor_apis/transactions.monitor.js new file mode 100644 index 00000000000..1cf01786ae8 --- /dev/null +++ b/rest-api/monitoring/monitor_apis/transactions.monitor.js @@ -0,0 +1,118 @@ +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +'use strict'; + +const fetch = require("node-fetch"); +const common = require('./common.js'); +const config = require('./config/config.js'); + +let testTransactions; + +/** + * Base url for the transactions API + * @param {Object} server The server to run the test against + * @return {String} Base query string for Hedera mirror node REST API + */ +const getBaseUrl = (server) => { + return (`http://${server.ip}:${server.port}/api/v1/transactions`); +} + +/** + * Executes the /transactions API with no parameters + * Expects the response to have config.limits.RESPONSE_ROWS entries, and + * timestamp in the last n minutes as specified in the config.fileUpdateRefreshTimes + * @param {Object} server The server to run the test against + * @return {} None. updates testTransactions variable + */ +const getTransactionsNoParams = async (server) => { + const url = getBaseUrl(server); + const response = await fetch(url); + const data = await response.json(); + + common.logResult (server, url, 'getTransactionsNoParams', + (data.transactions.length == config.limits.RESPONSE_ROWS) ? + {result: true, msg: `Received ${config.limits.RESPONSE_ROWS} entries`} : + {result: false, msg: `Received less than ${config.limits.RESPONSE_ROWS} entries`}); + + const txSec = data.transactions[0].consensus_timestamp.split('.')[0]; + const currSec = Math.floor(new Date().getTime() / 1000); + const delta = currSec - txSec; + + common.logResult (server, url, 'getTransactionsNoParams', + (delta < (2 * config.fileUpdateRefreshTimes.records)) ? + {result: true, msg: `Freshness: Received transactions from ${delta} seconds ago`} : + {result: false, msg: `Freshness: Got stale transactions from ${delta} seconds ago`} + ); + + testTransactions = data.transactions; +} + +/** + * Executes the /transactions API with timestamp filter of 1 ns before the first + * transaction received in the getTransactionsNoParams call. + * Expects the response to have config.limits.RESPONSE_ROWS entries, and the + * returned transactions list to be offset by 1 initial transaction. + * @param {Object} server The server to run the test against + * @return {} None. + */ +const checkTransactionsWithTimestamp = async (server) => { + const url = getBaseUrl(server) + '?timestamp=lt:' + testTransactions[0].consensus_timestamp; + const response = await fetch(url); + const data = await response.json(); + + common.logResult (server, url, 'checkTransactionsWithTimestamp', + (data.transactions.length === config.limits.RESPONSE_ROWS) ? + {result: true, msg: `Received ${config.limits.RESPONSE_ROWS} entries`} : + {result: false, msg: `Received less than ${config.limits.RESPONSE_ROWS} entries`}); + + common.logResult (server, url, 'checkTransactionsWithTimestamp', + (data.transactions[0].transaction_id === + testTransactions[1].transaction_id) ? + {result: true, msg: 'Transaction ids matched'} : + {result: false, msg: 'Transaction ids do not match'}); +} + +/** + * Executes the /transactions API for querying one single transaction + * Expects the transaction id to match the requested transaction id + * @param {Object} server The server to run the test against + * @return {} None. + */ +const getOneTransaction = async (server) => { + const txId = testTransactions[0].transaction_id; + const url = getBaseUrl(server) + '/' + txId; + const response = await fetch(url); + const data = await response.json(); + + common.logResult (server, url, 'getOneTransaction', + ((data.transactions.length <= config.network.numNodes) && + (data.transactions[0].transaction_id === txId)) ? + {result: true, msg: 'Received correct transaction'} : + {result: false, msg: 'Did not receive correct transaction'}); +} + + +module.exports = { + testTransactions: testTransactions, + getTransactionsNoParams: getTransactionsNoParams, + checkTransactionsWithTimestamp: checkTransactionsWithTimestamp, + getOneTransaction: getOneTransaction +} \ No newline at end of file diff --git a/rest-api/monitoring/monitor_dashboard/._index.html b/rest-api/monitoring/monitor_dashboard/._index.html new file mode 100644 index 00000000000..60154900827 Binary files /dev/null and b/rest-api/monitoring/monitor_dashboard/._index.html differ diff --git a/rest-api/monitoring/monitor_dashboard/css/main.css b/rest-api/monitoring/monitor_dashboard/css/main.css new file mode 100644 index 00000000000..10484d3c92d --- /dev/null +++ b/rest-api/monitoring/monitor_dashboard/css/main.css @@ -0,0 +1,55 @@ +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +.dot { + height: 20px; + width: 20px; + border-radius: 50%; + display: inline-block; + margin-right: 10px; + align-self: center; +} + +.my-card { + margin: 10px 30px 10px 30px !important; + background-color:#f0f0f0; +} + +.card-title { + font-size: 2em; + display: inline-block; +} + +.ip-addr { + font-size: 1.5em; + color: grey; + display: inline-block; + margin-bottom: 10px; +} + +.card-arrow { + position: absolute; + top: 50px; + right: 30px; +} + +.modal-content { + width: 700px; +} \ No newline at end of file diff --git a/rest-api/monitoring/monitor_dashboard/index.html b/rest-api/monitoring/monitor_dashboard/index.html new file mode 100644 index 00000000000..4d0e7fa073a --- /dev/null +++ b/rest-api/monitoring/monitor_dashboard/index.html @@ -0,0 +1,44 @@ + + + + + + Hedera mirror net monitoring dashboard + + + + + + + + + + + + + +
+
+ + \ No newline at end of file diff --git a/rest-api/monitoring/monitor_dashboard/js/main.js b/rest-api/monitoring/monitor_dashboard/js/main.js new file mode 100644 index 00000000000..8ccbbcf3416 --- /dev/null +++ b/rest-api/monitoring/monitor_dashboard/js/main.js @@ -0,0 +1,172 @@ +/*- + * ‌ + * Hedera Mirror Node + * ​ + * Copyright (C) 2019 Hedera Hashgraph, LLC + * ​ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ‍ + */ + +'use strict'; + +// TODO: Make this configurable +// This needs to be configured before deploying this file +let config = { + server: 'localhost:3000' +} + +/** + * Initializer - fetches the status of tests from the server + * @param {} None + * @return {} None + */ +const init = () => { + let app = document.getElementById('root'); + + app.innerHTML = ''; + + let container = document.createElement('div'); + container.setAttribute('class', 'container'); + container.id = 'rootcontainer'; + app.appendChild(container); + + fetchAndDisplay(container); +} + +/** + * Creates an html table from the test results + * @param {Object} data The data returned by the REST API + * @param {Object} server The server under test + * @return {HTML} HTML for the table + */ +const makeTable = (data, server) => { + let h = ''; + h += ` + + + + + + `; + data[server].results.forEach ((result) => { + h += + '' + + '' + + '' + + '' + + '\n'; + }); + h += '
ResultAtMessage and URL link
' + '' + '' + new Date(Number(result.at) * 1000).toLocaleString() + '' + '' + result.message + '' + '
\n'; + return (h); +} + +/** + * Makes a card for the given server + * @param {Object} data The data returned by the REST API + * @param {Object} server The server under test + * @return {HTML} HTML for the card for the given server + */ +const makeCard = (data, server) => { + let cntPassed = 0; + let cntFailed = 0; + + if (!('results' in data[server])) { + return ('No data received yet for at least one of the servers in your list ...'); + } + data[server].results.forEach((row) => { + if (row.result) { + cntPassed ++; + } else { + cntFailed ++; + } + }); + const cntTotal = cntPassed + cntFailed; + const dotcolor = (cntTotal > 0 && cntFailed === 0) ? 'green' : 'red'; + + let h = ''; + // Create a summary card + h += ` +
+
+
Network: ${server} +
+
(${data[server].ip}:${data[server].port})
+
+
+ + ${cntPassed} / ${cntTotal} Passed, + ${cntFailed} / ${cntTotal} Failed.    +
+
+
+
+
`; + + // Create a modal for showing test details + h += ` + `; + + return (h); +} + + +/** + * Fetch the results from the backend and display the results + * @param {} None + * @return {} None + */ +const fetchAndDisplay = () => { + const container = document.getElementById('rootcontainer'); + if (container === null) { + console.log ("No container found!"); + return; + } + + fetch(`http://${config.server}/api/v1/status`) + .then(function (response) { + return response.json(); + }) + .then(function (data) { + console.log(data); + let html; + if (Object.keys(data).length === 0) { + html = `No data received. +

+ If you have started the backend server recently, + please wait for a couple of minutes and refresh this page +

`; + } else { + html = ` +

Hedera mirror node status

+ ${Object.keys(data).map(server => + `
${makeCard(data, server)}
`).join('')} + `; + } + container.innerHTML = html; + }) +}