diff --git a/sharedPlugins/statisticsExporter/Lua/exportStatistics.lua b/sharedPlugins/statisticsExporter/Lua/exportStatistics.lua new file mode 100644 index 00000000..c50196b5 --- /dev/null +++ b/sharedPlugins/statisticsExporter/Lua/exportStatistics.lua @@ -0,0 +1,19 @@ +local statisticsData = "[" +for _, force in pairs(game.forces) do + statisticsData = statisticsData .. '{"forceName":"'..force.name..'","data":{' + local statistics = {"item_production_statistics", "fluid_production_statistics", "kill_count_statistics", "entity_build_count_statistics"} + for key, stat in pairs(statistics) do + statisticsData = statisticsData..'"'..stat..'":{"input":{' + for name, value in pairs(force[stat].input_counts) do + statisticsData = statisticsData..'"'..name..'":'..value..',' + end + statisticsData = statisticsData..'},"output":{' + for name, value in pairs(force[stat].output_counts) do + statisticsData = statisticsData..'"'..name..'":'..value..',' + end + statisticsData = statisticsData .. '}},' + end + statisticsData = statisticsData..'}},' +end +statisticsData = statisticsData..']' +rcon.print(statisticsData) diff --git a/sharedPlugins/statisticsExporter/config.js b/sharedPlugins/statisticsExporter/config.js new file mode 100644 index 00000000..bfa220de --- /dev/null +++ b/sharedPlugins/statisticsExporter/config.js @@ -0,0 +1,10 @@ +/* + Clusterio plugin for gathering ingame statistics +*/ +module.exports = { + name: "statisticsExporter", + version: "1.0.0", + binary: "nodePackage", + description: "Gathers ingame stats such as production, consumption, kills and buildings placed", + masterPlugin: "masterPlugin.js" +} diff --git a/sharedPlugins/statisticsExporter/index.js b/sharedPlugins/statisticsExporter/index.js new file mode 100644 index 00000000..bc2579fb --- /dev/null +++ b/sharedPlugins/statisticsExporter/index.js @@ -0,0 +1,40 @@ +const needle = require("needle"); +const fs = require("fs-extra"); +const path = require("path"); + +const clusterTools = require("_app/clusterTools")(); +const pluginConfig = require("./config"); + +module.exports = class remoteCommands { + constructor(mergedConfig, messageInterface, extras){ + this.messageInterface = messageInterface; + this.config = mergedConfig; + + const needleOptionsWithTokenAuthHeader = { + headers: { + 'x-access-token': this.config.masterAuthToken + }, + }; + + // Gather and submit statistics every 15 seconds + setInterval(async () => { + let stats = await this.getStats(); + stats.forEach(force => { + needle.post(`${mergedConfig.masterIP}:${mergedConfig.masterPort}/api/logStats`, { + instanceID: this.config.unique, + force: force.forceName, + data: force.data, + }, needleOptionsWithTokenAuthHeader, (err, resp) => { + if(!err && resp.body && resp.body.ok == false) console.log(resp.body); + }); + }); + }, 15000); + } + async getStats(){ + let command = fs.readFileSync(path.join(__dirname, "Lua/exportStatistics.lua"), "utf8")//clusterTools.getLua(path.join(__dirname, "Lua/exportStatistics.lua"), false); + let string = await this.messageInterface(`/silent-command ${command}`); + var data; + eval(`data = ${string}`); + return data; + } +} diff --git a/sharedPlugins/statisticsExporter/masterPlugin.js b/sharedPlugins/statisticsExporter/masterPlugin.js new file mode 100644 index 00000000..89b1a97b --- /dev/null +++ b/sharedPlugins/statisticsExporter/masterPlugin.js @@ -0,0 +1,58 @@ +const fs = require("fs-extra"); +const path = require("path"); + +class masterPlugin { + constructor({config, socketio, express, Prometheus}){ + const authenticate = require("./../../lib/authenticate")(config); + + this.config = config; + this.io = socketio; + const app = express; + + const prometheusStatisticsGauge = new Prometheus.Gauge({ + name: 'clusterio_statistics_gauge', + help: 'Items produced/built/killed by a force', + labelNames: ["instanceID", "force", "statistic", "direction", "itemName"], + }); + + /** --- TODO: Replace this with a simpler Prometheus exporter. Requires changes to the mod as well. + POST endpoint to log production graph statistics. Should contain an instanceID (also reffered to as "unique") + and of course the timeSeries data organized by category. + + @memberof clusterioMaster + @instance + @alias api/logStats + @param {object} JSON {instanceID: "string", force: "string" data: {"item_production_statistics": {"input":{"iron-plate":150}, "output":{}}, ...}} + @returns {string} failure + */ + app.post("/api/logStats", authenticate.middleware, function(req,res) { + // endpointHitCounter.labels(req.route.path).inc(); + if(typeof req.body == "object" + && req.body.instanceID + && typeof req.body.instanceID == "string" + && req.body.instanceID.length < 100 + && req.body.data + && typeof req.body.data == "object" + && !Array.isArray(req.body.data)){ + try{ + Object.keys(req.body.data).forEach(statisticName => { + Object.keys(req.body.data[statisticName]).forEach(direction => { + let statHash = req.body.data[statisticName][direction]; + Object.keys(statHash).forEach(item => { + let value = Number(statHash[item]); + if(value) prometheusStatisticsGauge.labels(req.body.instanceID, req.body.force, statisticName, direction, item).set(value); + }); + }); + }); + res.send({ok:true, msg:`Statistics successfully reported at ${Date.now()}`}); + }catch(e){ + res.send({ok:false, msg:"Statistics reporter crashed, see sharedPlugins/statisticsExporter/masterPlugin.js"}); + console.log(e) + }; + } else { + res.send("failure"); + } + }); + } +} +module.exports = masterPlugin;