Tracking server statistics can be extremely handy and I just haven't come across many great options for game servers. There are free options like GameTracker but their tracker only queries once every several minutes (and in some cases I have seen it take 15 minutes or more to query).

That is why I decided to try building something self-hosted and fully configurable to get this done. Node-RED lets you build flows that can basically do whatever you want so it was perfect for this project. I ended up creating a new package node-red-contrib-gamedig that anyone can easily install from the Palette Manager in Node-RED. This package adds the node query-game-server to the palette that you can then use to build flows to do whatever you want.

We will be using this new node to query game servers and then store the data in InfluxDB. Grafana will then be used to display the data. So lets get started!

Grafana Preview

If you want to know what the end result looks like in Grafana here it is:

Click here to view what the Grafana template looks like in use before you import it.

Requirements

The Node-RED Flow

The flow is actually pretty simple. It's just an inject node that fires every 20 seconds to query the server which then sends the data through the function block to format it into fields and tags for InfluxDB:

[{"id":"67177f4c.2cf43","type":"cron","z":"5ef5d69c.ad1238","name":"Every 20 seconds","crontab":"*/20 * * * * *","x":970,"y":700,"wires":[["f33dcf1d.901c1"]]},{"id":"f33dcf1d.901c1","type":"query-game-server","z":"5ef5d69c.ad1238","name":"","server_type":"hldm","host":"home.skylar.tech","port":"27018","halt_if":"","x":1190,"y":700,"wires":[["1bb131ca.19a91e"]]},{"id":"398f755c.118eca","type":"influxdb out","z":"5ef5d69c.ad1238","influxdb":"136b4827.f23048","name":"","measurement":"server_query","precision":"","retentionPolicy":"","x":1670,"y":700,"wires":[]},{"id":"c6a49618.65ed78","type":"comment","z":"5ef5d69c.ad1238","name":"Query server and store in influxDB","info":"","x":1360,"y":660,"wires":[]},{"id":"1bb131ca.19a91e","type":"function","z":"5ef5d69c.ad1238","name":"Fields and Tags","func":"if(msg.payload == 'online') {\n    msg.payload = [{\n        online: 1,\n        map: msg.data.map,\n        num_players: msg.data.players.length,\n        max_players: msg.data.maxplayers,\n        ping: msg.data.ping\n    },\n    {\n        game: msg.server_type,\n        server_name: msg.data.name,\n        host: msg.host + \":\" + msg.port\n    }];\n} else {\n    msg.payload = [{\n        online: 0\n    },\n    {\n        game: msg.server_type,\n        host: msg.host + \":\" + msg.port\n    }];\n}\ndelete msg.gamedig;\ndelete msg.data;\nreturn msg;","outputs":1,"noerr":0,"x":1400,"y":700,"wires":[["398f755c.118eca"]]},{"id":"136b4827.f23048","type":"influxdb","z":"","hostname":"192.168.1.10","port":"8086","protocol":"http","database":"gameserver","name":"","usetls":false,"tls":""}]

Import this flow and make sure to change the InfluxDB node to point to your database and change the server node to whatever server you want to query. Now we can get to importing our Grafana dashboard..

Setting up Grafana dashboard

