diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/VERSION b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/VERSION index 28cbf7c0aae..1a487e1a2e3 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/VERSION +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/.openapi-generator/VERSION @@ -1 +1 @@ -5.0.0 \ No newline at end of file +5.0.0-beta2 \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts index 4d188b2da35..55844bd813c 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -531,7 +531,6 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati if (configuration) { baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -540,23 +539,18 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati localVarHeaderParameter['Content-Type'] = 'application/json'; - const queryParameters = new URLSearchParams(localVarUrlObj.search); + const query = new URLSearchParams(localVarUrlObj.search); for (const key in localVarQueryParameter) { - queryParameters.set(key, localVarQueryParameter[key]); + query.set(key, localVarQueryParameter[key]); } for (const key in options.query) { - queryParameters.set(key, options.query[key]); + query.set(key, options.query[key]); } - localVarUrlObj.search = (new URLSearchParams(queryParameters)).toString(); + localVarUrlObj.search = (new URLSearchParams(query)).toString(); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - const nonString = typeof deployContractSolidityBytecodeV1Request !== 'string'; - const needsSerialization = nonString && configuration && configuration.isJsonMime - ? configuration.isJsonMime(localVarRequestOptions.headers['Content-Type']) - : nonString; - localVarRequestOptions.data = needsSerialization - ? JSON.stringify(deployContractSolidityBytecodeV1Request !== undefined ? deployContractSolidityBytecodeV1Request : {}) - : (deployContractSolidityBytecodeV1Request || ""); + const needsSerialization = (typeof deployContractSolidityBytecodeV1Request !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(deployContractSolidityBytecodeV1Request !== undefined ? deployContractSolidityBytecodeV1Request : {}) : (deployContractSolidityBytecodeV1Request || ""); return { url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, @@ -578,7 +572,6 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati if (configuration) { baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -587,23 +580,18 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati localVarHeaderParameter['Content-Type'] = 'application/json'; - const queryParameters = new URLSearchParams(localVarUrlObj.search); + const query = new URLSearchParams(localVarUrlObj.search); for (const key in localVarQueryParameter) { - queryParameters.set(key, localVarQueryParameter[key]); + query.set(key, localVarQueryParameter[key]); } for (const key in options.query) { - queryParameters.set(key, options.query[key]); + query.set(key, options.query[key]); } - localVarUrlObj.search = (new URLSearchParams(queryParameters)).toString(); + localVarUrlObj.search = (new URLSearchParams(query)).toString(); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - const nonString = typeof invokeContractV1Request !== 'string'; - const needsSerialization = nonString && configuration && configuration.isJsonMime - ? configuration.isJsonMime(localVarRequestOptions.headers['Content-Type']) - : nonString; - localVarRequestOptions.data = needsSerialization - ? JSON.stringify(invokeContractV1Request !== undefined ? invokeContractV1Request : {}) - : (invokeContractV1Request || ""); + const needsSerialization = (typeof invokeContractV1Request !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(invokeContractV1Request !== undefined ? invokeContractV1Request : {}) : (invokeContractV1Request || ""); return { url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, @@ -625,7 +613,6 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati if (configuration) { baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -634,23 +621,18 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati localVarHeaderParameter['Content-Type'] = 'application/json'; - const queryParameters = new URLSearchParams(localVarUrlObj.search); + const query = new URLSearchParams(localVarUrlObj.search); for (const key in localVarQueryParameter) { - queryParameters.set(key, localVarQueryParameter[key]); + query.set(key, localVarQueryParameter[key]); } for (const key in options.query) { - queryParameters.set(key, options.query[key]); + query.set(key, options.query[key]); } - localVarUrlObj.search = (new URLSearchParams(queryParameters)).toString(); + localVarUrlObj.search = (new URLSearchParams(query)).toString(); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - const nonString = typeof runTransactionRequest !== 'string'; - const needsSerialization = nonString && configuration && configuration.isJsonMime - ? configuration.isJsonMime(localVarRequestOptions.headers['Content-Type']) - : nonString; - localVarRequestOptions.data = needsSerialization - ? JSON.stringify(runTransactionRequest !== undefined ? runTransactionRequest : {}) - : (runTransactionRequest || ""); + const needsSerialization = (typeof runTransactionRequest !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(runTransactionRequest !== undefined ? runTransactionRequest : {}) : (runTransactionRequest || ""); return { url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, @@ -676,7 +658,6 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati if (configuration) { baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; @@ -685,23 +666,18 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati localVarHeaderParameter['Content-Type'] = 'application/json'; - const queryParameters = new URLSearchParams(localVarUrlObj.search); + const query = new URLSearchParams(localVarUrlObj.search); for (const key in localVarQueryParameter) { - queryParameters.set(key, localVarQueryParameter[key]); + query.set(key, localVarQueryParameter[key]); } for (const key in options.query) { - queryParameters.set(key, options.query[key]); + query.set(key, options.query[key]); } - localVarUrlObj.search = (new URLSearchParams(queryParameters)).toString(); + localVarUrlObj.search = (new URLSearchParams(query)).toString(); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - const nonString = typeof signTransactionRequest !== 'string'; - const needsSerialization = nonString && configuration && configuration.isJsonMime - ? configuration.isJsonMime(localVarRequestOptions.headers['Content-Type']) - : nonString; - localVarRequestOptions.data = needsSerialization - ? JSON.stringify(signTransactionRequest !== undefined ? signTransactionRequest : {}) - : (signTransactionRequest || ""); + const needsSerialization = (typeof signTransactionRequest !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; + localVarRequestOptions.data = needsSerialization ? JSON.stringify(signTransactionRequest !== undefined ? signTransactionRequest : {}) : (signTransactionRequest || ""); return { url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash, @@ -727,7 +703,7 @@ export const DefaultApiFp = function(configuration?: Configuration) { async apiV1BesuDeployContractSolidityBytecode(deployContractSolidityBytecodeV1Request?: DeployContractSolidityBytecodeV1Request, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await DefaultApiAxiosParamCreator(configuration).apiV1BesuDeployContractSolidityBytecode(deployContractSolidityBytecodeV1Request, options); return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { - const axiosRequestArgs = {...localVarAxiosArgs.options, url: (configuration?.basePath || basePath) + localVarAxiosArgs.url}; + const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; return axios.request(axiosRequestArgs); }; }, @@ -741,7 +717,7 @@ export const DefaultApiFp = function(configuration?: Configuration) { async apiV1BesuInvokeContract(invokeContractV1Request?: InvokeContractV1Request, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await DefaultApiAxiosParamCreator(configuration).apiV1BesuInvokeContract(invokeContractV1Request, options); return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { - const axiosRequestArgs = {...localVarAxiosArgs.options, url: (configuration?.basePath || basePath) + localVarAxiosArgs.url}; + const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; return axios.request(axiosRequestArgs); }; }, @@ -755,7 +731,7 @@ export const DefaultApiFp = function(configuration?: Configuration) { async apiV1BesuRunTransaction(runTransactionRequest?: RunTransactionRequest, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await DefaultApiAxiosParamCreator(configuration).apiV1BesuRunTransaction(runTransactionRequest, options); return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { - const axiosRequestArgs = {...localVarAxiosArgs.options, url: (configuration?.basePath || basePath) + localVarAxiosArgs.url}; + const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; return axios.request(axiosRequestArgs); }; }, @@ -769,7 +745,7 @@ export const DefaultApiFp = function(configuration?: Configuration) { async signTransactionV1(signTransactionRequest: SignTransactionRequest, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await DefaultApiAxiosParamCreator(configuration).signTransactionV1(signTransactionRequest, options); return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { - const axiosRequestArgs = {...localVarAxiosArgs.options, url: (configuration?.basePath || basePath) + localVarAxiosArgs.url}; + const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url}; return axios.request(axiosRequestArgs); }; }, diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/configuration.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/configuration.ts index 64dbb46b9b2..15244eaa832 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/configuration.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/configuration.ts @@ -20,7 +20,6 @@ export interface ConfigurationParameters { accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); basePath?: string; baseOptions?: any; - formDataCtor?: new () => any; } export class Configuration { @@ -65,14 +64,6 @@ export class Configuration { * @memberof Configuration */ baseOptions?: any; - /** - * The FormData constructor that will be used to create multipart form data - * requests. You can inject this here so that execution environments that - * do not support the FormData class can still run the generated client. - * - * @type {new () => FormData} - */ - formDataCtor?: new () => any; constructor(param: ConfigurationParameters = {}) { this.apiKey = param.apiKey; @@ -81,21 +72,5 @@ export class Configuration { this.accessToken = param.accessToken; this.basePath = param.basePath; this.baseOptions = param.baseOptions; - this.formDataCtor = param.formDataCtor; - } - - /** - * Check if the given MIME is a JSON MIME. - * JSON MIME examples: - * application/json - * application/json; charset=UTF8 - * APPLICATION/JSON - * application/vnd.company+json - * @param mime - MIME (Multipurpose Internet Mail Extensions) - * @return True if the given MIME is JSON, false otherwise. - */ - public isJsonMime(mime: string): boolean { - const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); - return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); } } diff --git a/packages/cactus-plugin-ledger-connector-quorum/README.md b/packages/cactus-plugin-ledger-connector-quorum/README.md index 765b805cf85..137dc78510d 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/README.md +++ b/packages/cactus-plugin-ledger-connector-quorum/README.md @@ -1,9 +1,140 @@ # `@hyperledger/cactus-plugin-ledger-connector-quorum` -> TODO: description +This plugin provides `Cactus` a way to interact with Quorum networks. Using this we can perform: +* Deploy Smart-contracts through bytecode. +* Build and sign transactions using different keystores. +* Invoke smart-contract functions that we have deployed on the network. + +## Summary + + - [Getting Started](#getting-started) + - [Usage](#usage) + - [Prometheus Exporter](#prometheus-exporter) + - [Runing the tests](#running-the-tests) + - [Contributing](#contributing) + - [License](#license) + - [Acknowledgments](#acknowledgments) + +## Getting Started + +Clone the git repository on your local machine. Follow these instructions that will get you a copy of the project up and running on +your local machine for development and testing purposes. + +### Prerequisites + +In the root of the project to install the dependencies execute the command: +```sh +npm run comfigure +``` + +### Compiling + +In the projects root folder, run this command to compile the plugin and create the dist directory: +```sh +npm run tsc +``` ## Usage +To use this import public-api and create new **PluginLedgerConnectorQuorum**. +```typescript + const connector: PluginLedgerConnectorQuorum = new PluginLedgerConnectorQuorum({ + instanceId: uuidV4(), + rpcApiHttpHost, + pluginRegistry: new PluginRegistry(), + }); ``` -// TODO: DEMONSTRATE API +You can make calls through the connector to the plugin API: + +```typescript +async invokeContract(req: InvokeContractV1Request):Promise; +async transactSigned(rawTransaction: string): Promise; +async transactPrivateKey(req: RunTransactionRequest): Promise; +async transactCactusKeychainRef(req: RunTransactionRequest):Promise; +async deployContract(req: DeployContractSolidityBytecodeV1Request):Promise; +async signTransaction(req: SignTransactionRequest):Promise>; +``` + +Call example to deploy a contract: +```typescript +const deployOut = await connector.deployContract({ + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: "", + type: Web3SigningCredentialType.GETHKEYCHAINPASSWORD, + }, + bytecode: ContractJson.bytecode, + gas: 1000000, +}); ``` +The field "type" can have the following values: +```typescript +enum Web3SigningCredentialType { + CACTUSKEYCHAINREF = 'CACTUS_KEYCHAIN_REF', + GETHKEYCHAINPASSWORD = 'GETH_KEYCHAIN_PASSWORD', + PRIVATEKEYHEX = 'PRIVATE_KEY_HEX', + NONE = 'NONE' +} +``` +> Extensive documentation and examples in the [readthedocs](https://readthedocs.org/projects/hyperledger-cactus/) (WIP) + +## Prometheus Exporter + +This class creates a prometheus exporter, which scraps the transactions (total transaction count) for the use cases incorporating the use of Quorum connector plugin. + +### Prometheus Exporter Usage +The prometheus exporter object is initialized in the `PluginLedgerConnectorQuorum` class constructor itself, so instantiating the object of the `PluginLedgerConnectorQuorum` class, gives access to the exporter object. +You can also initialize the prometheus exporter object seperately and then pass it to the `IPluginLedgerConnectorQuorumOptions` interface for `PluginLedgerConnectoQuorum` constructor. + +`getPrometheusExporterMetricsEndpointV1` function returns the prometheus exporter metrics, currently displaying the total transaction count, which currently increments everytime the `transact()` method of the `PluginLedgerConnectorQuorum` 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: 'quorum_ledger_connector_exporter' + metrics_path: api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/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-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/deploy-contract-from-json.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-quorum/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_quorum_total_tx_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. + +## Running the tests + +To check that all has been installed correctly and that the pugin has no errors, there are two options to run the tests: + +* Run this command at the project's root: +```sh +npm run test:plugin-ledger-connector-quorum +``` + +## Contributing + +We welcome contributions to Hyperledger Cactus in many forms, and there’s always plenty to do! + +Please review [CONTIRBUTING.md](../../CONTRIBUTING.md) to get started. + +## License + +This distribution is published under the Apache License Version 2.0 found in the [LICENSE](../../LICENSE) file. + +## Acknowledgments +``` \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-quorum/package-lock.json b/packages/cactus-plugin-ledger-connector-quorum/package-lock.json index e9728671d23..406143d4095 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/package-lock.json +++ b/packages/cactus-plugin-ledger-connector-quorum/package-lock.json @@ -355,6 +355,11 @@ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.1.tgz", "integrity": "sha512-IdZR9mh6ahOBv/hYGiXyVuyCetmGJhtYkqLBpTStdhEGjegpPlUawydyaF3pbIOFynJTpllEs+NP+CS9jKFLjA==" }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, "blakejs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz", @@ -1764,6 +1769,14 @@ "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" }, + "prom-client": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-13.1.0.tgz", + "integrity": "sha512-jT9VccZCWrJWXdyEtQddCDszYsiuWj5T0ekrPszi/WEegj3IZy6Mm09iOOVM86A4IKMWq8hZkT2dD9MaSe+sng==", + "requires": { + "tdigest": "^0.1.1" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -2185,6 +2198,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" + } + }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", diff --git a/packages/cactus-plugin-ledger-connector-quorum/package.json b/packages/cactus-plugin-ledger-connector-quorum/package.json index 51d18a4d7ea..bb67c4fba82 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/package.json +++ b/packages/cactus-plugin-ledger-connector-quorum/package.json @@ -33,7 +33,10 @@ "ignore": [ "src/**/generated/*" ], - "extensions": ["ts", "json"], + "extensions": [ + "ts", + "json" + ], "quiet": true, "verbose": false, "runOnChangeOnly": true @@ -87,6 +90,7 @@ "express": "4.17.1", "joi": "14.3.1", "openapi-types": "7.0.1", + "prom-client": "13.1.0", "typescript-optional": "2.0.1", "web3": "1.2.7", "web3-core-method": "1.3.0", diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json index 40e9a54e8f1..8edb53d1bba 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json @@ -527,6 +527,10 @@ }, "callOutput": {} } + }, + "PrometheusExporterMetricsResponse": { + "type": "string", + "nullable": false } } }, @@ -614,6 +618,31 @@ } } } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/get-prometheus-exporter-metrics": { + "get": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "get", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/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-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts index 7905a9414fb..1c532a68ef8 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -615,6 +615,42 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati const needsSerialization = (typeof runTransactionRequest !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json'; localVarRequestOptions.data = needsSerialization ? JSON.stringify(runTransactionRequest !== undefined ? runTransactionRequest : {}) : (runTransactionRequest || ""); + 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-ledger-connector-quorum/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]); + } + 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, @@ -671,6 +707,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); + }; + }, } }; @@ -710,6 +759,15 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa apiV1QuorumRunTransaction(runTransactionRequest?: RunTransactionRequest, options?: any): AxiosPromise { return DefaultApiFp(configuration).apiV1QuorumRunTransaction(runTransactionRequest, 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)); + }, }; }; @@ -755,6 +813,17 @@ export class DefaultApi extends BaseAPI { public apiV1QuorumRunTransaction(runTransactionRequest?: RunTransactionRequest, options?: any) { return DefaultApiFp(this.configuration).apiV1QuorumRunTransaction(runTransactionRequest, 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-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts index 63805ff6864..91e9115d929 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts @@ -49,10 +49,17 @@ import { RunTransactionEndpoint } from "./web-services/run-transaction-endpoint" import { InvokeContractEndpoint } from "./web-services/invoke-contract-endpoint"; import { isWeb3SigningCredentialNone } from "./model-type-guards"; +import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; +import { + GetPrometheusExporterMetricsEndpointV1, + IGetPrometheusExporterMetricsEndpointV1Options, +} from "./web-services/get-prometheus-exporter-metrics-endpoint-v1"; + export interface IPluginLedgerConnectorQuorumOptions extends ICactusPluginOptions { rpcApiHttpHost: string; logLevel?: LogLevelDesc; + prometheusExporter?: PrometheusExporter; pluginRegistry: PluginRegistry; } @@ -67,6 +74,7 @@ export class PluginLedgerConnectorQuorum ICactusPlugin, IPluginWebService { private readonly pluginRegistry: PluginRegistry; + public prometheusExporter: PrometheusExporter; private readonly instanceId: string; private readonly log: Logger; private readonly web3: Web3; @@ -95,6 +103,26 @@ export class PluginLedgerConnectorQuorum this.web3 = new Web3(web3Provider); this.instanceId = options.instanceId; this.pluginRegistry = options.pluginRegistry; + + this.prometheusExporter = + options.prometheusExporter || + new PrometheusExporter({ pollingIntervalInMin: 1 }); + Checks.truthy( + this.prometheusExporter, + `${fnTag} options.prometheusExporter`, + ); + + this.prometheusExporter.startMetricsCollection(); + } + + 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 getInstanceId(): string { @@ -141,6 +169,15 @@ export class PluginLedgerConnectorQuorum endpoint.registerExpress(expressApp); endpoints.push(endpoint); } + { + const opts: IGetPrometheusExporterMetricsEndpointV1Options = { + connector: this, + logLevel: this.options.logLevel, + }; + const endpoint = new GetPrometheusExporterMetricsEndpointV1(opts); + endpoint.registerExpress(expressApp); + endpoints.push(endpoint); + } return endpoints; } @@ -258,6 +295,7 @@ export class PluginLedgerConnectorQuorum this.log.debug(`${fnTag} Web3 sendSignedTransaction failed`, receipt); throw receipt; } else { + this.prometheusExporter.addCurrentTransaction(); return { transactionReceipt: receipt }; } } diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/data.fetcher.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/data.fetcher.ts new file mode 100644 index 00000000000..aa254f4fc17 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/data.fetcher.ts @@ -0,0 +1,7 @@ +import { Transactions } from "./response.type"; + +import { totalTxCount } from "./metrics"; + +export async function collectMetrics(transactions: Transactions) { + totalTxCount.labels("cactus_quorum_total_tx_count").set(transactions.counter); +} diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/metrics.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/metrics.ts new file mode 100644 index 00000000000..43278478189 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/metrics.ts @@ -0,0 +1,7 @@ +import { Gauge } from "prom-client"; + +export const totalTxCount = new Gauge({ + name: "cactus_quorum_total_tx_count", + help: "Total transactions executed", + labelNames: ["type"], +}); diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/prometheus-exporter.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/prometheus-exporter.ts new file mode 100644 index 00000000000..56c766cfe73 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/prometheus-exporter.ts @@ -0,0 +1,41 @@ +import promClient from "prom-client"; +import { Transactions } from "./response.type"; +import { totalTxCount } from "./metrics"; + +export const K_CACTUS_QUORUM_TOTAL_TX_COUNT = "cactus_quorum_total_tx_count"; + +export interface IPrometheusExporterOptions { + pollingIntervalInMin?: number; +} + +export class PrometheusExporter { + public readonly metricsPollingIntervalInMin: number; + public readonly transactions: Transactions = { counter: 0 }; + + constructor( + public readonly prometheusExporterOptions: IPrometheusExporterOptions, + ) { + this.metricsPollingIntervalInMin = + prometheusExporterOptions.pollingIntervalInMin || 1; + } + + public addCurrentTransaction(): void { + this.transactions.counter++; + totalTxCount + .labels(K_CACTUS_QUORUM_TOTAL_TX_COUNT) + .set(this.transactions.counter); + } + + public async getPrometheusMetrics(): Promise { + const result = await promClient.register.getSingleMetricAsString( + K_CACTUS_QUORUM_TOTAL_TX_COUNT, + ); + return result; + } + + public startMetricsCollection(): void { + const Registry = promClient.Registry; + const register = new Registry(); + promClient.collectDefaultMetrics({ register }); + } +} diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/response.type.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/response.type.ts new file mode 100644 index 00000000000..3f1bc7f4911 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/prometheus-exporter/response.type.ts @@ -0,0 +1,3 @@ +export type Transactions = { + counter: number; +}; diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/get-prometheus-exporter-metrics-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/get-prometheus-exporter-metrics-endpoint-v1.ts new file mode 100644 index 00000000000..54f1ad026e2 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/get-prometheus-exporter-metrics-endpoint-v1.ts @@ -0,0 +1,80 @@ +import { Express, Request, Response } from "express"; + +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; + +import OAS from "../../json/openapi.json"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, +} from "@hyperledger/cactus-core-api"; + +import { + LogLevelDesc, + Logger, + LoggerProvider, + Checks, +} from "@hyperledger/cactus-common"; + +import { PluginLedgerConnectorQuorum } from "../plugin-ledger-connector-quorum"; + +export interface IGetPrometheusExporterMetricsEndpointV1Options { + connector: PluginLedgerConnectorQuorum; + logLevel?: LogLevelDesc; +} + +export class GetPrometheusExporterMetricsEndpointV1 + implements IWebServiceEndpoint { + private readonly log: Logger; + + constructor( + public readonly options: IGetPrometheusExporterMetricsEndpointV1Options, + ) { + const fnTag = "GetPrometheusExporterMetricsEndpointV1#constructor()"; + + Checks.truthy(options, `${fnTag} options`); + Checks.truthy(options.connector, `${fnTag} options.connector`); + + const label = "get-prometheus-exporter-metrics-endpoint"; + const level = options.logLevel || "INFO"; + this.log = LoggerProvider.getOrCreate({ label, level }); + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + getPath(): string { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/get-prometheus-exporter-metrics" + ].get["x-hyperledger-cactus"].http.path; + } + + getVerbLowerCase(): string { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/get-prometheus-exporter-metrics" + ].get["x-hyperledger-cactus"].http.verbLowerCase; + } + + 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.options.connector.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-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/deploy-contract-from-json.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/deploy-contract-from-json.test.ts index 91a567151d6..bb4d3b800bb 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/deploy-contract-from-json.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/deploy-contract-from-json.test.ts @@ -2,7 +2,11 @@ import test, { Test } from "tape"; import Web3 from "web3"; import { v4 as uuidV4 } from "uuid"; -import { LogLevelDesc } from "@hyperledger/cactus-common"; +import { + LogLevelDesc, + IListenOptions, + Servers, +} from "@hyperledger/cactus-common"; import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; @@ -13,6 +17,7 @@ import { PluginLedgerConnectorQuorum, Web3SigningCredentialCactusKeychainRef, Web3SigningCredentialType, + DefaultApi as QuorumApi, } from "../../../../../main/typescript/public-api"; import { @@ -22,6 +27,11 @@ import { } from "@hyperledger/cactus-test-tooling"; import { PluginRegistry } from "@hyperledger/cactus-core"; +import express from "express"; +import bodyParser from "body-parser"; +import http from "http"; +import { AddressInfo } from "net"; + const logLevel: LogLevelDesc = "INFO"; test("Quorum Ledger Connector Plugin", async (t: Test) => { @@ -73,6 +83,25 @@ test("Quorum Ledger Connector Plugin", async (t: Test) => { }, ); + 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-ledger-connector-quorum/get-prometheus-exporter-metrics`, + ); + const apiClient = new QuorumApi({ basePath: apiHost }); + + await connector.installWebServices(expressApp); + await connector.transact({ web3SigningCredential: { ethAccount: firstHighNetWorthAccount, @@ -315,5 +344,21 @@ test("Quorum Ledger Connector Plugin", async (t: Test) => { t2.end(); }); + test("get prometheus exporter metrics", async (t2: Test) => { + const res = await apiClient.getPrometheusExporterMetricsV1(); + const promMetricsOutput = + "# HELP cactus_quorum_total_tx_count Total transactions executed\n" + + "# TYPE cactus_quorum_total_tx_count gauge\n" + + 'cactus_quorum_total_tx_count{type="cactus_quorum_total_tx_count"} 5'; + t2.ok(res); + t2.ok(res.data); + t2.equal(res.status, 200); + t2.true( + res.data.includes(promMetricsOutput), + "Total Transaction Count of 5 recorded as expected. RESULT OK.", + ); + t2.end(); + }); + t.end(); });