Monitoring CPU metrics on Grafana using Node-red

Grafana 8.2.2 seems does not support running a shell script natively. I would recommend parsing the CPU status via an external REST API. Node-red will be used in the following example.

In this article, you will learn:

  1. How to monitor cpu status on Node-red
  2. How to create a REST api on Node-red
  3. How to parse the REST data using Grafana Infinity plug-in

Monitoring CPU status

We will be using exec node to run shell commands. In addition, we can add buttons to shutdown and reboot as well.

Notice that the script for running CPU usage is a bit complicated: top -d 0.5 -b -n3| grep "Cpu(s)"|tail -n 1| awk '{print 100-$8}'as well as ram free | grep Mem | awk '{printf "%.2f", ($2-$7)*100/$2}'. The final number is calculated in percentage, where 100 is overloaded. You may play around with the parameters, I will make a post to briefly introduce shell manipulation in the future.

[{"id":"2653a0b0.26d638","type":"ui_gauge","z":"1a2e114b.5f398f","name":"","group":"1890881e.83819","order":2,"width":0,"height":0,"gtype":"gage","title":"CPU Temperature","label":"C","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":1090,"y":120,"wires":[]},{"id":"fba68adf.14e13","type":"exec","z":"1a2e114b.5f398f","command":"vcgencmd measure_temp","addpay":false,"append":"","useSpawn":"","timer":"","name":"raspberry pi temperature","x":630,"y":160,"wires":[["fa5b499.e176cb8"],[],[]]},{"id":"7c8379de.068868","type":"inject","z":"1a2e114b.5f398f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"2","crontab":"","once":false,"onceDelay":"","topic":"","payload":"","payloadType":"date","x":310,"y":200,"wires":[["fba68adf.14e13","972ece2a.3dbe8","6242be99.26ac88"]]},{"id":"fa5b499.e176cb8","type":"function","z":"1a2e114b.5f398f","name":"trim string","func":"str = msg.payload\nmsg.payload = str.substring(5,9);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":860,"y":120,"wires":[["2653a0b0.26d638","28b64a2c.b32116","7d734a89.e1e164"]]},{"id":"683a2b05.736204","type":"ui_button","z":"1a2e114b.5f398f","name":"","group":"c5f1b8aa.45bc08","order":2,"width":0,"height":0,"label":"Reboot","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"","x":320,"y":580,"wires":[["1cf31554.2aaa63"]]},{"id":"1cf31554.2aaa63","type":"exec","z":"1a2e114b.5f398f","command":"sudo reboot","addpay":false,"append":"","useSpawn":"","timer":"","name":"sudo reboot","x":590,"y":580,"wires":[[],[],[]]},{"id":"38e53dbf.ca594a","type":"ui_button","z":"1a2e114b.5f398f","name":"","group":"c5f1b8aa.45bc08","order":3,"width":0,"height":0,"label":"Shutdown","color":"","bgcolor":"red","icon":"","payload":"","payloadType":"str","topic":"","x":320,"y":640,"wires":[["c409cae5.8e4128"]]},{"id":"c409cae5.8e4128","type":"exec","z":"1a2e114b.5f398f","command":"sudo shutdown 0","addpay":false,"append":"","useSpawn":"","timer":"","name":"sudo shutdown -h now","x":620,"y":640,"wires":[[],[],[]]},{"id":"28b64a2c.b32116","type":"ui_chart","z":"1a2e114b.5f398f","name":"","group":"1890881e.83819","order":3,"width":0,"height":0,"label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"12","removeOlderPoints":"1000","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":1070,"y":160,"wires":[[]]},{"id":"972ece2a.3dbe8","type":"exec","z":"1a2e114b.5f398f","command":"top -d 0.5 -b -n3| grep \"Cpu(s)\"|tail -n 1| awk '{print 100-$8}'","addpay":false,"append":"","useSpawn":"","timer":"","name":"cpu usage","x":590,"y":240,"wires":[["b9372186.ed1a5","8144e676.4d5d68"],[],[]]},{"id":"6242be99.26ac88","type":"exec","z":"1a2e114b.5f398f","command":"free | grep Mem | awk '{printf \"%.2f\", ($2-$7)*100/$2}'","addpay":false,"append":"","useSpawn":"","timer":"","name":"ram usage","x":590,"y":320,"wires":[["d8ede4a4.507998","9b301b09.8c0468"],[],[]]},{"id":"b9372186.ed1a5","type":"ui_gauge","z":"1a2e114b.5f398f","name":"","group":"1890881e.83819","order":1,"width":0,"height":0,"gtype":"gage","title":"Processor","label":"CPU","format":"{{value}}","min":0,"max":"102","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":1080,"y":200,"wires":[]},{"id":"9b301b09.8c0468","type":"ui_gauge","z":"1a2e114b.5f398f","name":"","group":"9a96a8b1.92db78","order":1,"width":0,"height":0,"gtype":"gage","title":"Memory","label":"RAM","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":1060,"y":300,"wires":[]},{"id":"d8ede4a4.507998","type":"debug","z":"1a2e114b.5f398f","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1070,"y":340,"wires":[]},{"id":"8144e676.4d5d68","type":"debug","z":"1a2e114b.5f398f","name":"","active":true,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1090,"y":240,"wires":[]},{"id":"57aad7a5.391708","type":"inject","z":"1a2e114b.5f398f","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":false,"onceDelay":"","topic":"","payload":"","payloadType":"date","x":310,"y":460,"wires":[["8c19e802.e92e88"]]},{"id":"8c19e802.e92e88","type":"exec","z":"1a2e114b.5f398f","command":"df -h / | awk '{print $5}'","addpay":false,"append":"","useSpawn":"","timer":"","name":"disk storage usage","x":610,"y":460,"wires":[["9869bc88.1d73f"],[],[]]},{"id":"9869bc88.1d73f","type":"function","z":"1a2e114b.5f398f","name":"trim string","func":"var test = msg.payload.substr(5);  //trim off Use%\nmsg.payload = test.substring(0, test.length - 1) //trim off the next line string\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":900,"y":460,"wires":[["1ba34040.c7d4","9c3b4510.f9fbc8"]]},{"id":"9c3b4510.f9fbc8","type":"debug","z":"1a2e114b.5f398f","name":"","active":false,"tosidebar":false,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":1130,"y":480,"wires":[]},{"id":"1ba34040.c7d4","type":"ui_gauge","z":"1a2e114b.5f398f","name":"","group":"72fc319.cc425d","order":1,"width":0,"height":0,"gtype":"gage","title":"Disk","label":"Usage","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"x":1110,"y":440,"wires":[]},{"id":"7d734a89.e1e164","type":"debug","z":"1a2e114b.5f398f","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1050,"y":80,"wires":[]},{"id":"1890881e.83819","type":"ui_group","name":"Col1","tab":"c3173234.2636e","order":1,"disp":false,"width":"6"},{"id":"c5f1b8aa.45bc08","type":"ui_group","name":"Actions","tab":"c3173234.2636e","order":4,"disp":true,"width":"6"},{"id":"9a96a8b1.92db78","type":"ui_group","name":"Col2","tab":"c3173234.2636e","order":2,"disp":false,"width":"6"},{"id":"72fc319.cc425d","type":"ui_group","name":"Col3","tab":"c3173234.2636e","order":3,"disp":false,"width":"6"},{"id":"c3173234.2636e","type":"ui_tab","name":"RPi Control","icon":"dashboard","order":1}]

Creating REST api on Node-red

We will use the http in and http response node to set up the REST API. Please note that you cannot initiate an HTTP response by itself. You should rather trigger the flow by going into the URL, or by an HTTP request node or by Grafana in this case. The http in node (Method: GET) will initiate the flow once the URL has been requested. Here is an example of setting up a simple payload as a content of the URL /cpumetrics. Open the browser and navigate to http://localhost:1880/cpumetrics.

[{"id":"c01ca5bf.a938b8","type":"http in","z":"9566abd8.7dc4d8","name":"","url":"/cpumetrics","method":"get","upload":false,"swaggerDoc":"","x":140,"y":140,"wires":[["1b98b2af.7af11d"]]},{"id":"8ccf75b3.ddd338","type":"comment","z":"9566abd8.7dc4d8","name":"REST endpoint","info":"","x":140,"y":80,"wires":[]},{"id":"1b98b2af.7af11d","type":"function","z":"9566abd8.7dc4d8","name":"simple payload","func":"msg.payload = {\"message\": \"this is the payload\"};\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":140,"wires":[["26f72364.1a687c"]]},{"id":"26f72364.1a687c","type":"http response","z":"9566abd8.7dc4d8","name":"","statusCode":"","headers":{},"x":510,"y":140,"wires":[]}]

REST api with query parameters

Sometimes you want your reply to be changed dynamically based on different parameters. You could use an if-else statement on the function to give a specific reply to different parameters. After deploying the flow, navigate to the URL: http://localhost:1880/cpumetrics?metrics=cpu. If the metric parameter is equal to “cpu”, the page will reply a {“message”: “cpu loading”} otherwise, {“message”: “some other metrics”} will be replied.

[{"id":"59ff2a1.fa600d4","type":"http in","z":"1adc4b7f.bb9655","name":"","url":"/cpumetrics","method":"get","upload":false,"swaggerDoc":"","x":240,"y":200,"wires":[["a3d72dd3.05bc2","2ec4bd64.072302","e6e4ab4c.254ea8","8a2f5529.8baa88","54c1e70d.ab3e18","c4d1aa9b.1bb7c8"]]},{"id":"4eefdf83.b473c","type":"comment","z":"1adc4b7f.bb9655","name":"REST endpoint","info":"","x":240,"y":140,"wires":[]},{"id":"266c286f.d993d8","type":"http response","z":"1adc4b7f.bb9655","name":"","statusCode":"","headers":{},"x":1030,"y":220,"wires":[]},{"id":"a3d72dd3.05bc2","type":"function","z":"1adc4b7f.bb9655","name":"request with Query Parameters","func":"if (msg.payload.metrics === \"cpu\"){\n    msg.payload = {\"message\": \"cpu loading\"};\n}else{\n    msg.payload = {\"message\": \"some other metrics\"};\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":690,"y":100,"wires":[["266c286f.d993d8","2ec4bd64.072302"]]}]

REST api with cpu metrics

In the last example, we will put the real-time cpu metrics as content. A join node is used to put together the disk space and ram space into a single payload. Besides, we will use a http request node to invoke the endpoint. The payload could be checked on the debug window.

[{"id":"4512cdf1.3cd794","type":"exec","z":"608a04eb.6011bc","command":"free | grep Mem | awk '{printf \"%.2f\", ($2-$7)*100/$2}'","addpay":"","append":"","useSpawn":"false","timer":"","oldrc":false,"name":"RAM","x":490,"y":220,"wires":[["c5b1a07e.aa5fb"],[],[]]},{"id":"1ade78d7.8383f7","type":"http in","z":"608a04eb.6011bc","name":"","url":"/cpumetrics","method":"get","upload":false,"swaggerDoc":"","x":120,"y":160,"wires":[["334a3882.c0e9b8","696e606d.dc86f"]]},{"id":"696e606d.dc86f","type":"function","z":"608a04eb.6011bc","name":"append topic","func":"msg.topic = \"ram\";\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":350,"y":220,"wires":[["4512cdf1.3cd794"]]},{"id":"334a3882.c0e9b8","type":"exec","z":"608a04eb.6011bc","command":"df -h / | awk '{print $5}'","addpay":"","append":"","useSpawn":"false","timer":"","oldrc":false,"name":"disk space","x":350,"y":160,"wires":[["c9c3ed28.056db"],[],[]]},{"id":"c9c3ed28.056db","type":"function","z":"608a04eb.6011bc","name":"trim off string","func":"var test = msg.payload.substr(5);  //trim off Use%\nmsg.payload = test.substring(0, test.length - 2) //trim off the next line string\nmsg.topic = \"storage\";\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":160,"wires":[["c5b1a07e.aa5fb"]]},{"id":"c5b1a07e.aa5fb","type":"join","z":"608a04eb.6011bc","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":false,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":710,"y":180,"wires":[["a00ebb59.59d158","2b0fcd92.4fd282"]]},{"id":"a00ebb59.59d158","type":"http response","z":"608a04eb.6011bc","name":"","statusCode":"","headers":{},"x":870,"y":160,"wires":[]},{"id":"4899725b.041d9c","type":"comment","z":"608a04eb.6011bc","name":"REST endpoint","info":"","x":120,"y":100,"wires":[]},{"id":"5f21f1c0.0f39c","type":"http request","z":"608a04eb.6011bc","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"localhost:1880/cpumetrics","tls":"","persist":false,"proxy":"","authType":"","x":290,"y":320,"wires":[[]]},{"id":"7904b22.74ae64c","type":"inject","z":"608a04eb.6011bc","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":120,"y":320,"wires":[["5f21f1c0.0f39c"]]},{"id":"2fc5dcef.b8ce74","type":"comment","z":"608a04eb.6011bc","name":"Invoke REST endpoint","info":"","x":160,"y":280,"wires":[]},{"id":"2b0fcd92.4fd282","type":"debug","z":"608a04eb.6011bc","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":890,"y":200,"wires":[]}]

Setting up Grafana plug-in

After preparing the data for display, we could set up the Grafana dashboard to display those data. Go to Configurations -> plugins, search for Infinity and install it.

Then go to Configurations -> Data sources -> Add data source.

The current version (Version 0.7.8) of Infinity does not require connectivity testing in this setup stage, so leave all fields blank and click Save & test.

Head over to your dashboard and add a new panel. Select a Stat graph, and select Infinity as the Data source. Put http://localhost:1880/cpumetrics in the URL field. Click Add Colums and put storage and ram as the selector, Number as type. You should now see the data shown in the preview panel.