{"annotations":{"list":[{"builtIn":1,"datasource":"-- Grafana --","enable":true,"hide":true,"iconColor":"rgba(0, 211, 255, 1)","name":"Annotations & Alerts","type":"dashboard"}]},"editable":true,"gnetId":null,"graphTooltip":1,"id":23,"iteration":1575533013236,"links":[],"panels":[{"aliasColors":{},"bars":false,"dashLength":10,"dashes":false,"datasource":"InfluxDB gameserver","fill":1,"fillGradient":0,"gridPos":{"h":6,"w":21,"x":0,"y":0},"hiddenSeries":false,"id":2,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":false},"lines":true,"linewidth":1,"nullPointMode":"connected","options":{"dataLinks":[]},"percentage":false,"pointradius":2,"points":false,"renderer":"flot","seriesOverrides":[],"spaceLength":10,"stack":false,"steppedLine":false,"targets":[{"alias":"Players","groupBy":[{"params":["$__interval"],"type":"time"},{"params":["null"],"type":"fill"}],"measurement":"server_query","orderByTime":"ASC","policy":"default","refId":"A","resultFormat":"time_series","select":[[{"params":["num_players"],"type":"field"},{"params":[],"type":"max"}]],"tags":[{"key":"host","operator":"=~","value":"/^$Servers$/"}]},{"alias":"Max Players","groupBy":[{"params":["$__interval"],"type":"time"},{"params":["null"],"type":"fill"}],"measurement":"server_query","orderByTime":"ASC","policy":"default","refId":"B","resultFormat":"time_series","select":[[{"params":["max_players"],"type":"field"},{"params":[],"type":"max"}]],"tags":[{"key":"host","operator":"=~","value":"/^$Servers$/"}]}],"thresholds":[],"timeFrom":null,"timeRegions":[],"timeShift":null,"title":"Players","tooltip":{"shared":true,"sort":0,"value_type":"individual"},"type":"graph","xaxis":{"buckets":null,"mode":"time","name":null,"show":true,"values":[]},"yaxes":[{"decimals":null,"format":"short","label":null,"logBase":1,"max":null,"min":"0","show":true},{"decimals":null,"format":"short","label":null,"logBase":1,"max":null,"min":"0","show":true}],"yaxis":{"align":false,"alignLevel":null}},{"cacheTimeout":null,"colorBackground":true,"colorPostfix":false,"colorPrefix":false,"colorValue":false,"colors":["#C4162A","#C4162A","#299c46"],"datasource":null,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"gridPos":{"h":6,"w":3,"x":21,"y":0},"id":15,"interval":null,"links":[],"mappingType":1,"mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"options":{},"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","rangeMaps":[{"from":"null","text":"N/A","to":"null"}],"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":false,"lineColor":"rgb(31, 120, 193)","show":false,"ymax":null,"ymin":null},"tableColumn":"","targets":[{"groupBy":[],"measurement":"server_query","orderByTime":"ASC","policy":"default","refId":"A","resultFormat":"time_series","select":[[{"params":["online"],"type":"field"},{"params":[],"type":"last"}]],"tags":[{"key":"host","operator":"=~","value":"/^$Servers$/"}]}],"thresholds":"0,1","timeFrom":null,"timeShift":null,"title":"Latest Status (for range)","type":"singlestat","valueFontSize":"80%","valueMaps":[{"op":"=","text":"Online","value":"1"},{"op":"=","text":"Offline","value":"0"}],"valueName":"max"},{"alert":{"alertRuleTags":{},"conditions":[{"evaluator":{"params":[1],"type":"lt"},"operator":{"type":"and"},"query":{"params":["A","1m","now"]},"reducer":{"params":[],"type":"sum"},"type":"query"}],"executionErrorState":"alerting","for":"1m","frequency":"20s","handler":1,"name":"win.taco-land.net:27025 Offline Alert","noDataState":"alerting","notifications":[{"uid":"rqtBLZtWz"}]},"aliasColors":{},"bars":false,"dashLength":10,"dashes":false,"datasource":"InfluxDB gameserver","fill":1,"fillGradient":0,"gridPos":{"h":6,"w":21,"x":0,"y":6},"hiddenSeries":false,"id":24,"legend":{"avg":false,"current":false,"max":false,"min":false,"show":true,"total":false,"values":false},"lines":true,"linewidth":1,"nullPointMode":"connected","options":{"dataLinks":[]},"percentage":false,"pointradius":2,"points":false,"renderer":"flot","seriesOverrides":[],"spaceLength":10,"stack":false,"steppedLine":false,"targets":[{"alias":"Ping","groupBy":[{"params":["$__interval"],"type":"time"},{"params":["0"],"type":"fill"}],"hide":false,"measurement":"server_query","orderByTime":"ASC","policy":"default","query":"SELECT max(\"ping\") FROM \"server_query\" WHERE (\"host\" = 'win.taco-land.net:27025') AND $timeFilter GROUP BY time($__interval) fill(previous)","rawQuery":false,"refId":"D","resultFormat":"time_series","select":[[{"params":["ping"],"type":"field"},{"params":[],"type":"max"}]],"tags":[{"key":"host","operator":"=","value":"win.taco-land.net:27025"}]},{"alias":"Online","groupBy":[{"params":["$__interval"],"type":"time"},{"params":["null"],"type":"fill"}],"orderByTime":"ASC","policy":"default","query":"SELECT max(\"online\") FROM \"server_query\" WHERE (\"host\" = 'win.taco-land.net:27025') AND $timeFilter GROUP BY time($__interval) fill(null)","rawQuery":true,"refId":"A","resultFormat":"time_series","select":[[{"params":["value"],"type":"field"},{"params":[],"type":"mean"}]],"tags":[]}],"thresholds":[{"colorMode":"critical","fill":true,"line":true,"op":"lt","value":1}],"timeFrom":null,"timeRegions":[],"timeShift":null,"title":"Ping/Online Status","tooltip":{"shared":true,"sort":0,"value_type":"individual"},"type":"graph","xaxis":{"buckets":null,"mode":"time","name":null,"show":true,"values":[]},"yaxes":[{"decimals":null,"format":"short","label":null,"logBase":1,"max":null,"min":"0","show":true},{"decimals":null,"format":"short","label":null,"logBase":1,"max":null,"min":"0","show":true}],"yaxis":{"align":false,"alignLevel":null}},{"cacheTimeout":null,"colorBackground":false,"colorValue":false,"colors":["#299c46","rgba(237, 129, 40, 0.89)","#d44a3a"],"datasource":null,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"gridPos":{"h":3,"w":3,"x":21,"y":6},"id":13,"interval":null,"links":[],"mappingType":1,"mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"options":{},"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","rangeMaps":[{"from":"null","text":"N/A","to":"null"}],"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":false,"lineColor":"rgb(31, 120, 193)","show":false,"ymax":null,"ymin":null},"tableColumn":"","targets":[{"groupBy":[{"params":["$__interval"],"type":"time"},{"params":["null"],"type":"fill"}],"measurement":"server_query","orderByTime":"ASC","policy":"default","refId":"A","resultFormat":"time_series","select":[[{"params":["num_players"],"type":"field"},{"params":[],"type":"mean"}]],"tags":[{"key":"host","operator":"=~","value":"/^$Servers$/"}]}],"thresholds":"","timeFrom":null,"timeShift":null,"title":"Player Avg (for range)","type":"singlestat","valueFontSize":"80%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg"},{"cacheTimeout":null,"colorBackground":false,"colorValue":false,"colors":["#299c46","rgba(237, 129, 40, 0.89)","#d44a3a"],"datasource":null,"format":"none","gauge":{"maxValue":100,"minValue":0,"show":false,"thresholdLabels":false,"thresholdMarkers":true},"gridPos":{"h":3,"w":3,"x":21,"y":9},"id":29,"interval":null,"links":[],"mappingType":1,"mappingTypes":[{"name":"value to text","value":1},{"name":"range to text","value":2}],"maxDataPoints":100,"nullPointMode":"connected","nullText":null,"options":{},"postfix":"","postfixFontSize":"50%","prefix":"","prefixFontSize":"50%","rangeMaps":[{"from":"null","text":"N/A","to":"null"}],"sparkline":{"fillColor":"rgba(31, 118, 189, 0.18)","full":false,"lineColor":"rgb(31, 120, 193)","show":false,"ymax":null,"ymin":null},"tableColumn":"","targets":[{"groupBy":[{"params":["$__interval"],"type":"time"},{"params":["null"],"type":"fill"}],"measurement":"server_query","orderByTime":"ASC","policy":"default","refId":"A","resultFormat":"time_series","select":[[{"params":["ping"],"type":"field"},{"params":[],"type":"mean"}]],"tags":[{"key":"host","operator":"=~","value":"/^$Servers$/"}]}],"thresholds":"","timeFrom":null,"timeShift":null,"title":"Ping Avg (for range)","type":"singlestat","valueFontSize":"80%","valueMaps":[{"op":"=","text":"N/A","value":"null"}],"valueName":"avg"},{"columns":[],"datasource":null,"fontSize":"100%","gridPos":{"h":13,"w":24,"x":0,"y":12},"id":8,"options":{},"pageSize":null,"repeat":null,"scroll":true,"showHeader":true,"sort":{"col":0,"desc":true},"styles":[{"alias":"Time","dateFormat":"YYYY-MM-DD HH:mm:ss","pattern":"Time","type":"date"},{"alias":"Map","colorMode":null,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"dateFormat":"YYYY-MM-DD HH:mm:ss","decimals":2,"link":false,"mappingType":1,"pattern":"map","thresholds":[],"type":"string","unit":"short","valueMaps":[]},{"alias":"","colorMode":null,"colors":["rgba(245, 54, 54, 0.9)","rgba(237, 129, 40, 0.89)","rgba(50, 172, 45, 0.97)"],"decimals":2,"pattern":"/.*/","thresholds":[],"type":"number","unit":"short"}],"targets":[{"groupBy":[],"measurement":"server_query","orderByTime":"ASC","policy":"default","refId":"A","resultFormat":"table","select":[[{"params":["map"],"type":"field"}]],"tags":[{"key":"host","operator":"=~","value":"/^$Servers$/"}]}],"timeFrom":null,"timeShift":null,"title":"Maps","transform":"table","type":"table"}],"refresh":false,"schemaVersion":21,"style":"dark","tags":[],"templating":{"list":[{"allValue":null,"current":{"selected":false,"text":"home.skylar.tech:27015","value":"home.skylar.tech:27015"},"datasource":"InfluxDB gameserver","definition":"SHOW TAG VALUES WITH KEY = \"host\"","hide":0,"includeAll":false,"label":"Server Host","multi":false,"name":"Servers","options":[],"query":"SHOW TAG VALUES WITH KEY = \"host\"","refresh":2,"regex":"","skipUrlSync":false,"sort":0,"tagValuesQuery":"","tags":[],"tagsQuery":"","type":"query","useTags":false}]},"time":{"from":"now-6h","to":"now"},"timepicker":{"refresh_intervals":["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"]},"timezone":"","title":"Server Query","uid":"W9k3PcWLTzg1","version":3}

Import the above JSON into Grafana and point it to your InfluxDB datasource and you should be able to see the servers you are tracking show up in the Servers dropdown in the top-left just like this:

And now you are done! If for some reason servers aren't showing up you will need to check your flow in Node-RED to make sure it's working (and debug it using the Debug node to see if it's even passing data to InfluxDB). If you need any help at all just drop me a comment below and I will assist you the best I can.

Conclusion

Node-RED works great for this task and I actually track a lot of data using it already (it runs my entire Home Automation setup) so this was perfect. I was a little surprised no one had created a package like this already for Node-RED but at the same time I am glad because it gave me a chance to create my first Node-RED contribution.

I have another post about using Node-RED to restart crashed game servers. Containers have built in auto restart functionality that you can use but if the server gets frozen/over-loaded it wont actually fully crash and will be unavailable to query. I have this problem with custom maps on my Sven co-op server and this has fixed problems with the server getting stuck.

I hope others find this useful and if anyone makes changes to what I have here I would love to hear about them. Feel free to leave any feedback below and I will get back to you. I love hearing from my readers.