diff --git a/packages/controller/src/routes.ts b/packages/controller/src/routes.ts index f8e58fe5..1c9c441c 100644 --- a/packages/controller/src/routes.ts +++ b/packages/controller/src/routes.ts @@ -18,7 +18,7 @@ const finished = util.promisify(nodeStream.finished); // Merges samples from sourceResult to destinationResult -function mergeSamples(destinationResult: any, sourceResult: any) { +function mergeSamples(destinationResult: lib.CollectorResultSerialized, sourceResult: lib.CollectorResultSerialized) { let receivedSamples = new Map(sourceResult.samples); for (let [suffix, suffixSamples] of destinationResult.samples) { if (receivedSamples.has(suffix)) { @@ -36,15 +36,16 @@ function mergeSamples(destinationResult: any, sourceResult: any) { async function getMetrics(req: Request, res: Response, next: any) { const controller: Controller = req.app.locals.controller; - let results: any[] = []; - let pluginResults: any[] = await lib.invokeHook(controller.plugins, "onMetrics"); + let results: lib.CollectorResult[] = []; + type ResultIterator = AsyncIterable | Iterable + let pluginResults = await lib.invokeHook(controller.plugins, "onMetrics") as ResultIterator[]; for (let metricIterator of pluginResults) { for await (let metric of metricIterator) { results.push(metric); } } - let requests: Promise[] = []; + let requests: Promise | null>[] = []; let timeout = controller.config.get("controller.metrics_timeout") * 1000; for (let [hostId, hostConnection] of controller.wsServer.hostConnections) { if (!hostConnection.connected) { @@ -65,7 +66,7 @@ async function getMetrics(req: Request, res: Response, next: any) { results.push(result); } - let resultMap: Map = new Map(); + let resultMap: Map = new Map(); for (let response of await Promise.all(requests)) { if (!response) { // TODO: Log timeout occured? @@ -83,17 +84,10 @@ async function getMetrics(req: Request, res: Response, next: any) { } } - // TODO: results seem to mix deserialized and serialized lib.CollectorResult ? - // mergeSamples imply samples be [string, [string, number][]] - // but lib.defaultRegistry.collect() imply Map> for (let result of resultMap.values()) { results.push(lib.deserializeResult(result)); } - - // lib.exposition expect AsyncIterable as parameter. - // Should wrap results into a Generator functions ? - // @ts-ignore let text = await lib.exposition(results); res.set("Content-Type", lib.expositionContentType); res.send(text); diff --git a/packages/host/package.json b/packages/host/package.json index 0468f46b..c333e939 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -20,6 +20,7 @@ }, "dependencies": { "@clusterio/lib": "^2.0.0-alpha.13", + "@sinclair/typebox": "^0.30.4", "fs-extra": "^11.1.1", "jimp": "^0.22.8", "jszip": "^3.10.1", @@ -36,7 +37,6 @@ "access": "public" }, "devDependencies": { - "@sinclair/typebox": "^0.30.4", "@types/fs-extra": "^11.0.1", "@types/node": "^20.4.5", "@types/pidusage": "^2.0.2", diff --git a/packages/host/src/Host.ts b/packages/host/src/Host.ts index b6311733..602d1217 100644 --- a/packages/host/src/Host.ts +++ b/packages/host/src/Host.ts @@ -542,7 +542,7 @@ export default class Host extends lib.Link { } async handleHostMetricsRequest() { - let requests = []; + let requests: Promise>[] = []; for (let instanceConnection of this.instanceConnections.values()) { requests.push(instanceConnection.send(new lib.InstanceMetricsRequest())); } diff --git a/packages/host/src/Instance.ts b/packages/host/src/Instance.ts index 1f3f0800..dd9e7e21 100644 --- a/packages/host/src/Instance.ts +++ b/packages/host/src/Instance.ts @@ -962,11 +962,12 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " "); } async handleInstanceMetricsRequest() { - let results = []; + let results: ReturnType[] = []; if (!["stopped", "stopping"].includes(this._status)) { - let pluginResults = await lib.invokeHook(this.plugins, "onMetrics"); + type ResultIterator = AsyncIterable | Iterable + let pluginResults = await lib.invokeHook(this.plugins, "onMetrics") as ResultIterator[]; for (let metricIterator of pluginResults) { - for await (let metric of metricIterator as any) { + for await (let metric of metricIterator) { results.push(lib.serializeResult(metric)); } } diff --git a/packages/lib/src/data/messages_host.ts b/packages/lib/src/data/messages_host.ts index 4343e14b..92b5452f 100644 --- a/packages/lib/src/data/messages_host.ts +++ b/packages/lib/src/data/messages_host.ts @@ -1,5 +1,6 @@ import { Type, Static } from "@sinclair/typebox"; import { jsonArray, StringEnum } from "./composites"; +import { CollectorResultSerialized } from "../prometheus"; export class HostDetails { constructor( @@ -75,11 +76,11 @@ export class HostMetricsRequest { static dst = "host" as const; static Response = class Response { // TODO: Use JSON class pattern in Prometheus constructor( - public results: object[], + public results: CollectorResultSerialized[], ) { } static jsonSchema = Type.Object({ - "results": Type.Array(Type.Object({})), + "results": Type.Array(CollectorResultSerialized), }); static fromJSON(json: Static) { diff --git a/packages/lib/src/data/messages_instance.ts b/packages/lib/src/data/messages_instance.ts index 32cdab2b..d97ca0be 100644 --- a/packages/lib/src/data/messages_instance.ts +++ b/packages/lib/src/data/messages_instance.ts @@ -4,7 +4,7 @@ import { JsonString, StringEnum, jsonArray } from "./composites"; import { RawConfig } from "../config"; import type { User } from "../users"; import type { MessageRequest } from "./messages_core"; -import type { SerializedConfig } from "../config"; +import { CollectorResultSerialized } from "../prometheus"; export type InstanceStatus = @@ -216,11 +216,11 @@ export class InstanceMetricsRequest { static Response = class Response { // TODO: Use JSON class pattern in Prometheus constructor( - public results: object[], + public results: CollectorResultSerialized[], ) { } static jsonSchema = Type.Object({ - "results": Type.Array(Type.Object({})), + "results": Type.Array(CollectorResultSerialized), }); static fromJSON(json: Static) { diff --git a/packages/lib/src/prometheus.ts b/packages/lib/src/prometheus.ts index 74bc8d65..d3359fe7 100644 --- a/packages/lib/src/prometheus.ts +++ b/packages/lib/src/prometheus.ts @@ -38,6 +38,8 @@ * app.listen(9100); * @module lib/prometheus */ +import { Type, Static } from "@sinclair/typebox"; +import { StringEnum } from "./data/composites"; /** * Result from collecting a {@link Collector} @@ -1611,7 +1613,9 @@ function escapeHelp(help: string) { ; } -async function* expositionLines(resultsIterator: AsyncIterable) { +async function* expositionLines( + resultsIterator: AsyncIterable | Iterable +) { let first = true; for await (let result of resultsIterator) { if (first) { @@ -1665,7 +1669,9 @@ async function* expositionLines(resultsIterator: AsyncIterable) * defaultRegistry}. * @returns Prometheus exposition. */ -export async function exposition(resultsIterator = defaultRegistry.collect()) { +export async function exposition( + resultsIterator: AsyncIterable | Iterable = defaultRegistry.collect() +) { let lines = ""; for await (let line of expositionLines(resultsIterator)) { lines += line; @@ -1678,6 +1684,24 @@ export async function exposition(resultsIterator = defaultRegistry.collect()) { */ export const expositionContentType = "text/plain; version=0.0.4"; +export const CollectorResultSerialized = Type.Object({ + metric: Type.Object({ + type: StringEnum(["counter", "gauge", "histogram", "summary", "untyped"]), + name: Type.String(), + help: Type.String(), + labels: Type.Array(Type.String()), + }), + samples: Type.Array( + Type.Tuple([ + Type.String(), + Type.Array( + Type.Tuple([Type.String(), Type.Number()]) + ), + ]), + ), +}); +export type CollectorResultSerialized = Static; + /** * Serialize CollectorResult into a plain object * @@ -1702,7 +1726,7 @@ export function serializeResult( metricName?: string, metricHelp?: string, } = {} -) { +): CollectorResultSerialized { let { addLabels, metricName = result.metric.name, @@ -1756,7 +1780,7 @@ export function serializeResult( * @returns deserialized result. */ export function deserializeResult( - serializedResult: ReturnType + serializedResult: CollectorResultSerialized ): CollectorResult { return { metric: new Metric(