This blog post was written under the Pusher Guest Writer program.

Despite the hype of serverless architectures and microservices, there are still a lot of applications deployed in servers that need to be managed, and one important part of this task is monitoring resources like CPU, memory, or disk space.

There are a lot of commercial and open source tools for monitoring servers, but what if you just need something simple and specific? Maybe something that can easily show in realtime if things are doing fine, and that you can check on your phone.

In this tutorial, we’ll set up a Node.js process to calculate the memory usage of the system at specified intervals, send this information to a Pusher channel, and show it as a graph in an Android app.

This is how the final app will look:

You can find the source code of the Android app in this repository.

Setting up your Pusher application

Create a free account at https://pusher.com/signup.

When you create an app, you’ll be asked to enter some configuration options:

Enter a name, choose Android as your front-end tech, and Node.js as the back-end tech. This will give you some sample code to get you started:

But don’t worry, this won’t lock you into this specific set of technologies as you can always change them. With Pusher, you can use any combination of libraries.

Next, copy your cluster ID (next to the app title, in this example mt1), App ID, Key, and Secret information, we’ll need them next. You can also find them in the App Keys tab.

The Node process

In Node.js, the os module provides a number of operating system-related utility methods.

After requiring the module:

const os = require('os');

We can use the totalmem() function to get the total amount of system memory in bytes and freemem() to get the amount of free system memory, also in bytes.

This way, we use the setInterval function to get the memory information every ten seconds, for example, calculate the used memory, and publish it to a Pusher channel:

const os = require('os');
const Pusher = require('pusher');

// Set up Pusher
const pusher = new Pusher({
  appId: '<INSERT_PUSHER_APP_ID>',
  key: '<INSERT_PUSHER_APP_KEY>',
  secret: '<INSERT_PUSHER_APP_SECRET>',
  cluster: '<INSERT_PUSHER_APP_CLUSTER>',
  encrypted: true,
});

// To convert from bytes to gigabytes
const bytesToGigaBytes = 1024 * 1024 * 1024;
// To specify the interval (in milliseconds)
const intervalInMs = 10000;

setInterval(() => {
  const totalMemGb = os.totalmem()/bytesToGigaBytes;
  const freeMemGb = os.freemem()/bytesToGigaBytes;
  const usedMemGb = totalMemGb - freeMemGb;

  console.log(`Total: ${totalMemGb}`);
  console.log(`Free: ${freeMemGb}`);
  console.log(`Used: ${usedMemGb}`);

  // To publish to the channel 'stats' the event 'new_memory_stat' 
  pusher.trigger('stats', 'new_memory_stat', {
    memory: usedMemGb,
  });
}, intervalInMs);

Save this to a file, for example memory.js, create a package.json file if you haven’t already with:

npm init -y

Install the Pusher dependency with:

npm install --save pusher

And execute it with the command:

node memory.js

You should get the memory information printed in your console. Also, if you go to the Debug Console section of your app in the Pusher dashboard, you should see the events coming up:

Now let’s build the Android app.

Building the Android app

First, make sure to have the latest version of Android Studio. Then, create a new project:

We’re not going to use anything special, so we can safely support a low API level:

Next, create an initial empty activity:

And use the default name of MainActivity with backward compatibility:

Once everything is set up, let’s install the project dependencies. First, add the following repository to your project level build.gradle:

allprojects {
    repositories {
        ...
        maven { url "https://jitpack.io" }
    }
}

Next, in the dependencies section of the build.gradle file of your application module add:

dependencies {
    ...
    compile 'com.android.support:recyclerview-v7:25.1.1'
    compile 'com.github.PhilJay:MPAndroidChart:v3.0.2'
    compile 'com.pusher:pusher-java-client:1.4.0'
    compile 'com.google.code.gson:gson:2.4'
    ...
}

At the time of writing, the latest SDK version is 25, so that’s my target SDK version.

