mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 23:04:52 +09:00
This allows queries to map between the platform surfaces tracked in the statistics and the name assigned by the player
179 lines
5.3 KiB
TypeScript
179 lines
5.3 KiB
TypeScript
import * as lib from "@clusterio/lib";
|
|
import { BaseInstancePlugin } from "@clusterio/host";
|
|
|
|
const { Gauge } = lib;
|
|
|
|
type IpcStats = {
|
|
game_tick: number,
|
|
player_count: number,
|
|
surface_statistics: {
|
|
[key: string]: {
|
|
game_flow_statistics: {
|
|
pollution_statistics: IpcFlowType,
|
|
},
|
|
force_flow_statistics: {
|
|
[key:string]: {
|
|
[key:string]: IpcFlowType,
|
|
}
|
|
},
|
|
}
|
|
},
|
|
platforms: {
|
|
[key: string]: {
|
|
force: string,
|
|
surface: string,
|
|
}
|
|
}
|
|
}
|
|
|
|
type IpcFlowType = {
|
|
"input": Record<string, number>,
|
|
"output": Record<string, number>,
|
|
}
|
|
|
|
const instancePlayerCount = new Gauge(
|
|
"clusterio_statistics_exporter_instance_player_count",
|
|
"Amount of players connected to this cluster",
|
|
{ labels: ["instance_id"] }
|
|
);
|
|
const instanceGameTicksTotal = new Gauge(
|
|
"clusterio_statistics_exporter_instance_game_ticks_total",
|
|
"Game tick an instance has progressed to",
|
|
{ labels: ["instance_id"] }
|
|
);
|
|
const instanceForceFlowStatistics = new Gauge(
|
|
"clusterio_statistics_exporter_instance_force_flow_statistics",
|
|
"Items/fluids/enemies/buildings produced/built/killed by a force",
|
|
{ labels: ["instance_id", "surface", "force", "statistic", "direction", "name"] },
|
|
);
|
|
const instanceGameFlowStatistics = new Gauge(
|
|
"clusterio_statistics_exporter_instance_game_flow_statistics",
|
|
"Pollution produced/consumed in the game",
|
|
{ labels: ["instance_id", "surface", "statistic", "direction", "name"] },
|
|
);
|
|
const instancePlatformMapping = new Gauge(
|
|
"clusterio_statistics_exporter_platform_mapping",
|
|
"Mapping of platform name to force, surface and instance",
|
|
{ labels: ["instance_id", "platform_name", "force", "surface"] }
|
|
);
|
|
|
|
function setForceFlowStatistic(
|
|
instanceId: number,
|
|
surfaceName: string,
|
|
forceName: string,
|
|
statisticName: string,
|
|
direction: string,
|
|
item: string,
|
|
value: number,
|
|
) {
|
|
instanceForceFlowStatistics.labels(
|
|
String(instanceId),
|
|
surfaceName,
|
|
forceName,
|
|
statisticName,
|
|
direction,
|
|
item
|
|
).set(value);
|
|
|
|
// For item and fluid statistics it's useful to compare the input flow with the
|
|
// output flow, to simplify the comparison ensure both directions have a value.
|
|
if (["item_production_statistics", "fluid_production_statistic"].includes(statisticName)) {
|
|
let reverseDirection = direction === "input" ? "output" : "input";
|
|
instanceForceFlowStatistics.labels(
|
|
String(instanceId),
|
|
surfaceName,
|
|
forceName,
|
|
statisticName,
|
|
reverseDirection,
|
|
item
|
|
).set(value);
|
|
}
|
|
}
|
|
|
|
|
|
export class InstancePlugin extends BaseInstancePlugin {
|
|
async init() {
|
|
if (!this.instance.config.get("factorio.enable_save_patching")) {
|
|
throw new Error("statistics_exporter plugin requires save patching.");
|
|
}
|
|
if (!this.instance.config.get("factorio.enable_script_commands")) {
|
|
throw new Error("statistics_exporter plugin requires script commands.");
|
|
}
|
|
}
|
|
|
|
async gatherMetrics() {
|
|
let string = await this.sendRcon("/sc statistics_exporter.export()");
|
|
let stats: IpcStats;
|
|
try {
|
|
stats = JSON.parse(string);
|
|
} catch (err: any) {
|
|
throw new Error(`Error parsing statistics JSON: ${err.message}, content "${string}"`);
|
|
}
|
|
|
|
let instanceId = this.instance.id;
|
|
instanceGameTicksTotal.labels(String(instanceId)).set(stats.game_tick);
|
|
instancePlayerCount.labels(String(instanceId)).set(stats.player_count);
|
|
|
|
// Remove existing platform mappings for this instance
|
|
instancePlatformMapping.removeAll({ instance_id: String(instanceId) });
|
|
|
|
// Set new platform mappings
|
|
for (let [platformName, platform] of Object.entries(stats.platforms)) {
|
|
instancePlatformMapping.labels(
|
|
String(instanceId),
|
|
platformName,
|
|
platform.force,
|
|
platform.surface
|
|
).set(1);
|
|
}
|
|
|
|
for (let [surfaceName, surfaceStats] of Object.entries(stats.surface_statistics)) {
|
|
for (let [forceName, flowStatistics] of Object.entries(surfaceStats.force_flow_statistics)) {
|
|
for (let [statisticName, statistic] of Object.entries(flowStatistics)) {
|
|
for (let [direction, counts] of Object.entries(statistic)) {
|
|
// eslint-disable-next-line max-depth
|
|
for (let [item, value] of Object.entries(counts)) {
|
|
setForceFlowStatistic(
|
|
instanceId,
|
|
surfaceName,
|
|
forceName,
|
|
statisticName,
|
|
direction,
|
|
item,
|
|
value
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let [direction, counts] of Object.entries(surfaceStats.game_flow_statistics.pollution_statistics)) {
|
|
for (let [item, value] of Object.entries(counts)) {
|
|
instanceGameFlowStatistics.labels(
|
|
String(instanceId), surfaceName, "pollution_statistics", direction, item
|
|
).set(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async onMetrics() {
|
|
if (this.instance.status !== "running") {
|
|
return;
|
|
}
|
|
|
|
// Wait configured timeout for the metrics to be collected. It may
|
|
// take a long time for the command to go through if the command
|
|
// stream is overloaded. Should the timeout be exceeded the
|
|
// previous values for the metrics will end up being sent to controller.
|
|
let timeoutMs = this.instance.config.get("statistics_exporter.command_timeout") * 1000;
|
|
await lib.timeout(this.gatherMetrics(), timeoutMs, undefined);
|
|
}
|
|
}
|
|
|
|
export const _instancePlayerCount = instancePlayerCount;
|
|
export const _instanceGameTicksTotal = instanceGameTicksTotal;
|
|
export const _instanceForceFlowStatistics = instanceForceFlowStatistics;
|
|
export const _instanceGameFlowStatistics = instanceGameFlowStatistics;
|
|
export const _instancePlatformMapping = instancePlatformMapping;
|