From 32d95d4106836f232d818cc73915c681aa7490db Mon Sep 17 00:00:00 2001 From: jagpreetsinghsasan Date: Wed, 10 Feb 2021 11:16:03 +0530 Subject: [PATCH] Signed-off-by: jagpreetsinghsasan feat(fabric): add prometheus metrics support to the OpenAPI specs of the plugin Primary Change -------------- 1. The fabric ledger connector plugin now includes the prometheus metrics exporter integration 2. OpenAPI spec now has api endpoint for the getting the prometheus metrics Refactorings that were also necessary to accomodate 1) and 2) ------------------------------------------------------------ 3. The generate-sdk command now has DateTime mapping to Date (to read Date as Date object instead of string) 4. GetPrometheusMetricsV1 class is created to handle the corresponding api endpoint 5. IPluginLedgerConnectorFabricOptions interface in PluginLedgerConnectorFabric class now has a prometheusExporter optional field 6. The PluginLedgerConnectorFabric class has relevant functions and codes to incorporate prometheus exporter 7. run-transaction-endpoint-v1.test is changed to incorporate the prometheus exporter for both fabric 1.4.x and 2.x versions Fixes #531 --- .../package-lock.json | 48 +++++- .../package.json | 3 +- .../src/main/json/openapi.json | 97 ++++++++++++ .../.openapi-generator-ignore | 1 + .../typescript-axios/.openapi-generator/FILES | 1 - .../generated/openapi/typescript-axios/api.ts | 141 ++++++++++++++++++ .../get-prometheus-exporter-metrics-v1.ts | 83 +++++++++++ .../plugin-ledger-connector-fabric.ts | 56 ++++++- .../prometheus-exporter/data-fetcher.ts | 13 ++ .../typescript/prometheus-exporter/metrics.ts | 7 + .../prometheus-exporter.ts | 53 +++++++ .../prometheus-exporter/response.type.ts | 6 + .../run-transaction-endpoint-v1.test.ts | 15 +- .../run-transaction-endpoint-v1.test.ts | 14 ++ 14 files changed, 530 insertions(+), 8 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-v1.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/data-fetcher.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/metrics.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/prometheus-exporter.ts create mode 100644 packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/response.type.ts diff --git a/packages/cactus-plugin-ledger-connector-fabric/package-lock.json b/packages/cactus-plugin-ledger-connector-fabric/package-lock.json index 223a603556a..97f3f875a8f 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/package-lock.json +++ b/packages/cactus-plugin-ledger-connector-fabric/package-lock.json @@ -374,6 +374,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=" + }, "bip66": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", @@ -3291,6 +3296,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" + } + }, "promise-settle": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/promise-settle/-/promise-settle-0.3.0.tgz", @@ -3905,6 +3918,14 @@ } } }, + "tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } + }, "temp": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.1.tgz", @@ -4224,6 +4245,7 @@ "resolved": "https://registry.npmjs.org/web3-eea/-/web3-eea-0.9.0.tgz", "integrity": "sha512-QCtwos4DVdvw+NhfIKi3/sLs3sVfO2q5Xw/6wQooHkqoIT/O8/0x6YbR2+OUpV2bdcvuowWOxA0JJ9K/mFGF7A==", "requires": { + "axios": "0.19.2", "ethereumjs-tx": "1.3.7", "ethereumjs-util": "6.1.0", "lodash": "4.17.15", @@ -4237,11 +4259,29 @@ "integrity": "sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w==" }, "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + }, + "dependencies": { + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + } + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "requires": { - "follow-redirects": "^1.10.0" + "ms": "2.0.0" } }, "ethereumjs-tx": { diff --git a/packages/cactus-plugin-ledger-connector-fabric/package.json b/packages/cactus-plugin-ledger-connector-fabric/package.json index 79e126559c7..4949b44991c 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/package.json +++ b/packages/cactus-plugin-ledger-connector-fabric/package.json @@ -12,7 +12,7 @@ "dist/*" ], "scripts": { - "generate-sdk": "openapi-generator generate --input-spec src/main/json/openapi.json -g typescript-axios -o ./src/main/typescript/generated/openapi/typescript-axios/ --reserved-words-mappings protected=protected", + "generate-sdk": "openapi-generator generate --input-spec src/main/json/openapi.json -g typescript-axios -o ./src/main/typescript/generated/openapi/typescript-axios/ --reserved-words-mappings protected=protected --type-mappings=DateTime=Date", "pretsc": "npm run generate-sdk", "tsc": "tsc --project ./tsconfig.json", "watch": "npm-watch", @@ -95,6 +95,7 @@ "ngo": "2.6.2", "node-ssh": "11.0.0", "openapi-types": "7.0.1", + "prom-client": "13.0.0", "temp": "0.9.1", "typescript-optional": "2.0.1", "uuid": "8.3.0", diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json index 7865982525e..046a07fdc2a 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json @@ -286,6 +286,69 @@ "type": "string" } } + }, + "Transaction": { + "type": "object", + "required": [ + "startTime", + "endTime" + ], + "properties": { + "startTime":{ + "format": "date-time", + "type": "string" + }, + "endTime":{ + "format": "date-time", + "type": "string" + } + } + }, + "Transactions": { + "type": "array", + "items":{ + "$ref": "#/components/schemas/Transaction" + } + }, + "PrometheusExporter": { + "type": "object", + "required": [ + "metricsPollingIntervalInMin" + ], + "properties": { + "metricsPollingIntervalInMin": { + "description": "The polling interval for the prometheus exporter (in minutes)", + "type": "number", + "nullable": false + }, + "transactions":{ + "description": "The transcation queue", + "$ref": "#/components/schemas/Transactions" + } + } + }, + "PrometheusExporterMetricsRequest": { + "type": "object", + "required": [ + "promExporter" + ], + "properties": { + "promExporter":{ + "description": "The prometheus exporter class object", + "$ref": "#/components/schemas/PrometheusExporter" + } + } + }, + "PrometheusExporterMetricsResponse": { + "type": "object", + "required": [ + "result" + ], + "properties": { + "result": { + "type": "string" + } + } } } }, @@ -368,6 +431,40 @@ } } } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-prometheus-exporter-metrics": { + "get": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "get", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/get-prometheus-exporter-metrics" + } + }, + "operationId": "getPrometheusExporterMetricsV1", + "summary": "Get the Prometheus Metrics", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrometheusExporterMetricsRequest" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrometheusExporterMetricsResponse" + } + } + } + } + } + } } } } \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator-ignore b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator-ignore index 6a6325b75c7..ecd97ff37fe 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator-ignore +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator-ignore @@ -24,3 +24,4 @@ git_push.sh .npmignore +.gitignore \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/FILES b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/FILES index 565ea4915d4..c123dd7d454 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/FILES +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/FILES @@ -1,4 +1,3 @@ -.gitignore api.ts base.ts configuration.ts diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts index 248eb444b34..2330ec55573 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -243,6 +243,51 @@ export interface InlineResponse501 { */ message?: string; } +/** + * + * @export + * @interface PrometheusExporter + */ +export interface PrometheusExporter { + /** + * The polling interval for the prometheus exporter (in minutes) + * @type {number} + * @memberof PrometheusExporter + */ + metricsPollingIntervalInMin: number; + /** + * + * @type {Array} + * @memberof PrometheusExporter + */ + transactions?: Array; +} +/** + * + * @export + * @interface PrometheusExporterMetricsRequest + */ +export interface PrometheusExporterMetricsRequest { + /** + * + * @type {PrometheusExporter} + * @memberof PrometheusExporterMetricsRequest + */ + promExporter: PrometheusExporter; +} +/** + * + * @export + * @interface PrometheusExporterMetricsResponse + */ +export interface PrometheusExporterMetricsResponse { + /** + * + * @type {string} + * @memberof PrometheusExporterMetricsResponse + */ + result: string; +} /** * * @export @@ -305,6 +350,25 @@ export interface RunTransactionResponse { */ functionOutput: string; } +/** + * + * @export + * @interface Transaction + */ +export interface Transaction { + /** + * + * @type {Date} + * @memberof Transaction + */ + startTime: Date; + /** + * + * @type {Date} + * @memberof Transaction + */ + endTime: Date; +} /** * DefaultApi - axios parameter creator @@ -353,6 +417,47 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Get the Prometheus Metrics + * @param {PrometheusExporterMetricsRequest} [prometheusExporterMetricsRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPrometheusExporterMetricsV1: async (prometheusExporterMetricsRequest?: PrometheusExporterMetricsRequest, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/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; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + 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}; + const needsSerialization = (typeof prometheusExporterMetricsRequest !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(prometheusExporterMetricsRequest !== undefined ? prometheusExporterMetricsRequest : {}) : (prometheusExporterMetricsRequest || ""); + + return { + url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, + options: localVarRequestOptions, + }; + }, /** * * @summary Runs a transaction on a Fabric ledger. @@ -421,6 +526,20 @@ export const DefaultApiFp = function(configuration?: Configuration) { return axios.request(axiosRequestArgs); }; }, + /** + * + * @summary Get the Prometheus Metrics + * @param {PrometheusExporterMetricsRequest} [prometheusExporterMetricsRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getPrometheusExporterMetricsV1(prometheusExporterMetricsRequest?: PrometheusExporterMetricsRequest, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await DefaultApiAxiosParamCreator(configuration).getPrometheusExporterMetricsV1(prometheusExporterMetricsRequest, options); + return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; + return axios.request(axiosRequestArgs); + }; + }, /** * * @summary Runs a transaction on a Fabric ledger. @@ -454,6 +573,16 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa deployContractGoSourceV1(deployContractGoSourceV1Request?: DeployContractGoSourceV1Request, options?: any): AxiosPromise { return DefaultApiFp(configuration).deployContractGoSourceV1(deployContractGoSourceV1Request, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Get the Prometheus Metrics + * @param {PrometheusExporterMetricsRequest} [prometheusExporterMetricsRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getPrometheusExporterMetricsV1(prometheusExporterMetricsRequest?: PrometheusExporterMetricsRequest, options?: any): AxiosPromise { + return DefaultApiFp(configuration).getPrometheusExporterMetricsV1(prometheusExporterMetricsRequest, options).then((request) => request(axios, basePath)); + }, /** * * @summary Runs a transaction on a Fabric ledger. @@ -486,6 +615,18 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).deployContractGoSourceV1(deployContractGoSourceV1Request, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Get the Prometheus Metrics + * @param {PrometheusExporterMetricsRequest} [prometheusExporterMetricsRequest] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getPrometheusExporterMetricsV1(prometheusExporterMetricsRequest?: PrometheusExporterMetricsRequest, options?: any) { + return DefaultApiFp(this.configuration).getPrometheusExporterMetricsV1(prometheusExporterMetricsRequest, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Runs a transaction on a Fabric ledger. diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-v1.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-v1.ts new file mode 100644 index 00000000000..b51aa350b7e --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-v1.ts @@ -0,0 +1,83 @@ +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 { PluginLedgerConnectorFabric } from "../plugin-ledger-connector-fabric"; +import { PrometheusExporter } from "../prometheus-exporter/prometheus-exporter"; + +export interface IGetPrometheusExporterMetricsV1Options { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorFabric; +} + +export class GetPrometheusExporterMetricsV1 implements IWebServiceEndpoint { + private readonly log: Logger; + + constructor(public readonly opts: IGetPrometheusExporterMetricsV1Options) { + const fnTag = "GetPrometheusExporterMetricsV1#constructor()"; + + Checks.truthy(opts, `${fnTag} options`); + Checks.truthy(opts.connector, `${fnTag} options.connector`); + + 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-ledger-connector-fabric/get-prometheus-exporter-metrics" + ].get["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-fabric/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()"; + this.log.debug(`POST ${this.getPath()}`); + + try { + let prometheusExporterObject: PrometheusExporter = new PrometheusExporter({pollingIntervalInMin: req.body.promExporter.prometheusExporterOptions.pollingIntervalInMin }) + prometheusExporterObject = Object.assign(prometheusExporterObject, req.body.promExporter) + const resBody = await this.opts.connector.getPrometheusExporterMetrics({ promExporter: prometheusExporterObject }); + res.status(200); + res.json(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-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts index 9e108ebaa9e..d18627c948f 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts @@ -39,6 +39,11 @@ import { RunTransactionEndpointV1, } from "./run-transaction/run-transaction-endpoint-v1"; +import { + IGetPrometheusExporterMetricsV1Options, + GetPrometheusExporterMetricsV1, +} from "./get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-v1" + import { ConnectionProfile, GatewayDiscoveryOptions, @@ -48,6 +53,8 @@ import { FabricContractInvocationType, RunTransactionRequest, RunTransactionResponse, + PrometheusExporterMetricsRequest, + PrometheusExporterMetricsResponse, } from "./generated/openapi/typescript-axios/index"; import { @@ -55,12 +62,19 @@ import { IDeployContractGoSourceEndpointV1Options, } from "./deploy-contract-go-source/deploy-contract-go-source-endpoint-v1"; +import { + PrometheusExporter, + PrometheusExporterOptions, +} from "./prometheus-exporter/prometheus-exporter"; +import { start } from "repl"; + export interface IPluginLedgerConnectorFabricOptions extends ICactusPluginOptions { logLevel?: LogLevelDesc; pluginRegistry: PluginRegistry; sshConfig: SshConfig; connectionProfile: ConnectionProfile; + prometheusExporter?: PrometheusExporter; discoveryOptions?: GatewayDiscoveryOptions; eventHandlerOptions?: GatewayEventHandlerOptions; } @@ -78,6 +92,7 @@ export class PluginLedgerConnectorFabric public static readonly CLASS_NAME = "PluginLedgerConnectorFabric"; private readonly instanceId: string; private readonly log: Logger; + public prometheusExporter: PrometheusExporter; public get className(): string { return PluginLedgerConnectorFabric.CLASS_NAME; @@ -89,11 +104,17 @@ export class PluginLedgerConnectorFabric Checks.truthy(opts.instanceId, `${fnTag} options.instanceId`); Checks.truthy(opts.pluginRegistry, `${fnTag} options.pluginRegistry`); Checks.truthy(opts.connectionProfile, `${fnTag} options.connectionProfile`); + this.prometheusExporter = + opts.prometheusExporter || + new PrometheusExporter({ pollingIntervalInMin: 1 }); + Checks.truthy( + this.prometheusExporter, + `${fnTag} options.prometheusExporter` + ); const level = this.opts.logLevel || "INFO"; const label = this.className; this.log = LoggerProvider.getOrCreate({ level, label }); - this.instanceId = opts.instanceId; } @@ -101,6 +122,21 @@ export class PluginLedgerConnectorFabric throw new Error("Method not implemented."); } + public getPrometheusExporter(): PrometheusExporter { + return this.prometheusExporter; + } + + public startPrometheusExporterMetricsCollection(): NodeJS.Timeout{ + return this.prometheusExporter.startMetricsCollection() + } + + public async getPrometheusExporterMetrics(req: PrometheusExporterMetricsRequest): Promise { + const { promExporter } = req; + const res: PrometheusExporterMetricsResponse = await (promExporter as PrometheusExporter).getPrometheusMetrics() + this.log.debug(`getPrometheusExporterMetrics() response: %o`, res); + return res + } + public getInstanceId(): string { return this.instanceId; } @@ -165,6 +201,16 @@ export class PluginLedgerConnectorFabric endpoints.push(endpoint); } + { + const opts: IGetPrometheusExporterMetricsV1Options = { + connector: this, + logLevel: this.opts.logLevel, + }; + const endpoint = new GetPrometheusExporterMetricsV1(opts); + endpoint.registerExpress(expressApp); + endpoints.push(endpoint); + } + const pkg = this.getPackageName(); log.info(`Installed web services for plugin ${pkg} OK`, { endpoints }); @@ -176,6 +222,8 @@ export class PluginLedgerConnectorFabric ): Promise { const fnTag = `${this.className}#transact()`; + const startTransactionTime = new Date(); + const { connectionProfile } = this.opts; const { keychainId, @@ -243,6 +291,12 @@ export class PluginLedgerConnectorFabric functionOutput: outUtf8, }; this.log.debug(`transact() response: %o`, res); + const endTransactionTime = new Date(); + this.prometheusExporter.addCurrentTransaction( + startTransactionTime, + endTransactionTime + ); + return res; } catch (ex) { this.log.error(`transact() crashed: `, ex); diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/data-fetcher.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/data-fetcher.ts new file mode 100644 index 00000000000..a6b5445925e --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/data-fetcher.ts @@ -0,0 +1,13 @@ +import { + Transactions +} from './response.type' + +import { + totalTxCount +} from './metrics' + +export async function collectMetrics(transactions: Transactions) { + totalTxCount + .labels('totalTxCount') + .set(transactions.length) +} \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/metrics.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/metrics.ts new file mode 100644 index 00000000000..0357b0d1af0 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/metrics.ts @@ -0,0 +1,7 @@ +import { Gauge } from 'prom-client' + +export const totalTxCount = new Gauge({ + name: "totalTxCount", + help: "Total transactions executed", + labelNames: ['type'] +}) \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/prometheus-exporter.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/prometheus-exporter.ts new file mode 100644 index 00000000000..7ecec3e424f --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/prometheus-exporter.ts @@ -0,0 +1,53 @@ +import express from 'express' +import promClient from 'prom-client' +import { Transaction,Transactions } from './response.type' +import { collectMetrics } from './data-fetcher' +import { PrometheusExporterMetricsResponse } from '../generated/openapi/typescript-axios'; + +export interface PrometheusExporterOptions { + pollingIntervalInMin?: number +} + +export interface PrometheusExporterResponse { + result: string +} + +export class PrometheusExporter { + + public readonly metricsPollingIntervalInMin: number; + public readonly transactions: Transactions = []; + + constructor(public readonly prometheusExporterOptions : PrometheusExporterOptions){ + this.metricsPollingIntervalInMin = prometheusExporterOptions.pollingIntervalInMin || 1; + } + + public addCurrentTransaction(startTimestamp: Date, endTimestamp: Date) { + this.transactions.push({ + startTime: startTimestamp, + endTime: endTimestamp + } as Transaction) + } + + public async getPrometheusMetrics(): Promise { + const result = { result: await promClient.register.getSingleMetricAsString('totalTxCount')} + return result + } + + public startMetricsCollection(): NodeJS.Timeout { + promClient.collectDefaultMetrics(); + + const pollTimeoutRefreshIntervalId = setInterval(() => { + collectMetrics(this.transactions) + }, this.metricsPollingIntervalInMin * 60 * 1000) + collectMetrics(this.transactions) + + const metricServer = express() + metricServer.get('/metrics', async (req, res) => { + res.send(await promClient.register.getSingleMetricAsString('totalTxCount')) + }) + metricServer.listen(9991, () => 1 + ) + + return pollTimeoutRefreshIntervalId + } +} \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/response.type.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/response.type.ts new file mode 100644 index 00000000000..15f8db28e49 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/prometheus-exporter/response.type.ts @@ -0,0 +1,6 @@ +export type Transaction = { + startTime: Date + endTime: Date +}; + +export type Transactions = Transaction[] \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v1-4-x/run-transaction-endpoint-v1.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v1-4-x/run-transaction-endpoint-v1.test.ts index 477ae724155..73109d95a3c 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v1-4-x/run-transaction-endpoint-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v1-4-x/run-transaction-endpoint-v1.test.ts @@ -100,6 +100,7 @@ test("runs tx on a Fabric v1.4.8 ledger", async (t: Test) => { }, }; const plugin = new PluginLedgerConnectorFabric(pluginOptions); + const pollTimeoutRefreshIntervalId = plugin.startPrometheusExporterMetricsCollection(); const expressApp = express(); expressApp.use(bodyParser.json({ limit: "250mb" })); @@ -173,6 +174,18 @@ test("runs tx on a Fabric v1.4.8 ledger", async (t: Test) => { t.ok(car277.Record.owner, `Car object has "Record"."owner" property OK`); t.equal(car277.Record.owner, carOwner, `Car has expected owner OK`); } - + await new Promise(resolve => setTimeout(resolve, 200000)); + { + const promExporter = plugin.getPrometheusExporter(); + const res = await apiClient.getPrometheusExporterMetricsV1({ + promExporter: promExporter + }) + const promMetricsOutput = '# HELP totalTxCount Total transactions executed\n# TYPE totalTxCount gauge\ntotalTxCount{type="totalTxCount"} 3' + t.ok(res) + t.ok(res.data) + t.equal(res.status,200) + t.equal(res.data.result,promMetricsOutput,"Total Transaction Count of 3 recorded as expected. RESULT OK") + } + clearInterval(pollTimeoutRefreshIntervalId) t.end(); }); diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts index 5952ea6ccf5..14fbfece088 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/run-transaction-endpoint-v1.test.ts @@ -100,6 +100,7 @@ test("runs tx on a Fabric v2.2.0 ledger", async (t: Test) => { }, }; const plugin = new PluginLedgerConnectorFabric(pluginOptions); + const pollTimeoutRefreshIntervalId = plugin.startPrometheusExporterMetricsCollection(); const expressApp = express(); expressApp.use(bodyParser.json({ limit: "250mb" })); @@ -174,5 +175,18 @@ test("runs tx on a Fabric v2.2.0 ledger", async (t: Test) => { t.equal(car277.Record.owner, carOwner, `Car has expected owner OK`); } + await new Promise(resolve => setTimeout(resolve, 200000)); + { + const promExporter = plugin.getPrometheusExporter(); + const res = await apiClient.getPrometheusExporterMetricsV1({ + promExporter: promExporter + }) + const promMetricsOutput = '# HELP totalTxCount Total transactions executed\n# TYPE totalTxCount gauge\ntotalTxCount{type="totalTxCount"} 3' + t.ok(res) + t.ok(res.data) + t.equal(res.status,200) + t.equal(res.data.result,promMetricsOutput,"Total Transaction Count of 3 recorded as expected. RESULT OK") + } + clearInterval(pollTimeoutRefreshIntervalId) t.end(); });