facebook
Bob at Genuitec
Virtual evangelist at large. The face of Genuitec often appearing in graphics. Pen-name for our more shy writers of content.
Posted on Aug 14th 2017

This article will show you how to build a very simple monitor that allows you to observe some OS parameters, such as free memory available. We will be using Node.js, Angular 4, and Chart.js – and all you need is Angular IDE.

We will be creating two applications; the first is a Node application that will monitor the OS parameters and send them via websocket to the second app, which is an Angular 4 application. The Angular app will utilize Chart.js to represent the server status graphically.

Server App

To keep things simple, we will run our Node application on the same machine as the Angular application; typically, you would be executing this code on your server, a different physical, or virtual machine.
If you don’t already have Angular IDE, download and install it now. Create a new project using the Faceted Project wizard (File > New > Other > General > Faceted Project) and name it DuneServer.

faceted-project-wizard

Create a package.json file in the root folder of the project with the following content

{
  "name": "duneserver",
  "version": "1.0.0",
  "description": "Realtime server monitoring app with Angular 4 & NodeJS",
  "main": "index.js",
  "scripts": {
    "start": "forever start index.js",
    "dev": "nodemon index.js",
    "test": "echo 'Error: no test specified'"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "os-monitor": "~1.0.5",
    "nodemon": "~1.11.0",
    "socket.io": "~2.0.3"
  }
}

Our `package.json` contains 3 dependencies: `os-monitor` – a very simple monitor for the built-in os module in Node.js, `nodemon` – monitor for any changes in your Node.js application and automatically restart the server (perfect for development), and `socket.io` – enables real-time bidirectional event-based communication.

Now, in the Terminal+ view (Window > Show View > Terminal+) , let’s select the DuneServer project and type in the following command.
npm install

npm-install

This will download and install all the dependencies of the project as Node modules.

Let’s also create the main code for our application, create a new file named index.js in the root of the project, and place the following code in it.

var io = require('socket.io')(9500);
var osm = require("os-monitor");


io.on('connect', function (socket) {
    socket.emit('connected', {
        status: 'connected',
        type: osm.os.type(), 
        cpus: osm.os.cpus(),
    });
});

io.on('disconnect', function (socket) {
    socket.emit('disconnected');
});


osm.start({
    delay: 3000 // interval in ms between monitor cycles
    , stream: false // set true to enable the monitor as a Readable Stream
    , immediate: false // set true to execute a monitor cycle at start()
}).pipe(process.stdout);


// define handler that will always fire every cycle
osm.on('monitor', function (monitorEvent) {
    io.emit('os-update', monitorEvent);
});

In the code above we imported `socket.io` and created a server running on port 9500, next we imported `os-monitor.` The `socket.io` server will emit a `connect` event when a client app connects with it, so on `connect` we send the os type (`osm.os.type`) and number of cpus (`osm.os.cpus()`) to the client.

In the last two sections of the code above, we start monitoring the server using `osm.start()`, at an interval of 300ms. `os-monitor` emits a `monitor` event on each monitor cycle, so on `monitor` we use `socket.io` to send the `monitorEvent` to the client.

When the dependencies from the previous step have finished downloading, open the Terminal+ view again and type the following command to run our server app.
npm run dev

npm-run
Note: If you see an EADDRINUSE error show up in the Terminal when executing the above command, the port 9500 may already be in use on your system. Change it to another port, but do remember to change the references to this port in the Angular app below as well.

Angular App, Socket.io and Chart,js

Let’s move on to the Angular frontend, by creating a new Angular project named `DuneServerMonitor`. Use the Angular project wizard at File > New > Angular Project.

new-angular-project

Once the project is created, we can now add Socket.io and Chart.js to our project. In the Terminal+ view, with DuneServerMonitor selected, type the following commands to install Socket.io and Chart.js:

`npm install socket.io chart.js –save`

Monitoring the free memory

Replace the content of `src/app/app.component.html` with:

<div class="container">
    <h1>Dune Server Monitor</h1>
    <div style="background:#eee" class="">
        <div class="col-md-4">
            <h4>Memory</h4>
            <canvas id="mChart" width="400" height="400"></canvas>
        </div>

        <div class="col-md-4">
            <h4>Cpu load average</h4>
            <canvas id="cChart" width="400" height="400"></canvas>
        </div>
    </div>
</div>

Replace the content of `src/app/app.component.ts` with:

// Imports
import { Component, OnInit } from '@angular/core';
import io from 'socket.io-client';
import { Chart, pattern } from 'chart.js';

// ---------------------------------------------------Section I
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app';
  socket = io.connect('localhost:9500');
  memChart = undefined;
  cpuChart = undefined;
  cpuType = '';
  noOfCpu = '';
  ngOnInit() {
// ---------------------------------------------------Section II
    const ctx = document.getElementById('mChart');
    const doughnutGraphData = {
      datasets: [{
        data: [1, 0],
        backgroundColor: ['#36a2eb', '#ff6384'],
      }],
      labels: [
        'Free',
        'Used',
      ]
    };
    this.memChart = new Chart(ctx, {
      type: 'doughnut',
      data: doughnutGraphData,
      options: {}
    });

    const ctx2 = document.getElementById('cChart');
    const cpuLoadGraphData = {
      datasets: [{
        label: '15 min average',
        data: [],
        backgroundColor: 'rgba(75, 192, 192, 0.4)',
      }],
      labels: ['x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x'],

    };
    this.cpuChart = new Chart(ctx2, {
      type: 'line',
      data: cpuLoadGraphData,
      options: {}
    });

// ----------------------------------------------------------------------Section III
    this.socket.on('connected', (connectData) => this.connected(connectData));
    this.socket.on('os-update', (event) => this.updateCharts(event));
  }