To graph the memory information we’re going to use MPAndroidChart, one of the most popular chart libraries for Android.

Sync the Gradle project so the modules can be installed and the project built.

Don’t forget to add the INTERNET permission to the AndroidManifest.xml file. This is required so we can connect to Pusher and get the events in realtime:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.pusher.photofeed">

    <uses-permission android:name="android.permission.INTERNET" />

    <application>
        ...
    </application>

</manifest>

Now, modify the layout file activity_main.xml to set a line chart that fills all the available space:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.pusher.memorygraph.MainActivity">

    <com.github.mikephil.charting.charts.LineChart
        android:id="@+id/chart"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</android.support.constraint.ConstraintLayout>

Open the com.pusher.memorygraph.MainActivity class. Let’s start by defining some constants, like the info we’ll need to instantiate the Pusher object. Also, let’s define the total memory of our server as 16 (gigabytes) and set a maximum limit of 12 to draw a limit line in our chart.

public class MainActivity extends AppCompatActivity {

    private LineChart mChart;

    private Pusher pusher;

    private static final String PUSHER_APP_KEY = "<INSERT_PUSHER_KEY>";
    private static final String PUSHER_APP_CLUSTER = "<INSERT_PUSHER_CLUSTER>";
    private static final String CHANNEL_NAME = "stats";
    private static final String EVENT_NAME = "new_memory_stat";

    private static final float TOTAL_MEMORY = 16.0f;
    private static final float LIMIT_MAX_MEMORY = 12.0f;

    ...

}

In the next code block, you can see how the job of configuring the chart is divided into four functions, how Pusher is set up, specifying that when an event arrives, the JSON object will be converted to an instance of the class Stat (that just contains the property memory) and this will be added to the chart with the addEntry(stat) method.

public class MainActivity extends AppCompatActivity {

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mChart = (LineChart) findViewById(R.id.chart);

        setupChart();
        setupAxes();
        setupData();
        setLegend();

        PusherOptions options = new PusherOptions();
        options.setCluster(PUSHER_APP_CLUSTER);
        pusher = new Pusher(PUSHER_APP_KEY);
        Channel channel = pusher.subscribe(CHANNEL_NAME);

        SubscriptionEventListener eventListener = new SubscriptionEventListener() {
            @Override
            public void onEvent(String channel, final String event, final String data) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        System.out.println("Received event with data: " + data);
                        Gson gson = new Gson();
                        Stat stat = gson.fromJson(data, Stat.class);
                        addEntry(stat);
                    }
                });
            }
        };

        channel.bind(EVENT_NAME, eventListener);
        pusher.connect();

    }

}

Let’s review all the methods defined above. First, setupChart() configures some general options of the chart:

public class MainActivity extends AppCompatActivity {

    ...

    private void setupChart() {
        // disable description text
        mChart.getDescription().setEnabled(false);
        // enable touch gestures
        mChart.setTouchEnabled(true);
        // if disabled, scaling can be done on x- and y-axis separately
        mChart.setPinchZoom(true);
        // enable scaling
        mChart.setScaleEnabled(true);
        mChart.setDrawGridBackground(false);
        // set an alternative background color
        mChart.setBackgroundColor(Color.DKGRAY);
    }

}

The setupAxes() method configures the options of the X and Y axes and adds the limit line we talked about before:

public class MainActivity extends AppCompatActivity {

    ...

