From 26429b7ab93739176f71470619ea36bf01205f66 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 11 May 2019 18:33:46 +0100 Subject: [PATCH] Display stats for all nodes running stats container --- .Python | 2 +- .gitignore | 9 +++++ app/files/application.js | 12 ++++-- app/files/cpu_stats.js | 25 ++++++++++++ app/files/dashboard.js | 45 ++++++++++++++++++--- app/files/memory_stats.js | 36 +++++++++++++++++ app/files/node.js | 83 +++++++++++++++++++++++++++++++++++++-- app/files/node_stats.js | 81 ++++++++++++++++++++++++++++++++++++++ app/files/service.js | 2 +- app/files/styles.css | 49 +++++++++++++++++++++++ app/templates/layout.html | 21 +++++++++- docker-compose.yml | 3 +- launch-dev-stats | 6 +++ stats/stats.py | 20 ++++++++-- 14 files changed, 375 insertions(+), 19 deletions(-) create mode 100644 app/files/cpu_stats.js create mode 100644 app/files/memory_stats.js create mode 100644 app/files/node_stats.js create mode 100755 launch-dev-stats diff --git a/.Python b/.Python index 2cc85da..0756951 120000 --- a/.Python +++ b/.Python @@ -1 +1 @@ -/usr/local/Cellar/python/3.7.2/Frameworks/Python.framework/Versions/3.7/Python \ No newline at end of file +/usr/local/Cellar/python/3.7.3/Frameworks/Python.framework/Versions/3.7/Python \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6d5e495..eb34bda 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,12 @@ app/.build stats/.build + +linux-metrics +bin/ +install/ +include/ +lib/ +pyvenv.cfg +.Python +.env diff --git a/app/files/application.js b/app/files/application.js index 469a19a..1e7c0a6 100644 --- a/app/files/application.js +++ b/app/files/application.js @@ -8,11 +8,11 @@ $(function () { setInterval(function () { socket.emit('manifest'); - }, 1000); + }, 5000); socket.on('manifest', function(data) { var manifest = JSON.parse(data); - console.log(manifest); + if (!Skep.dashboard) { Skep.dashboard = ReactDOM.render( , @@ -25,6 +25,12 @@ $(function () { socket.on('stats', function (json) { var data = JSON.parse(json); - Skep.dashboard.getNode(data.hostname).setState(data); + var node = Skep.dashboard.getNode(data.hostname) + if (!node) { + console.log('Could not find node for stats collection.', data); + return; + } + + node.ref.current.setState({ stats: data }); }); }); diff --git a/app/files/cpu_stats.js b/app/files/cpu_stats.js new file mode 100644 index 0000000..1fbe5f7 --- /dev/null +++ b/app/files/cpu_stats.js @@ -0,0 +1,25 @@ +class CPUStats { + constructor(stats) { + this.stats = stats; + } + + label() { + return this.percent(); + } + + level() { + const percent = 100 - this.stats.cpu_usage.idle; + + if (percent < 75) { + return 'success'; + } else if (percent < 90) { + return 'warning'; + } else { + return 'danger'; + } + } + + percent() { + return numeral(100 - this.stats.cpu_usage.idle).format('0.00') + '%'; + } +} diff --git a/app/files/dashboard.js b/app/files/dashboard.js index c74f1e7..0264dc3 100644 --- a/app/files/dashboard.js +++ b/app/files/dashboard.js @@ -1,6 +1,43 @@ class Dashboard extends React.Component { + constructor(props) { + super(props); + this._nodes = []; + } + getNode(hostname) { - return this.props.manifest.nodes.find(node => node.hostname == hostname); + return this._nodes.find( + node => node.hostname === hostname + ) + } + + nodes() { + return this.props.manifest.nodes.map( + node => this.findOrCreateNode(node) + ); + } + + findOrCreateNode(props) { + var found = this._nodes.find(node => node.id === props.id); + + if (found) { + return found; + } + + var node = this.node(props); + this._nodes.push(node); + return node; + } + + node(props) { + var ref = React.createRef(); + return { + id: props.id, + hostname: props.hostname, + ref: ref, + component: ( + + ) + } } render() { @@ -8,16 +45,14 @@ class Dashboard extends React.Component {

Nodes

- {this.props.manifest.nodes.map(node => ( - - ))} + {this.nodes().map(node => node.component)}

Stacks

{this.props.manifest.stacks.map(stack => ( diff --git a/app/files/memory_stats.js b/app/files/memory_stats.js new file mode 100644 index 0000000..7e571ba --- /dev/null +++ b/app/files/memory_stats.js @@ -0,0 +1,36 @@ +class MemoryStats { + constructor(stats) { + this.stats = stats; + } + + label() { + const free = this.formatNumber(this.free()); + const total = this.formatNumber(this.stats.total); + + return `${free} / ${total}`; + } + + level() { + const percent = 100 * (this.stats.used / this.stats.total); + + if (percent < 75) { + return 'success'; + } else if (percent < 90) { + return 'warning'; + } else { + return 'danger'; + } + } + + percent() { + return numeral(this.stats.used / this.stats.total).format('0.00%'); + } + + free() { + return this.stats.total - this.stats.used; + } + + formatNumber(number) { + return numeral(number).format('0.00b'); + } +} diff --git a/app/files/node.js b/app/files/node.js index 3d61763..910b542 100644 --- a/app/files/node.js +++ b/app/files/node.js @@ -1,10 +1,87 @@ class Node extends React.Component { + tasks() { + return this.services().map( + service => service.tasks.filter( + task => task.node_id === this.props.node.id + ) + ).flat(1) + } + + services() { + return this.stacks().map( + stack => stack.services.filter( + service => service.tasks.find( + task => task.node_id === this.props.node.id + ) + ) + ).flat(1) + } + + stacks() { + return this.props.stacks.filter( + stack => stack.services.filter( + service => service.tasks.find( + task => task.node_id === this.props.node.id + ) + ) + ) + } + + stats() { + if (!this.state || !this.state.stats) { + return null; + } + + return this.state.stats; + } + + roleClass() { + console.log(this.props.node.role); + if (this.props.node.role === 'manager') { + return 'primary'; + } else { + return 'info'; + } + } + + roleBadge() { + if (this.props.node.role === 'manager') { + return ( + + Manager + + ); + } else { + return ( + + Worker + + ); + } + } + + leaderBadge() { + if (!this.props.node.leader) { + return null; + } + + return ( + Leader + ); + } + render() { return (
-

{this.props.manifest.hostname}

-

{this.props.manifest.role}

-

{this.props.manifest.version}

+

{this.props.node.hostname}

+ + {this.roleBadge()} + {this.leaderBadge()} + +
); } diff --git a/app/files/node_stats.js b/app/files/node_stats.js new file mode 100644 index 0000000..9512586 --- /dev/null +++ b/app/files/node_stats.js @@ -0,0 +1,81 @@ +class NodeStats extends React.Component { + constructor(props) { + super(props); + this.initialize(props); + } + + initialize(props) { + const { stats } = props; + const { memory, cpu } = (stats || {}); + + this.memory = memory ? new MemoryStats(memory) : null; + this.cpu = cpu ? new CPUStats(cpu) : null; + } + + progress(options) { + const { percent, level, label, className } = options; + + return ( +
+
+ + {label} + +
+
+ ); + } + + renderMemory() { + if (!this.memory) return null; + + return this.progress({ + percent: this.memory.percent(), + label: this.memory.label(), + level: this.memory.level(), + className: 'memory' + }); + } + + renderCPU() { + if (!this.cpu) return null; + + return this.progress({ + percent: this.cpu.percent(), + label: this.cpu.label(), + level: this.cpu.level(), + className: 'cpu' + }); + } + + render() { + this.initialize(this.props); + + return ( +
+ + + + + + + + + + + +
+ {'RAM'} + + {this.renderMemory()} +
+ {'CPU'} + + {this.renderCPU()} +
+
+ ); + } +} diff --git a/app/files/service.js b/app/files/service.js index c636fb8..ed654c1 100644 --- a/app/files/service.js +++ b/app/files/service.js @@ -3,7 +3,7 @@ class Service extends React.Component { return (

{this.props.service.name}

-
+
{this.props.service.tasks.map(task => ( ))} diff --git a/app/files/styles.css b/app/files/styles.css index f075454..cb225de 100644 --- a/app/files/styles.css +++ b/app/files/styles.css @@ -4,6 +4,7 @@ width: 20%; border: 1px solid #f0f; padding: 1em; + overflow-y: scroll; } #stacks { @@ -19,3 +20,51 @@ margin: 1em; border: 1px solid #000; } + +.node { + border: 1px solid #999; +} + +.node h2 { + font-size: 1.5em; +} + +.node h3 { + font-size: 1.0em; +} + +.node h2.hostname { + font-family: monospace; + text-align: center; + padding: 0.2em; +} + +.node .badge { + margin: 0.2em; +} + +.node .alert { + padding: 0.2em; + margin: 0.2em; +} + +.node-stats table { + width: 100%; +} + +.node-stats th { + padding: 0.2em; +} + +.node-stats .label { + text-align: center; + width: 100%; + color: #555; + text-shadow: 0px 0px 5px #fff; + padding: 0.2em; +} + +.node-stats .progress { + padding: 0.2em; + margin: 0.2em; +} diff --git a/app/templates/layout.html b/app/templates/layout.html index 8724886..c0e906c 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -2,20 +2,39 @@
- + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml index 81fa132..d0b7f43 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,8 @@ services: - "/proc:/hostfs/proc:ro" - "/etc:/hostfs/etc:ro" environment: - SKEP_HOST_URL: http://app:8080 + SKEP_APP_URL: http://host.docker.internal:5000 + SKEP_HOST: LINUX_METRICS_ROOT_FS: '/hostfs' DISK_DRIVES: 'sda:sda1' NETWORK_INTERFACES: 'eth0' diff --git a/launch-dev-stats b/launch-dev-stats new file mode 100755 index 0000000..ace1747 --- /dev/null +++ b/launch-dev-stats @@ -0,0 +1,6 @@ +#!/bin/bash +cd stats +export SKEP_APP_URL=http://localhost:5000/ +export LINUX_METRICS_ROOT_FS=../linux-metrics/linux_metrics/test_filesystem/ + +../bin/python stats.py diff --git a/stats/stats.py b/stats/stats.py index 0081bef..8630dac 100644 --- a/stats/stats.py +++ b/stats/stats.py @@ -102,7 +102,19 @@ def logger(self, kwargs): return logger def hostname(): - path = os.environ.get('HOSTNAME_PATH', '/hostfs/etc/hostname') + if 'SKEP_HOST' in os.environ: + # Allow manual configuration for e.g. testing on Mac with Docker + # Machine. + return os.environ['SKEP_HOST'] + + path = os.environ.get( + 'HOSTNAME_PATH', + os.path.join( + os.environ.get('LINUX_METRICS_ROOT_FS', '/'), + '/etc/hostname' + ) + ) + try: return open(path, 'r').read().strip() except FileNotFoundError: @@ -114,9 +126,9 @@ def hostname(): if __name__ == '__main__': StatRunner( hostname=hostname(), - url=urllib.parse.urljoin(os.environ['SKEP_HOST_URL'], '/stats'), - drives=os.environ.get('DISK_DRIVES', '').split(','), - network=os.environ.get('NETWORK_INTERFACES', '').split(','), + url=urllib.parse.urljoin(os.environ['SKEP_APP_URL'], '/stats'), + drives=filter(None, os.environ.get('DISK_DRIVES', '').split(',')), + network=filter(None, os.environ.get('NETWORK_INTERFACES', '').split(',')), interval=int(os.environ.get('INTERVAL', '5')), duration=int(os.environ.get('DURATION', '1')), log_level=os.environ.get('LOG_LEVEL', 'INFO')