// -----------------------------------------------------------------------Section IV
  updateCharts(event) {

    this.memChart.data.labels.pop();
    this.memChart.data.labels.pop();
    this.memChart.data.labels.push(`Free:${this.formatBytes(event.freemem, 2)}`);
    this.memChart.data.labels.push(`Used:${this.formatBytes(event.totalmem - event.freemem, 2)}`);

    this.memChart.data.datasets.forEach((dataset) => {
      dataset.data.pop();
      dataset.data.pop();
      dataset.data.push(event.freemem);
      dataset.data.push(event.totalmem - event.freemem);
    });
    this.memChart.update(0);

    this.cpuChart.data.datasets.forEach((dataset) => {
      if ( dataset.data.length > 9) {
        dataset.data.shift();
      }
      dataset.data.push(event.loadavg[2]);
    });
    this.cpuChart.update(0);
  }

  formatBytes(bytes, decimals) {
    if (bytes === 0) {
      return '0 Bytes';
    }
    const k = 1000,
      dm = decimals || 2,
      sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
      i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  }

  connected(connectData) {
    this.cpuType = connectData.types;
    this.noOfCpu = connectData.cpus;
  }
}
Finally, for styling, let’s add some boostrap CSS to our index.html file. Add the following snippet of code into the head section of the src/index.html file.
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
    integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">

<!-- Optional theme -->
<link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css"
    integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp"
    crossorigin="anonymous">
Our client app looks like the image below, so we’ll now go on to discuss the sections of the code.

duneservermonitor-app

Section I

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

We used the Component decorator to mark our class as an Angular component and provide additional metadata that determines how the component should behave. The app component (`app.component.ts`) uses the content of `app.component.html` as its template and applies the style rules in `app.component.css`.

export class AppComponent implements OnInit {
  title = 'app';
  socket = io.connect('localhost:9500');
  memChart = undefined;
  cpuChart = undefined;
  cpuType = '';
  noOfCpu = '';
  ngOnInit() {

Our app class implements `OnInit` interface which we imported from ‘@angular/core’, so we can use the `ngOnInit` life cycle hook called by Angular to indicate that Angular is done creating the component. Finally, we created some class variables to be used in the succeeding sections.

  • `socket` which is an instance of the `socket.io` client
  • `memChart` currently undefined, but will be assigned as an instance of `Chart` from Chart.js
  • `cpuChart` currently undefined, but will be assigned as an instance of `Chart` from Chart.js
  • `cpuType` and `noOfCpu` will be used to store the variables returned from the section of the server code highlighted below
io.on('connect', function (socket) {
    socket.emit('connected', {
        status: 'connected',
        type: osm.os.type(), 
        cpus: osm.os.cpus(),
    });
});

Section II

const ctx = document.getElementById('mChart');
    const doughnutGraphData = {
      datasets: [{
        data: [1, 0],
        backgroundColor: ['#36a2eb', '#ff6384'],
      }],
      labels: [
        'Free',
        'Used',
      ]
    };
    this.memChart = new Chart(ctx, {
      type: 'doughnut',
      data: doughnutGraphData,
      options: {}
    });

Here we created a doughnut chart to be used to display the free memory.

const ctx2 = document.getElementById('cChart');
    const cpuLoadGraphData = {
      datasets: [{
        label: '15 min average',
        data: [],
        backgroundColor: 'rgba(75, 192, 192, 0.4)',
      }],
      labels: ['x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x'],

    };
    this.cpuChart = new Chart(ctx2, {
      type: 'line',
      data: cpuLoadGraphData,
      options: {}
    });

Here we created a line chart to be used to display the 15 minute cpu load average.

Section III

this.socket.on('connected', (connectData) => this.connected(connectData));
this.socket.on('os-update', (event) => this.updateCharts(event));

Here we are setting callbacks for the `connected` and `os-update` events emitted by our `socket.io` server.

  • We set the `connected` method as the callback for the `connected` event
  • We set the `updateCharts` method as the callback for the `os-update` event

Section IV

this.memChart.data.labels.pop();
 this.memChart.data.labels.pop();
 this.memChart.data.labels.push(`Free:${this.formatBytes(event.freemem, 2)}`);
 this.memChart.data.labels.push(`Used:${this.formatBytes(event.totalmem - event.freemem, 2)}`);

Here we empty the the array that holds the labels for the free memory chart whose initial value is [`Free`,`Used`], then we push two new labels of the format `Free:XXMB` and `Used:XXMB`.

this.memChart.data.datasets.forEach((dataset) => {
  dataset.data.pop();
  dataset.data.pop();
  dataset.data.push(event.freemem);
  dataset.data.push(event.totalmem - event.freemem);
});
this.memChart.update(0);

Here we empty the dataset for the doughnut chart and push new values, then we update the chart.

this.cpuChart.data.datasets.forEach((dataset) => {
  if ( dataset.data.length > 9) {
    dataset.data.shift();
  }
  dataset.data.push(event.loadavg[2]);
});
this.cpuChart.update(0);

Note: The loadavg array is available only on Linux and macOS. On Windows, the array will always be [0,0,0], resulting in a flat graph.
Finally let us run the Angular app, there is a server view in Angular IDE, in order to open it, select `Window` then `Show view` then `Servers`, right click on `DuneServerMonitor` and click `Start Server`.

ng-serve

Use the Run or Debug context menu actions to automatically open the app in Chrome, or manually open  `http://127.0.0.1:4200` in your browser to see the app in action. Note that the Angular application is served at port 4200 by the `ng serve` command, executed by the Start Server action – which is why we use this port to access the app. This application itself still connects with the Node server we set up earlier at port 9500.

Our final real-time server monitoring dashboard looks like this:

duneservermonitor-app-final

`

`