    private void setupAxes() {
        XAxis xl = mChart.getXAxis();
        xl.setTextColor(Color.WHITE);
        xl.setDrawGridLines(false);
        xl.setAvoidFirstLastClipping(true);
        xl.setEnabled(true);

        YAxis leftAxis = mChart.getAxisLeft();
        leftAxis.setTextColor(Color.WHITE);
        leftAxis.setAxisMaximum(TOTAL_MEMORY);
        leftAxis.setAxisMinimum(0f);
        leftAxis.setDrawGridLines(true);

        YAxis rightAxis = mChart.getAxisRight();
        rightAxis.setEnabled(false);

        // Add a limit line
        LimitLine ll = new LimitLine(LIMIT_MAX_MEMORY, "Upper Limit");
        ll.setLineWidth(2f);
        ll.setLabelPosition(LimitLine.LimitLabelPosition.RIGHT_TOP);
        ll.setTextSize(10f);
        ll.setTextColor(Color.WHITE);
        // reset all limit lines to avoid overlapping lines
        leftAxis.removeAllLimitLines();
        leftAxis.addLimitLine(ll);
        // limit lines are drawn behind data (and not on top)
        leftAxis.setDrawLimitLinesBehindData(true);
    }

}

The setupData() method just adds an empty LineData object:

public class MainActivity extends AppCompatActivity {

    ...

    private void setupData() {
        LineData data = new LineData();
        data.setValueTextColor(Color.WHITE);

        // add empty data
        mChart.setData(data);
    }

}

The setLegend() method sets the options of the legend for the data set that will be shown below the chart:

public class MainActivity extends AppCompatActivity {

    ...

    private void setLegend() {
        // get the legend (only possible after setting data)
        Legend l = mChart.getLegend();

        // modify the legend ...
        l.setForm(Legend.LegendForm.CIRCLE);
        l.setTextColor(Color.WHITE);
    }

}

In turn, createSet() will create the data set for the memory data configuring some options for its presentation:

public class MainActivity extends AppCompatActivity {

    ...

    private LineDataSet createSet() {
        LineDataSet set = new LineDataSet(null, "Memory Data");
        set.setAxisDependency(YAxis.AxisDependency.LEFT);
        set.setColors(ColorTemplate.VORDIPLOM_COLORS[0]);
        set.setCircleColor(Color.WHITE);
        set.setLineWidth(2f);
        set.setCircleRadius(4f);
        set.setValueTextColor(Color.WHITE);
        set.setValueTextSize(10f);
        // To show values of each point
        set.setDrawValues(true);

        return set;
    }

}

The addEntry(stat) method, the one used when an event arrives, will create a data set if none exists using the above method, add the entry from the Stat instance that is passed as argument, notify the data has changed, and set the options to limit the view to 15 visible entries (to avoid the chart looking crowded):

public class MainActivity extends AppCompatActivity {

    ...

    private void addEntry(Stat stat) {
        LineData data = mChart.getData();

        if (data != null) {
            ILineDataSet set = data.getDataSetByIndex(0);

            if (set == null) {
                set = createSet();
                data.addDataSet(set);
            }

            data.addEntry(new Entry(set.getEntryCount(), stat.getMemory()), 0);

            // let the chart know it's data has changed
            data.notifyDataChanged();
            mChart.notifyDataSetChanged();

            // limit the number of visible entries
            mChart.setVisibleXRangeMaximum(15);

            // move to the latest entry
            mChart.moveViewToX(data.getEntryCount());
        }
    }

}

And finally, we override the method onDestroy() to disconnect from Pusher when needed:

public class MainActivity extends AppCompatActivity {

    ...

    @Override
    public void onDestroy() {
        super.onDestroy();
        pusher.disconnect();
    }

}

And we’re done, let’s test it.

Testing the app

Execute the app, either on a real device or a virtual one:

The following screen will show up:

Make sure the Node.js is running. When new data about the memory is received, it will show up in the graph:

Conclusion

Remember that you can find the final version of the Android app here and the Node.js process here.

Hopefully, this tutorial has shown you how simple it is to build a realtime graph in Android with Pusher and MPAndroidChart. You can improve the app by changing the design or type of graphic (a pie chart will work great to see the used vs the free memory), or show more information.

Remember that your free Pusher account includes 100 connections, unlimited channels, 200k daily messages, SSL protection, and there are more features than just Pub/Sub Messaging. Sign up here.