From 1a166bc2798991c35d50b433681a2950f1214021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Fri, 16 Jul 2021 03:22:47 +0100 Subject: [PATCH] feat: machine performance indicators --- lib/machine-loader.js | 68 +++++++++++++++++-- lib/new-admin/graphql/types/machine.type.js | 3 + lib/plugins.js | 20 +++++- lib/poller.js | 2 + lib/routes/performanceRoutes.js | 18 ++--- ...5844773-add-machine-network-performance.js | 26 +++++++ .../Machines/MachineComponents/Overview.js | 32 +++++++++ .../src/pages/Machines/Machines.js | 3 + .../src/pages/Maintenance/MachineStatus.js | 40 ++++++++++- 9 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 migrations/1626275844773-add-machine-network-performance.js diff --git a/lib/machine-loader.js b/lib/machine-loader.js index b449e68b..d5f41415 100644 --- a/lib/machine-loader.js +++ b/lib/machine-loader.js @@ -1,5 +1,7 @@ const _ = require('lodash/fp') +const pgp = require('pg-promise')() const axios = require('axios') +const uuid = require('uuid') const db = require('./db') const pairing = require('./pairing') @@ -39,11 +41,14 @@ function getMachineNames (config) { const unresponsiveStatus = { label: 'Unresponsive', type: 'error' } const stuckStatus = { label: 'Stuck', type: 'error' } - return Promise.all([getMachines(), getConfig(config)]) - .then(([machines, config]) => Promise.all( - [machines, checkPings(machines), dbm.machineEvents(), config] + return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()]) + .then(([rawMachines, config, heartbeat, performance]) => Promise.all( + [rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance] )) - .then(([machines, pings, events, config]) => { + .then(([rawMachines, pings, events, config, heartbeat, performance]) => { + const mergeByDeviceId = (x, y) => _.values(_.merge(_.keyBy('deviceId', x), _.keyBy('deviceId', y))) + const machines = mergeByDeviceId(mergeByDeviceId(rawMachines, heartbeat), performance) + const getStatus = (ping, stuck) => { if (ping && ping.age) return unresponsiveStatus @@ -66,7 +71,6 @@ function getMachineNames (config) { return _.assign(r, { cashOut, statuses }) } - return _.map(addName, machines) }) } @@ -143,4 +147,56 @@ function setMachine (rec) { } } -module.exports = { getMachineName, getMachines, getMachine, getMachineNames, setMachine } +function updateNetworkPerformance (deviceId, data) { + const downloadSpeed = _.head(data) + const dbData = { + device_id: deviceId, + download_speed: downloadSpeed.speed, + created: new Date() + } + const cs = new pgp.helpers.ColumnSet(['device_id', 'download_speed', 'created'], + { table: 'machine_network_performance' }) + const onConflict = ' ON CONFLICT (device_id) DO UPDATE SET ' + + cs.assignColumns({ from: 'EXCLUDED', skip: ['device_id'] }) + const upsert = pgp.helpers.insert(dbData, cs) + onConflict + return db.none(upsert) +} + +function updateNetworkHeartbeat (deviceId, data) { + const avgResponseTime = _.meanBy(e => _.toNumber(e.averageResponseTime), data) + const avgPacketLoss = _.meanBy(e => _.toNumber(e.packetLoss), data) + const dbData = { + id: uuid.v4(), + device_id: deviceId, + average_response_time: avgResponseTime, + average_packet_loss: avgPacketLoss + } + const sql = pgp.helpers.insert(dbData, null, 'machine_network_heartbeat') + return db.none(sql) +} + +function getNetworkPerformance () { + const sql = `SELECT device_id, download_speed FROM machine_network_performance` + return db.manyOrNone(sql) + .then(res => _.map(_.mapKeys(_.camelCase))(res)) +} + +function getNetworkHeartbeat () { + const sql = `SELECT AVG(average_response_time) AS response_time, AVG(average_packet_loss) AS packet_loss, device_id + FROM machine_network_heartbeat + GROUP BY device_id` + return db.manyOrNone(sql) + .then(res => _.map(_.mapKeys(_.camelCase))(res)) +} + +module.exports = { + getMachineName, + getMachines, + getMachine, + getMachineNames, + setMachine, + updateNetworkPerformance, + updateNetworkHeartbeat, + getNetworkPerformance, + getNetworkHeartbeat +} diff --git a/lib/new-admin/graphql/types/machine.type.js b/lib/new-admin/graphql/types/machine.type.js index 5fb80dab..b50f28b7 100644 --- a/lib/new-admin/graphql/types/machine.type.js +++ b/lib/new-admin/graphql/types/machine.type.js @@ -19,6 +19,9 @@ const typeDef = gql` cassette2: Int statuses: [MachineStatus] latestEvent: MachineEvent + downloadSpeed: String + responseTime: String + packetLoss: String } type MachineEvent { diff --git a/lib/plugins.js b/lib/plugins.js index 985f7365..82bfc3bc 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -289,6 +289,23 @@ function plugins (settings, deviceId) { ]) } + function pruneMachinesHeartbeat () { + const sql = `DELETE + FROM + machine_network_heartbeat h + USING (SELECT + device_id, + max(created) as lastEntry + FROM + machine_network_heartbeat + GROUP BY + device_id) d + WHERE + d.device_id = h.device_id + AND h.created < d.lastEntry` + db.none(sql) + } + function isHd (tx) { return wallet.isHd(settings, tx) } @@ -795,7 +812,8 @@ function plugins (settings, deviceId) { sell, getNotificationConfig, notifyOperator, - fetchCurrentConfigVersion + fetchCurrentConfigVersion, + pruneMachinesHeartbeat } } diff --git a/lib/poller.js b/lib/poller.js index e03bd689..52d43f76 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -24,6 +24,7 @@ const LOGS_CLEAR_INTERVAL = 1 * T.day const SANCTIONS_INITIAL_DOWNLOAD_INTERVAL = 5 * T.minutes const SANCTIONS_UPDATE_INTERVAL = 1 * T.week const RADAR_UPDATE_INTERVAL = 5 * T.minutes +const PRUNE_MACHINES_HEARBEAT = 1 * T.day const CHECK_NOTIFICATION_INTERVAL = 20 * T.seconds @@ -102,6 +103,7 @@ function start (__settings) { setInterval(initialSanctionsDownload, SANCTIONS_INITIAL_DOWNLOAD_INTERVAL) setInterval(updateAndLoadSanctions, SANCTIONS_UPDATE_INTERVAL) setInterval(updateCoinAtmRadar, RADAR_UPDATE_INTERVAL) + setInterval(() => pi().pruneMachinesHeartbeat(), PRUNE_MACHINES_HEARBEAT) } module.exports = { start, reload } diff --git a/lib/routes/performanceRoutes.js b/lib/routes/performanceRoutes.js index db34bd62..d09330c2 100644 --- a/lib/routes/performanceRoutes.js +++ b/lib/routes/performanceRoutes.js @@ -1,25 +1,17 @@ const express = require('express') const router = express.Router() -const { getMachine } = require('../machine-loader') +const { updateNetworkHeartbeat, updateNetworkPerformance } = require('../machine-loader') function networkHeartbeat (req, res, next) { - return getMachine(req.deviceId) - .then(machine => { - console.log(`${machine.name} network heartbeat:`) - console.log(req.body) - return res.status(200).send({ status: 'OK' }) - }) + return updateNetworkHeartbeat(req.deviceId, req.body) + .then(() => res.status(200).send({ status: 'OK' })) .catch(next) } function networkPerformance (req, res, next) { - return getMachine(req.deviceId) - .then(machine => { - console.log(`${machine.name} network performance:`) - console.log(req.body) - return res.status(200).send({ status: 'OK' }) - }) + return updateNetworkPerformance(req.deviceId, req.body) + .then(() => res.status(200).send({ status: 'OK' })) .catch(next) } diff --git a/migrations/1626275844773-add-machine-network-performance.js b/migrations/1626275844773-add-machine-network-performance.js new file mode 100644 index 00000000..344714ac --- /dev/null +++ b/migrations/1626275844773-add-machine-network-performance.js @@ -0,0 +1,26 @@ +const db = require('./db') + +exports.up = function (next) { + var sql = [ + 'drop table if exists machine_network_heartbeat', + 'drop table if exists machine_network_performance', + `create table machine_network_performance ( + device_id text PRIMARY KEY, + download_speed numeric NOT NULL, + created timestamptz NOT NULL default now() + )`, + `create table machine_network_heartbeat ( + id uuid PRIMARY KEY, + device_id text not null, + average_response_time numeric NOT NULL, + average_packet_loss numeric NOT NULL, + created timestamptz NOT NULL default now() + )` + ] + + db.multi(sql, next) +} + +exports.down = function (next) { + next() +} diff --git a/new-lamassu-admin/src/pages/Machines/MachineComponents/Overview.js b/new-lamassu-admin/src/pages/Machines/MachineComponents/Overview.js index 11f9f295..66f2076e 100644 --- a/new-lamassu-admin/src/pages/Machines/MachineComponents/Overview.js +++ b/new-lamassu-admin/src/pages/Machines/MachineComponents/Overview.js @@ -1,5 +1,6 @@ import { useMutation } from '@apollo/react-hooks' import { makeStyles } from '@material-ui/core/styles' +import BigNumber from 'bignumber.js' import gql from 'graphql-tag' import moment from 'moment' import React, { useState } from 'react' @@ -88,6 +89,37 @@ const Overview = ({ data, onActionSuccess }) => {

{makeLastPing(data.lastPing)}

+
+
+ Network speed +

+ {data.downloadSpeed + ? new BigNumber(data.downloadSpeed).toFixed(4).toString() + + ' MB/s' + : 'unavailable'} +

+
+
+
+
+ Latency +

+ {data.responseTime + ? new BigNumber(data.responseTime).toFixed(3).toString() + ' ms' + : 'unavailable'} +

+
+
+
+
+ Loss +

+ {data.packetLoss + ? new BigNumber(data.packetLoss).toFixed(3).toString() + ' %' + : 'unavailable'} +

+
+
{' '} diff --git a/new-lamassu-admin/src/pages/Machines/Machines.js b/new-lamassu-admin/src/pages/Machines/Machines.js index 79082773..2d489584 100644 --- a/new-lamassu-admin/src/pages/Machines/Machines.js +++ b/new-lamassu-admin/src/pages/Machines/Machines.js @@ -38,6 +38,9 @@ const GET_INFO = gql` label type } + downloadSpeed + responseTime + packetLoss } config } diff --git a/new-lamassu-admin/src/pages/Maintenance/MachineStatus.js b/new-lamassu-admin/src/pages/Maintenance/MachineStatus.js index 39da4c0b..adefc9c3 100644 --- a/new-lamassu-admin/src/pages/Maintenance/MachineStatus.js +++ b/new-lamassu-admin/src/pages/Maintenance/MachineStatus.js @@ -1,5 +1,6 @@ import { useQuery } from '@apollo/react-hooks' import { makeStyles } from '@material-ui/core' +import BigNumber from 'bignumber.js' import gql from 'graphql-tag' import moment from 'moment' import * as R from 'ramda' @@ -34,6 +35,9 @@ const GET_MACHINES = gql` label type } + downloadSpeed + responseTime + packetLoss } } ` @@ -58,7 +62,7 @@ const MachineStatus = () => { const elements = [ { header: 'Machine Name', - width: 250, + width: 150, size: 'sm', textAlign: 'left', view: m => ( @@ -76,18 +80,48 @@ const MachineStatus = () => { }, { header: 'Status', - width: 350, + width: 150, size: 'sm', textAlign: 'left', view: m => }, { header: 'Last ping', - width: 200, + width: 175, size: 'sm', textAlign: 'left', view: m => (m.lastPing ? moment(m.lastPing).fromNow() : 'unknown') }, + { + header: 'Network speed', + width: 150, + size: 'sm', + textAlign: 'left', + view: m => + m.downloadSpeed + ? new BigNumber(m.downloadSpeed).toFixed(4).toString() + ' MB/s' + : 'unavailable' + }, + { + header: 'Latency', + width: 150, + size: 'sm', + textAlign: 'left', + view: m => + m.responseTime + ? new BigNumber(m.responseTime).toFixed(3).toString() + ' ms' + : 'unavailable' + }, + { + header: 'Loss', + width: 125, + size: 'sm', + textAlign: 'left', + view: m => + m.packetLoss + ? new BigNumber(m.packetLoss).toFixed(3).toString() + ' %' + : 'unavailable' + }, { header: 'Software Version', width: 200,