diff --git a/packages/cactus-plugin-consortium-manual/README.md b/packages/cactus-plugin-consortium-manual/README.md new file mode 100644 index 00000000000..e10af3b864d --- /dev/null +++ b/packages/cactus-plugin-consortium-manual/README.md @@ -0,0 +1,39 @@ +# `@hyperledger/cactus-plugin-consortium-manual` + +## Prometheus Exporter +This class creates a prometheus exporter, which scrapes the total Cactus node count. + +### Usage Prometheus +The prometheus exporter object is initialized in the `PluginConsortiumManual` class constructor itself, so instantiating the object of the `PluginConsortiumManual` class, gives access to the exporter object. +You can also initialize the prometheus exporter object seperately and then pass it to the `IPluginConsortiumManualOptions` interface for `PluginConsortiumManual` constructor. + +`getPrometheusExporterMetricsV1` function returns the prometheus exporter metrics, currently displaying the total cactus node count, which currently refreshes to match the node count in the consortium, everytime `updateMetricNodeCount` method of the `PluginConsortiumManual` class is called. + +### Prometheus Integration +To use Prometheus with this exporter make sure to install [Prometheus main component](https://prometheus.io/download/). +Once Prometheus is setup, the corresponding scrape_config needs to be added to the prometheus.yml + +```(yaml) +- job_name: 'consortium_manual_exporter' + metrics_path: api/v1/plugins/@hyperledger/cactus-plugin-consortium-manual/get-prometheus-exporter-metrics + scrape_interval: 5s + static_configs: + - targets: ['{host}:{port}'] +``` + +Here the `host:port` is where the prometheus exporter metrics are exposed. The test cases (For example, packages/cactus-plugin-consortium-manual/src/test/typescript/unit/consortium/get-node-jws-endpoint-v1.test.ts) exposes it over `0.0.0.0` and a random port(). The random port can be found in the running logs of the test case and looks like (42379 in the below mentioned URL) +`Metrics URL: http://0.0.0.0:42379/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-prometheus-exporter-metrics` + +Once edited, you can start the prometheus service by referencing the above edited prometheus.yml file. +On the prometheus graphical interface (defaulted to http://localhost:9090), choose **Graph** from the menu bar, then select the **Console** tab. From the **Insert metric at cursor** drop down, select **cactus_consortium_manual_total_node_count** and click **execute** + +### Helper code + +###### response.type.ts +This file contains the various responses of the metrics. + +###### data-fetcher.ts +This file contains functions encasing the logic to process the data points. + +###### metrics.ts +This file lists all the prometheus metrics and what they are used for. \ No newline at end of file diff --git a/packages/cactus-plugin-consortium-manual/package-lock.json b/packages/cactus-plugin-consortium-manual/package-lock.json index 29c4e8c6492..7e7188493e7 100644 --- a/packages/cactus-plugin-consortium-manual/package-lock.json +++ b/packages/cactus-plugin-consortium-manual/package-lock.json @@ -197,6 +197,11 @@ "file-uri-to-path": "1.0.0" } }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, "bn.js": { "version": "4.11.9", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", @@ -785,6 +790,14 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "prom-client": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-13.0.0.tgz", + "integrity": "sha512-M7ZNjIO6x+2R/vjSD13yjJPjpoZA8eEwH2Bp2Re0/PvzozD7azikv+SaBtZes4Q1ca/xHjZ4RSCuTag3YZLg1A==", + "requires": { + "tdigest": "^0.1.1" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -914,6 +927,14 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" }, + "tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } + }, "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", diff --git a/packages/cactus-plugin-consortium-manual/package.json b/packages/cactus-plugin-consortium-manual/package.json index c042bdcac6c..871996c3c51 100644 --- a/packages/cactus-plugin-consortium-manual/package.json +++ b/packages/cactus-plugin-consortium-manual/package.json @@ -84,6 +84,7 @@ "body-parser": "1.19.0", "express": "4.17.1", "express-openapi-validator": "3.12.9", + "prom-client": "13.0.0", "joi": "14.3.1", "jose": "1.27.2", "js-sha3": "0.8.0", diff --git a/packages/cactus-plugin-consortium-manual/src/main/json/openapi.json b/packages/cactus-plugin-consortium-manual/src/main/json/openapi.json index 4a2e4fa3be9..4f25370b3dd 100644 --- a/packages/cactus-plugin-consortium-manual/src/main/json/openapi.json +++ b/packages/cactus-plugin-consortium-manual/src/main/json/openapi.json @@ -57,6 +57,10 @@ "format": "The general format which is a JSON object, not a string." } } + }, + "PrometheusExporterMetricsResponse": { + "type": "string", + "nullable": false } } }, @@ -111,6 +115,31 @@ } } } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-consortium-manual/get-prometheus-exporter-metrics": { + "get": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "get", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-consortium-manual/get-prometheus-exporter-metrics" + } + }, + "operationId": "getPrometheusExporterMetricsV1", + "summary": "Get the Prometheus Metrics", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PrometheusExporterMetricsResponse" + } + } + } + } + } + } } } } \ No newline at end of file diff --git a/packages/cactus-plugin-consortium-manual/src/main/typescript/consortium/get-node-jws-endpoint-v1.ts b/packages/cactus-plugin-consortium-manual/src/main/typescript/consortium/get-node-jws-endpoint-v1.ts index f8114f8c319..40468171fc1 100644 --- a/packages/cactus-plugin-consortium-manual/src/main/typescript/consortium/get-node-jws-endpoint-v1.ts +++ b/packages/cactus-plugin-consortium-manual/src/main/typescript/consortium/get-node-jws-endpoint-v1.ts @@ -24,8 +24,10 @@ import { } from "@hyperledger/cactus-core"; import OAS from "../../json/openapi.json"; +import { PluginConsortiumManual } from "../plugin-consortium-manual"; export interface IGetNodeJwsEndpointOptions { + plugin: PluginConsortiumManual; keyPairPem: string; consortiumRepo: ConsortiumRepository; logLevel?: LogLevelDesc; @@ -33,6 +35,7 @@ export interface IGetNodeJwsEndpointOptions { export class GetNodeJwsEndpoint implements IWebServiceEndpoint { private readonly log: Logger; + private readonly plugin: PluginConsortiumManual; constructor(public readonly options: IGetNodeJwsEndpointOptions) { const fnTag = "GetNodeJwsEndpoint#constructor()"; @@ -43,6 +46,12 @@ export class GetNodeJwsEndpoint implements IWebServiceEndpoint { throw new Error(`${fnTag} options.keyPairPem falsy.`); } Checks.truthy(options.consortiumRepo, `${fnTag} options.consortiumRepo`); + Checks.truthy(options.plugin, `${fnTag} options.plugin`); + Checks.truthy( + options.plugin instanceof PluginConsortiumManual, + `${fnTag} options.plugin instanceof PluginConsortiumManual`, + ); + this.plugin = options.plugin; const level = options.logLevel || "INFO"; const label = "get-node-jws-endpoint-v1"; @@ -95,6 +104,11 @@ export class GetNodeJwsEndpoint implements IWebServiceEndpoint { const fnTag = "GetNodeJwsEndpoint#createJws()"; const { keyPairPem, consortiumRepo: repo } = this.options; try { + // TODO: move this logic here entirely to the plugin itself. We already + // have an issue open for it on GH most likely, someone may already be + // working on this very thing actually so please do double check prior + // to diving in and working on it to avoid redundant effort. + this.plugin.updateMetricNodeCount(); const keyPair = JWK.asKey(keyPairPem); const payloadObject = { consortiumDatabase: repo.consortiumDatabase }; const payloadJson = jsonStableStringify(payloadObject); diff --git a/packages/cactus-plugin-consortium-manual/src/main/typescript/consortium/get-prometheus-exporter-metrics-endpoint-v1.ts b/packages/cactus-plugin-consortium-manual/src/main/typescript/consortium/get-prometheus-exporter-metrics-endpoint-v1.ts new file mode 100644 index 00000000000..0051d8e6d41 --- /dev/null +++ b/packages/cactus-plugin-consortium-manual/src/main/typescript/consortium/get-prometheus-exporter-metrics-endpoint-v1.ts @@ -0,0 +1,81 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + LoggerProvider, + LogLevelDesc, + Checks, +} from "@hyperledger/cactus-common"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, +} from "@hyperledger/cactus-core-api"; + +import OAS from "../../json/openapi.json"; + +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import { PluginConsortiumManual } from "../plugin-consortium-manual"; + +export interface IGetPrometheusExporterMetricsEndpointV1Options { + logLevel?: LogLevelDesc; + plugin: PluginConsortiumManual; +} + +export class GetPrometheusExporterMetricsEndpointV1 + implements IWebServiceEndpoint { + private readonly log: Logger; + + constructor( + public readonly opts: IGetPrometheusExporterMetricsEndpointV1Options, + ) { + const fnTag = "GetPrometheusExporterMetricsEndpointV1#constructor()"; + + Checks.truthy(opts, `${fnTag} options`); + Checks.truthy(opts.plugin, `${fnTag} options.plugin`); + + this.log = LoggerProvider.getOrCreate({ + label: "get-prometheus-exporter-metrics-v1", + level: opts.logLevel || "INFO", + }); + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public getPath(): string { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-consortium-manual/get-prometheus-exporter-metrics" + ].get["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-consortium-manual/get-prometheus-exporter-metrics" + ].get["x-hyperledger-cactus"].http.verbLowerCase; + } + + public registerExpress(app: Express): IWebServiceEndpoint { + registerWebServiceEndpoint(app, this); + return this; + } + + async handleRequest(req: Request, res: Response): Promise { + const fnTag = "GetPrometheusExporterMetrics#handleRequest()"; + const verbUpper = this.getVerbLowerCase().toUpperCase(); + this.log.debug(`${verbUpper} ${this.getPath()}`); + + try { + const resBody = await this.opts.plugin.getPrometheusExporterMetrics(); + res.status(200); + res.send(resBody); + } catch (ex) { + this.log.error(`${fnTag} failed to serve request`, ex); + res.status(500); + res.statusMessage = ex.message; + res.json({ error: ex.stack }); + } + } +} diff --git a/packages/cactus-plugin-consortium-manual/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-consortium-manual/src/main/typescript/generated/openapi/typescript-axios/api.ts index 7ef41874109..15bfeb5b38d 100644 --- a/packages/cactus-plugin-consortium-manual/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-consortium-manual/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -152,6 +152,42 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati + const query = new URLSearchParams(localVarUrlObj.search); + for (const key in localVarQueryParameter) { + query.set(key, localVarQueryParameter[key]); + } + for (const key in options.query) { + query.set(key, options.query[key]); + } + localVarUrlObj.search = (new URLSearchParams(query)).toString(); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get the Prometheus Metrics + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPrometheusExporterMetricsV1: async (options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-consortium-manual/get-prometheus-exporter-metrics`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, 'https://example.com'); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + const query = new URLSearchParams(localVarUrlObj.search); for (const key in localVarQueryParameter) { query.set(key, localVarQueryParameter[key]); @@ -203,6 +239,19 @@ export const DefaultApiFp = function(configuration?: Configuration) { return axios.request(axiosRequestArgs); }; }, + /** + * + * @summary Get the Prometheus Metrics + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPrometheusExporterMetricsV1(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await DefaultApiAxiosParamCreator(configuration).getPrometheusExporterMetricsV1(options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, } }; @@ -230,6 +279,15 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa getNodeJws(options?: any): AxiosPromise { return DefaultApiFp(configuration).getNodeJws(options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Get the Prometheus Metrics + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPrometheusExporterMetricsV1(options?: any): AxiosPromise { + return DefaultApiFp(configuration).getPrometheusExporterMetricsV1(options).then((request) => request(axios, basePath)); + }, }; }; @@ -261,6 +319,17 @@ export class DefaultApi extends BaseAPI { public getNodeJws(options?: any) { return DefaultApiFp(this.configuration).getNodeJws(options).then((request) => request(this.axios, this.basePath)); } + + /** + * + * @summary Get the Prometheus Metrics + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getPrometheusExporterMetricsV1(options?: any) { + return DefaultApiFp(this.configuration).getPrometheusExporterMetricsV1(options).then((request) => request(this.axios, this.basePath)); + } } diff --git a/packages/cactus-plugin-consortium-manual/src/main/typescript/plugin-consortium-manual.ts b/packages/cactus-plugin-consortium-manual/src/main/typescript/plugin-consortium-manual.ts index 8a05961b4b9..1caf6097fc3 100644 --- a/packages/cactus-plugin-consortium-manual/src/main/typescript/plugin-consortium-manual.ts +++ b/packages/cactus-plugin-consortium-manual/src/main/typescript/plugin-consortium-manual.ts @@ -25,6 +25,12 @@ import { import { GetConsortiumEndpointV1 } from "./consortium/get-consortium-jws-endpoint-v1"; import { GetNodeJwsEndpoint } from "./consortium/get-node-jws-endpoint-v1"; +import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; + +import { + IGetPrometheusExporterMetricsEndpointV1Options, + GetPrometheusExporterMetricsEndpointV1, +} from "./consortium/get-prometheus-exporter-metrics-endpoint-v1"; export interface IWebAppOptions { port: number; hostname: string; @@ -33,6 +39,7 @@ export interface IWebAppOptions { export interface IPluginConsortiumManualOptions extends ICactusPluginOptions { keyPairPem: string; consortiumDatabase: ConsortiumDatabase; + prometheusExporter?: PrometheusExporter; pluginRegistry?: PluginRegistry; logLevel?: LogLevelDesc; webAppOptions?: IWebAppOptions; @@ -40,6 +47,7 @@ export interface IPluginConsortiumManualOptions extends ICactusPluginOptions { export class PluginConsortiumManual implements ICactusPlugin, IPluginWebService { + public prometheusExporter: PrometheusExporter; private readonly log: Logger; private readonly instanceId: string; private httpServer: Server | SecureServer | null = null; @@ -58,12 +66,53 @@ export class PluginConsortiumManual label: "plugin-consortium-manual", }); this.instanceId = this.options.instanceId; + this.prometheusExporter = + options.prometheusExporter || + new PrometheusExporter({ pollingIntervalInMin: 1 }); + Checks.truthy( + this.prometheusExporter, + `${fnTag} options.prometheusExporter`, + ); + this.prometheusExporter.setNodeCount(this.getNodeCount()); } public getInstanceId(): string { return this.instanceId; } + public getPrometheusExporter(): PrometheusExporter { + return this.prometheusExporter; + } + + public async getPrometheusExporterMetrics(): Promise { + const res: string = await this.prometheusExporter.getPrometheusMetrics(); + this.log.debug(`getPrometheusExporterMetrics() response: %o`, res); + return res; + } + + public getNodeCount(): number { + const consortiumDatabase: ConsortiumDatabase = this.options + .consortiumDatabase; + const consortiumRepo: ConsortiumRepository = new ConsortiumRepository({ + db: consortiumDatabase, + }); + return consortiumRepo.allNodes.length; + } + + /** + * Updates the Node count Prometheus metric of the plugin. + * Note: This does not change the underlying consortium database at all, + * only affects **the metrics**. + */ + public updateMetricNodeCount(): void { + const constortiumDatabase: ConsortiumDatabase = this.options + .consortiumDatabase; + const consortiumRepo: ConsortiumRepository = new ConsortiumRepository({ + db: constortiumDatabase, + }); + this.prometheusExporter.setNodeCount(consortiumRepo.allNodes.length); + } + public async shutdown(): Promise { this.log.info(`Shutting down...`); const serverMaybe = this.getHttpServer(); @@ -123,13 +172,22 @@ export class PluginConsortiumManual this.log.info(`Registered GetConsortiumEndpointV1 at ${path}`); } { - const options = { keyPairPem, consortiumRepo }; + const options = { keyPairPem, consortiumRepo, plugin: this }; const endpoint = new GetNodeJwsEndpoint(options); const path = endpoint.getPath(); webApp.get(path, endpoint.getExpressRequestHandler()); endpoints.push(endpoint); this.log.info(`Registered GetNodeJwsEndpoint at ${path}`); } + { + const opts: IGetPrometheusExporterMetricsEndpointV1Options = { + plugin: this, + logLevel: this.options.logLevel, + }; + const endpoint = new GetPrometheusExporterMetricsEndpointV1(opts); + endpoint.registerExpress(expressApp); + endpoints.push(endpoint); + } log.info(`Installed web svcs for plugin ${this.getPackageName()} OK`, { endpoints, diff --git a/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/data-fetcher.ts b/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/data-fetcher.ts new file mode 100644 index 00000000000..b28f92c990f --- /dev/null +++ b/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/data-fetcher.ts @@ -0,0 +1,12 @@ +import { NodeCount } from "./response.type"; + +import { + totalTxCount, + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT, +} from "./metrics"; + +export async function collectMetrics(nodeCount: NodeCount) { + totalTxCount + .labels(K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT) + .set(nodeCount.counter); +} diff --git a/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/metrics.ts b/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/metrics.ts new file mode 100644 index 00000000000..55d35149ed6 --- /dev/null +++ b/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/metrics.ts @@ -0,0 +1,10 @@ +import { Gauge } from "prom-client"; + +export const K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT = + "cactus_consortium_manual_total_node_count"; + +export const totalTxCount = new Gauge({ + name: K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT, + help: "Total cactus node count", + labelNames: ["type"], +}); diff --git a/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/prometheus-exporter.ts b/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/prometheus-exporter.ts new file mode 100644 index 00000000000..4208c863598 --- /dev/null +++ b/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/prometheus-exporter.ts @@ -0,0 +1,42 @@ +import promClient from "prom-client"; +import { NodeCount } from "./response.type"; +import { + totalTxCount, + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT, +} from "./metrics"; + +export interface IPrometheusExporterOptions { + pollingIntervalInMin?: number; +} + +export class PrometheusExporter { + public readonly metricsPollingIntervalInMin: number; + public readonly nodeCount: NodeCount = { counter: 0 }; + + constructor( + public readonly prometheusExporterOptions: IPrometheusExporterOptions, + ) { + this.metricsPollingIntervalInMin = + prometheusExporterOptions.pollingIntervalInMin || 1; + } + + public setNodeCount(nodeCount: number): void { + this.nodeCount.counter = nodeCount; + totalTxCount + .labels(K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT) + .set(this.nodeCount.counter); + } + + public async getPrometheusMetrics(): Promise { + const result = await promClient.register.getSingleMetricAsString( + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT, + ); + return result; + } + + public startMetricsCollection(): void { + const Registry = promClient.Registry; + const register = new Registry(); + promClient.collectDefaultMetrics({ register }); + } +} diff --git a/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/response.type.ts b/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/response.type.ts new file mode 100644 index 00000000000..4f2cf2ac0c9 --- /dev/null +++ b/packages/cactus-plugin-consortium-manual/src/main/typescript/prometheus-exporter/response.type.ts @@ -0,0 +1,3 @@ +export type NodeCount = { + counter: number; +}; diff --git a/packages/cactus-plugin-consortium-manual/src/test/typescript/unit/consortium/get-node-jws-endpoint-v1.test.ts b/packages/cactus-plugin-consortium-manual/src/test/typescript/unit/consortium/get-node-jws-endpoint-v1.test.ts index e8d68440b17..280c5a6dd79 100644 --- a/packages/cactus-plugin-consortium-manual/src/test/typescript/unit/consortium/get-node-jws-endpoint-v1.test.ts +++ b/packages/cactus-plugin-consortium-manual/src/test/typescript/unit/consortium/get-node-jws-endpoint-v1.test.ts @@ -1,15 +1,32 @@ import test, { Test } from "tape"; import { JWS, JWK } from "jose"; +import express from "express"; +import bodyParser from "body-parser"; +import http from "http"; +import { AddressInfo } from "net"; -import { ConsortiumDatabase } from "@hyperledger/cactus-core-api"; +import { IListenOptions, Servers } from "@hyperledger/cactus-common"; + +import { ConsortiumDatabase, CactusNode } from "@hyperledger/cactus-core-api"; import { ConsortiumRepository } from "@hyperledger/cactus-core"; +import { + PluginConsortiumManual, + IPluginConsortiumManualOptions, +} from "../../../../main/typescript/plugin-consortium-manual"; + import { GetNodeJwsEndpoint, IGetNodeJwsEndpointOptions, } from "../../../../main/typescript/public-api"; +import { v4 as uuidv4 } from "uuid"; + +import { DefaultApi as ConsortiumManualApi } from "../../../../main/typescript/public-api"; + +import { K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT } from "../../../../main/typescript/prometheus-exporter/metrics"; + test("Can provide JWS", async (t: Test) => { t.ok(GetNodeJwsEndpoint); @@ -25,7 +42,40 @@ test("Can provide JWS", async (t: Test) => { }; const consortiumRepo = new ConsortiumRepository({ db }); + // Creating the PluginConsortiumManual object to observe the prometheus metrics. + const options: IPluginConsortiumManualOptions = { + instanceId: uuidv4(), + keyPairPem: keyPairPem, + consortiumDatabase: db, + }; + + const pluginConsortiumManual: PluginConsortiumManual = new PluginConsortiumManual( + options, + ); + + // Setting up of the api-server for hosting the endpoints defined in the openapi specs + // of the plugin + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + const server = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "0.0.0.0", + port: 0, + server, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + test.onFinish(async () => await Servers.shutdown(server)); + const { address, port } = addressInfo; + const apiHost = `http://${address}:${port}`; + t.comment( + `Metrics URL: ${apiHost}/api/v1/plugins/@hyperledger/cactus-plugin-consortium-manual/get-prometheus-exporter-metrics`, + ); + const apiClient = new ConsortiumManualApi({ basePath: apiHost }); + + await pluginConsortiumManual.installWebServices(expressApp); + const epOpts: IGetNodeJwsEndpointOptions = { + plugin: pluginConsortiumManual, consortiumRepo, keyPairPem, }; @@ -50,5 +100,72 @@ test("Can provide JWS", async (t: Test) => { t.ok(payload.consortiumDatabase, "JWS payload.consortiumDatabase truthy"); } + { + // The first check shall observe the cactus_consortium_manual_total_node_count metrics + // to be valued at zero, as the ConsortiumRepo object is initialized with an empty array of + // Cactus nodes. + const res = await apiClient.getPrometheusExporterMetricsV1(); + const promMetricsOutput = + "# HELP " + + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT + + " Total cactus node count\n" + + "# TYPE " + + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT + + " gauge\n" + + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT + + '{type="' + + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT + + '"} 0'; + t.ok(res); + t.ok(res.data); + t.equal(res.status, 200); + t.true( + res.data.includes(promMetricsOutput), + "Total Cactus Node Count of 0 recorded as expected. RESULT OK", + ); + } + + // Creating a dummy cactus node for adding it to the cactus node array + // and thus observing the change in prometheus exporter metrics (should increment by 1) + const dummyCactusNode: CactusNode = { + consortiumId: "", + id: "", + ledgerIds: [], + memberId: "", + nodeApiHost: "", + pluginInstanceIds: [], + publicKeyPem: "", + }; + + consortiumRepo.consortiumDatabase.cactusNode.push(dummyCactusNode); + // The invocation of the node JWS endpoint internally triggers the update + // of the metrics so after it has executed we can expect the metrics to + // show the new values for our assertions below + await apiClient.getNodeJws(); + + { + // The second check shall observe the cactus_consortium_manual_total_node_count metrics + // to be valued at One, as the Cactus node array is pushed with a dummy cactus node. + const res = await apiClient.getPrometheusExporterMetricsV1(); + const promMetricsOutput = + "# HELP " + + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT + + " Total cactus node count\n" + + "# TYPE " + + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT + + " gauge\n" + + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT + + '{type="' + + K_CACTUS_CONSORTIUM_MANUAL_TOTAL_NODE_COUNT + + '"} 1'; + t.ok(res); + t.ok(res.data); + t.equal(res.status, 200); + t.true( + res.data.includes(promMetricsOutput), + "Total Cactus Node Count of 1 recorded as expected. RESULT OK", + ); + } + t.end(); });