This is an update to the original (several years old now) psutil and MongoDB for System Monitoring
The code for this article and the other two parts is now available on my GitHub Repo
The old version is the python2
branch.
The default branch (master
) has the code
we’re going over in this article.
Introduction
This three-part article enables you to create your own system-monitoring web page.
Part 1 introduced the project and covered the Python data gathering part: DIY System Monitoring, Part 1: Python
Part 2, covered the Node/Express web server DIY System Monitoring, Part 2
Part 3, this part, covers the JS/HTML front-end, DIY System Monitoring, Part 3: Visualization
Setting up Javascript Code
Last time we set up the NodeJS server so we can query the data in the MongoDB database.
Also we set up an static
directory for the server to provide static files like
HTML and Javascript. So this code will go into static/js/mychart.js
. See the GitHub
layout for complete details.
First, here’s a helper function to “humanize” the sizes we have in memory and disk space. It will abbreviate the number and add the appropriate unit next to it.
function formatBytes(bytes, decimals) {
if (bytes == 0) return '0';
const k = 1024,
dm = decimals <= 0 ? 0 : decimals || 2,
sizes = ['Bytes', 'kB', 'mB', 'gB'],
i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
Next, in getApiData
, we make our call to the server for a specific machine name.
- Set up the properties on our new data structure
data
- Make the call to the NodeJS api. This uses
fetch
, a cool interface for fetching resources. MDN fetch docs
Most of this code is just pushing data from MongoDB structure into our Javascript structure.
async function getApiData(machine) {
let data = {};
data.name = machine;
data.listening = '';
data.cpu = [];
data.mem = [];
data.disk_root = [];
data.disk_app = [];
await fetch("http://myserver/api/load/" + machine)
.then(stream => stream.json())
.then(mydata => {
data.listening = mydata.slice(-1)[0]['myapp'];
mydata.forEach(record => {
data.cpu.push({
y: record.cpu,
x: record.date
});
data.memory.push({
y: record.memory,
x: record.date
});
data.disk_root.push({
y: record.disk_root,
x: record.date
});
data.disk_app.push({
y: record.disk_app,
x: record.date
});
});
});
return data;
};
The one thing that’s not so obvious is this line:
data.listening = mydata.slice(-1)[0]['myapp'];
We have a bunch of data coming at us in an array and this picks off the last one. Since this is capped collection, we get the data in order, so by pulling off the last item in the array, we have the latest info from our machine.
Our array is mydata
, so mydata.slice(-1)
picks the
last set of records in the array. The [0]
gets the first element of
the set. So the slice(-1)[0]['myapp']
gets the boolean showing if the
port is listening. That value will be ‘true’, ‘false’, or ‘null’
(see the load_data.py
script for details).
Now we’ve got the data, we just need to configure Chart.js so we can look at it.
From the documentation at Chart.js, the library provides:
Simple yet flexible JavaScript charting for designers & developers … It’s easy to get started with Chart.js. All that’s required is the script included in your page along with a single
canvas
node to render the chart.
function makeChart(machine, data, kind) {
var ctx = document.getElementById(machine+kind);
let maxY = 100;
switch (kind) {
case 'cpu':
maxY = 100;
break;
case 'memory':
maxY = 32000000000; //32gb
break;
case 'disk_root':
maxY = 50000000000; //50gb
break;
case 'disk_app':
maxY = 100000000000; //100gb
break;
}
In the makeChart
function, we will call it for each chart we want.
And we want one chart for each combination of machine/kind, where kind
is cpu, memory, disk_app, or disk_root.
This portion finds the div to display the chart and sets some display options. You could set whatever you like here; all I needed was to set the maximum Y value so all the charts of the same type are scaled identically, for comparison.
We will make sure there are divs
in our HTML page with an id that specifies
the machine name and the kind of chart we want.
Now to create the actual line chart:
new Chart(ctx, {
type: 'line',
data: {
datasets: [{
label: kind,
data: data[kind],
borderWidth: 1.0,
borderColor: 'navy',
pointRadius: 0,
pointHitRadius: 3,
fill: false
}]
},
Here we’re loading in the data of the appropriate kind
with some
display options. There’s a lot you can play around with here for making
your chart look nice. The main thing is that the data is structured correctly
so the library can understand it. We created it with the right structure to begin with,
so no worries there.
options: {
legend: {display: false},
title: {
display: true,
fontSize: 10,
fontColor: ((data.listening === true || data.listening === null) ? 'green': 'red'),
text: machine + ' (' + kind + ')',
},
See how the title changes to green if the machine is up? That little bit of code makes it easy to see when a problem occurs: the titles on the charts for that machine light up in red.
plugins: {
zoom:{
zoom:{
enabled:true,
drag:true,
}
}
},
scales: {
xAxes: [{
type: 'time',
time: {
unit: "hour",
displayFormats: { hour: "dddhA" },
tooltipFormat: "MMM. DD ddd hA"
},
ticks: { autoSkip: false, maxTicksLimit: 5, fontSize: 8},
position: 'bottom'
}],
yAxes: [{
ticks: {
fontSize: 8,
suggestedMin: 0,
suggestedMax: maxY,
callback: function(value, index, values) {
if (kind === 'memory' || kind === 'disk_root' || kind === 'disk_app') {
return formatBytes(value, 1);
} else {
return value
}
}
}
}]
}
}
});
return data;
}
I wanted the ability to zoom in on a chart so I added the zoom
plugin.
I added some axis options (including the maximum Y value we calculated at the beginning).
For memory and disk space I send the value through the formatBytes
function to make
it easier to read and understand.
All Together
Our HTML looks like this:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.8.0/dist/Chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@0.7.0"></script>
We load the libraries we need from the CDNs.
<style>
.myserver {
height: 300px;
width: 900px;
}
canvas {
height: 275px;
width: 400px;
background: ghostwhite;
float: left;
}
</style>
<title> Build Cluster Load Monitoring</title>
</head>
Set some CSS for the chart display. I made the chart divs kind of small so I can fit four
charts in a row in the browser. That is small but it works fine for me, all I need is
a glance. And with the zoom
plugin, I can get a more detailed look if needed.
<body>
<p style="font-family:sans-serif">Build cluster health. Drag to zoom. Legend is below.</p>
<div class="myserver">
<table>
<tr>
<td><canvas id="myserver00cpu"></canvas></td>
<td><canvas id="myserver00memory"></canvas></td>
<td><canvas id="myserver00disk_root"></canvas></td>
<td><canvas id="myserver00disk_app"></canvas></td>
</tr>
<tr>
<td><canvas id="myserver01cpu"></canvas></td>
<td><canvas id="myserver01memory"></canvas></td>
<td><canvas id="myserver01disk_root"></canvas></td>
<td><canvas id="myserver01disk_app"></canvas></td>
</tr>
</table>
You can repeat that div for as many servers as you like. I have two servers here,
named myserver00
and myserver01
.
You can see we’re putting 4 charts into a table row, one row for each server.
Then at the end of the html, add the calls to our getApiData
function
for each server and kind
of chart.
<script src="./js/mychart.js"></script>
<script>
getApiData('myserver00')
.then(data => makeChart(data.name, data, 'memory'))
.then(data => makeChart(data.name, data, 'cpu'))
.then(data => makeChart(data.name, data, 'disk_root'))
.then(data => makeChart(data.name, data, 'disk_app'));
getApiData('myserver01')
.then(data => makeChart(data.name, data, 'memory'))
.then(data => makeChart(data.name, data, 'cpu'))
.then(data => makeChart(data.name, data, 'disk_root'))
.then(data => makeChart(data.name, data, 'disk_app'));
Here, I’m only showing 2 servers. In my real situation, I have one page with six servers and one page with ten.
Here’s what it looks like. In real life that image spans the browser window so it is a little easier to read. Click the image to see it full-size.
You now have little application to monitor any number of servers on whatever measurements you like (using Python’s psutil package), and you can refresh your HTML page to see the latest information.
Good luck and let me know how you’ve used your version of the app.