From f74aea084d5c72a55b67b03b8b1a7a88bd12dcc0 Mon Sep 17 00:00:00 2001 From: matias martinez Date: Sun, 12 Nov 2023 23:53:11 -0300 Subject: [PATCH] adding basic metrics --- blockchain-watcher/README.md | 17 +- .../config/custom-environment-variables.json | 1 + blockchain-watcher/config/default.json | 1 + blockchain-watcher/package-lock.json | 350 ++++++------------ blockchain-watcher/package.json | 1 + .../src/domain/actions/PollEvmLogs.ts | 31 +- blockchain-watcher/src/domain/repositories.ts | 6 + .../src/infrastructure/RepositoriesBuilder.ts | 9 + .../src/infrastructure/config.ts | 2 + .../src/infrastructure/environment.ts | 3 +- .../repositories/PromStatRepository.ts | 55 +++ .../src/infrastructure/repositories/index.ts | 1 + .../infrastructure/rpc/HealthController.ts | 13 + .../src/infrastructure/rpc/Server.ts | 40 ++ blockchain-watcher/src/start.ts | 44 ++- .../test/domain/PollEvmLogs.test.ts | 21 +- deploy/blockchain-watcher/stateful-set.yaml | 21 +- 17 files changed, 348 insertions(+), 268 deletions(-) create mode 100644 blockchain-watcher/src/infrastructure/repositories/PromStatRepository.ts create mode 100644 blockchain-watcher/src/infrastructure/rpc/HealthController.ts create mode 100644 blockchain-watcher/src/infrastructure/rpc/Server.ts diff --git a/blockchain-watcher/README.md b/blockchain-watcher/README.md index 0ed034f6..30ee1b06 100644 --- a/blockchain-watcher/README.md +++ b/blockchain-watcher/README.md @@ -15,18 +15,15 @@ This process is meant to be deployed as a docker container. The dockerfile is lo ## Configuration -Look at the provided example .env file for configuration options. +Configuration is loaded from files in `config` directory. +There is a default file, and then a file for each environment. The environment is set by the NODE_ENV environment variable. +If NODE_ENV is not set, the default file is used. -A brief explanation of the environment variables: -RPCS : A json object of rpcs to connect to. The key is the wormhole chain ID, and the value is the rpc url. If an RPC is provided here, it is always used, regardless of the USE_DEFAULT_RPCS variable. -ENVIRONMENT : DEVNET , TESTNET, or MAINNET these are Network objects from the wormhole SDK -USE_DEFAULT_RPCS : if true, the RPCS according to the ENVIRONMENT will be used. If false, an exception is thrown if the RPCS environment variable is not set for a given chain. RPCS +Some values may be overriden by using environment variables. See `config/custom-environment-variables.json` for a list of these variables. -Contract overrides take the form of : -ETHEREUM_DEVNET_WORMHOLE_RELAYER_ADDRESS=0x53855d4b64E9A3CF59A84bc768adA716B5536BC5 -BSC_DEVNET_WORMHOLE_RELAYER_ADDRESS=0x53855d4b64E9A3CF59A84bc768adA716B5536BC5 - -If the contract override is supplied, it will be used, otherwise the default contract addresses from the wormhole SDK will be used instead. +```bash +$ NODE_ENV=staging LOG_LEVEL=debug npm run dev +``` ## Usage & Modification diff --git a/blockchain-watcher/config/custom-environment-variables.json b/blockchain-watcher/config/custom-environment-variables.json index 9e99d703..52f7d480 100644 --- a/blockchain-watcher/config/custom-environment-variables.json +++ b/blockchain-watcher/config/custom-environment-variables.json @@ -1,5 +1,6 @@ { "environment": "BLOCKCHAIN_ENV", + "port": "PORT", "logLevel": "LOG_LEVEL", "dryRun": "DRY_RUN_ENABLED", "sns": { diff --git a/blockchain-watcher/config/default.json b/blockchain-watcher/config/default.json index 37179a0b..8cb2816b 100644 --- a/blockchain-watcher/config/default.json +++ b/blockchain-watcher/config/default.json @@ -1,5 +1,6 @@ { "environment": "testnet", + "port": 9090, "logLevel": "debug", "dryRun": true, "supportedChains": ["ethereum"], diff --git a/blockchain-watcher/package-lock.json b/blockchain-watcher/package-lock.json index 7a7010b8..f6069c63 100644 --- a/blockchain-watcher/package-lock.json +++ b/blockchain-watcher/package-lock.json @@ -17,6 +17,7 @@ "config": "^3.3.9", "dotenv": "^16.3.1", "ethers": "^5", + "prom-client": "^15.0.0", "uuid": "^9.0.1", "winston": "3.8.2" }, @@ -3368,33 +3369,6 @@ "node": ">=10" } }, - "node_modules/@jest/reporters/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@jest/reporters/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@jest/reporters/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3407,12 +3381,6 @@ "node": ">=8" } }, - "node_modules/@jest/reporters/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -3829,6 +3797,14 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@project-serum/anchor": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.25.0.tgz", @@ -5603,6 +5579,11 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "node_modules/bip32": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.6.tgz", @@ -8921,33 +8902,6 @@ "node": ">=8" } }, - "node_modules/jest-snapshot/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-snapshot/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -8960,12 +8914,6 @@ "node": ">=8" } }, - "node_modules/jest-snapshot/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -9577,39 +9525,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-dir/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -10173,6 +10088,18 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "optional": true }, + "node_modules/prom-client": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.0.0.tgz", + "integrity": "sha512-UocpgIrKyA2TKLVZDSfm8rGkL13C19YrQBAiG3xo3aDFWcHedxRxI3z+cIcucoxpSO0h5lff5iv/SXoxyeopeA==", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -10522,6 +10449,39 @@ "node": ">=10.0.0" } }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -10862,6 +10822,14 @@ "node": ">=0.10" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -11074,39 +11042,6 @@ } } }, - "node_modules/ts-jest/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ts-jest/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/ts-jest/node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -14094,24 +14029,6 @@ "semver": "^7.5.4" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -14120,12 +14037,6 @@ "requires": { "has-flag": "^4.0.0" } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } }, @@ -14471,6 +14382,11 @@ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz", "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" }, + "@opentelemetry/api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.7.0.tgz", + "integrity": "sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw==" + }, "@project-serum/anchor": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@project-serum/anchor/-/anchor-0.25.0.tgz", @@ -15960,6 +15876,11 @@ "file-uri-to-path": "1.0.0" } }, + "bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "bip32": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.6.tgz", @@ -18478,24 +18399,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -18504,12 +18407,6 @@ "requires": { "has-flag": "^4.0.0" } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } }, @@ -18982,32 +18879,6 @@ "dev": true, "requires": { "semver": "^7.5.3" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } } }, "make-error": { @@ -19446,6 +19317,15 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "optional": true }, + "prom-client": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.0.0.tgz", + "integrity": "sha512-UocpgIrKyA2TKLVZDSfm8rGkL13C19YrQBAiG3xo3aDFWcHedxRxI3z+cIcucoxpSO0h5lff5iv/SXoxyeopeA==", + "requires": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + } + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -19693,6 +19573,32 @@ "node-gyp-build": "^4.2.0" } }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -19953,6 +19859,14 @@ "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", "optional": true }, + "tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "requires": { + "bintrees": "1.0.2" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -20120,30 +20034,6 @@ "yargs-parser": "^21.0.1" }, "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", diff --git a/blockchain-watcher/package.json b/blockchain-watcher/package.json index a23b4b28..706d8f71 100644 --- a/blockchain-watcher/package.json +++ b/blockchain-watcher/package.json @@ -22,6 +22,7 @@ "config": "^3.3.9", "dotenv": "^16.3.1", "ethers": "^5", + "prom-client": "^15.0.0", "uuid": "^9.0.1", "winston": "3.8.2" }, diff --git a/blockchain-watcher/src/domain/actions/PollEvmLogs.ts b/blockchain-watcher/src/domain/actions/PollEvmLogs.ts index 4c97e9f6..0f69ba9f 100644 --- a/blockchain-watcher/src/domain/actions/PollEvmLogs.ts +++ b/blockchain-watcher/src/domain/actions/PollEvmLogs.ts @@ -1,5 +1,5 @@ import { EvmLog } from "../entities"; -import { EvmBlockRepository, MetadataRepository } from "../repositories"; +import { EvmBlockRepository, MetadataRepository, StatRepository } from "../repositories"; import { setTimeout } from "timers/promises"; import winston from "winston"; @@ -10,21 +10,26 @@ let ref: any; * PollEvmLogs is an action that watches for new blocks and extracts logs from them. */ export class PollEvmLogs { + private readonly logger: winston.Logger = winston.child({ module: "PollEvmLogs" }); + private readonly blockRepo: EvmBlockRepository; private readonly metadataRepo: MetadataRepository; + private readonly statsRepository: StatRepository; + private cfg: PollEvmLogsConfig; + private latestBlockHeight?: bigint; private blockHeightCursor?: bigint; - private cfg: PollEvmLogsConfig; private started: boolean = false; - private readonly logger: winston.Logger = winston.child({ module: "PollEvmLogs" }); constructor( blockRepo: EvmBlockRepository, metadataRepo: MetadataRepository, + statsRepository: StatRepository, cfg: PollEvmLogsConfig ) { this.blockRepo = blockRepo; this.metadataRepo = metadataRepo; + this.statsRepository = statsRepository; this.cfg = cfg; } @@ -40,6 +45,7 @@ export class PollEvmLogs { private async watch(handlers: ((logs: EvmLog[]) => Promise)[]): Promise { while (this.started) { + this.report(); if (this.cfg.hasFinished(this.blockHeightCursor)) { this.logger.info( `PollEvmLogs: (${this.cfg.id}) Finished processing all blocks from ${this.cfg.fromBlock} to ${this.cfg.toBlock}` @@ -115,13 +121,21 @@ export class PollEvmLogs { return { fromBlock, toBlock }; } + private report(): void { + const labels = { + job: this.cfg.id, + chain: this.cfg.chain ?? "", + commitment: this.cfg.getCommitment(), + }; + this.statsRepository.count("job_execution", labels); + this.statsRepository.measure("block_height", this.latestBlockHeight ?? 0n, labels); + this.statsRepository.measure("block_cursor", this.blockHeightCursor ?? 0n, labels); + } + public async stop(): Promise { clearTimeout(ref); this.started = false; } - - // TODO: schedule getting latest block height in chain or use the value from poll to keep metrics updated - // this.latestBlockHeight = await this.blockRepo.getBlockHeight(this.commitment); } export type PollEvmLogsMetadata = { @@ -137,6 +151,7 @@ export interface PollEvmLogsConfigProps { addresses: string[]; topics: string[]; id?: string; + chain?: string; } export class PollEvmLogsConfig { @@ -190,6 +205,10 @@ export class PollEvmLogsConfig { return this.props.id ?? ID; } + public get chain() { + return this.props.chain; + } + static fromBlock(fromBlock: bigint) { const cfg = new PollEvmLogsConfig(); cfg.props.fromBlock = fromBlock; diff --git a/blockchain-watcher/src/domain/repositories.ts b/blockchain-watcher/src/domain/repositories.ts index fe2b044a..f5b034af 100644 --- a/blockchain-watcher/src/domain/repositories.ts +++ b/blockchain-watcher/src/domain/repositories.ts @@ -10,3 +10,9 @@ export interface MetadataRepository { get(id: string): Promise; save(id: string, metadata: Metadata): Promise; } + +export interface StatRepository { + count(id: string, labels: Record): void; + measure(id: string, value: bigint, labels: Record): void; + report: () => Promise; +} diff --git a/blockchain-watcher/src/infrastructure/RepositoriesBuilder.ts b/blockchain-watcher/src/infrastructure/RepositoriesBuilder.ts index 33566aa6..1362d895 100644 --- a/blockchain-watcher/src/infrastructure/RepositoriesBuilder.ts +++ b/blockchain-watcher/src/infrastructure/RepositoriesBuilder.ts @@ -5,6 +5,7 @@ import { EvmJsonRPCBlockRepository, EvmJsonRPCBlockRepositoryCfg, FileMetadataRepo, + PromStatRepository, } from "./repositories"; import axios, { AxiosInstance } from "axios"; import axiosRateLimit from "axios-rate-limit"; @@ -25,6 +26,7 @@ export class RepositoriesBuilder { this.axiosInstance = this.createAxios(); this.repositories.set("sns", new SnsEventRepository(this.snsClient, this.cfg.sns)); + this.repositories.set("metrics", new PromStatRepository()); this.cfg.metadata?.dir && this.repositories.set("metadata", new FileMetadataRepo(this.cfg.metadata.dir)); @@ -63,6 +65,13 @@ export class RepositoriesBuilder { return repo; } + public getStatsRepository(): PromStatRepository { + const repo = this.repositories.get("metrics"); + if (!repo) throw new Error(`No PromStatRepository`); + + return repo; + } + public close(): void { this.snsClient?.destroy(); } diff --git a/blockchain-watcher/src/infrastructure/config.ts b/blockchain-watcher/src/infrastructure/config.ts index d6ebd8b4..b383a1d7 100644 --- a/blockchain-watcher/src/infrastructure/config.ts +++ b/blockchain-watcher/src/infrastructure/config.ts @@ -3,6 +3,7 @@ import { SnsConfig } from "./repositories/SnsEventRepository"; export type Config = { environment: "testnet" | "mainnet"; + port: number; logLevel: "debug" | "info" | "warn" | "error"; dryRun: boolean; sns: SnsConfig; @@ -28,6 +29,7 @@ export type PlatformConfig = { */ export const configuration = { environment: config.get("environment"), + port: config.get("port") ?? 9090, logLevel: config.get("logLevel")?.toLowerCase() ?? "info", dryRun: config.get("dryRun") === "true" ? true : false, sns: config.get("sns"), diff --git a/blockchain-watcher/src/infrastructure/environment.ts b/blockchain-watcher/src/infrastructure/environment.ts index d391f0e6..acd084f7 100644 --- a/blockchain-watcher/src/infrastructure/environment.ts +++ b/blockchain-watcher/src/infrastructure/environment.ts @@ -1,6 +1,5 @@ import { ChainId, ChainName, Network, toChainName } from "@certusone/wormhole-sdk"; import AbstractWatcher from "./watchers/AbstractWatcher"; -import { rootLogger } from "./log"; import winston from "winston"; import EvmWatcher from "./watchers/EvmWatcher"; import AbstractHandler from "./handlers/AbstractHandler"; @@ -126,7 +125,7 @@ export async function initializeEnvironment(configurationPath: string) { configuration, supportedChains, rpcs, - logger: rootLogger, + logger: winston.child({}), }; } diff --git a/blockchain-watcher/src/infrastructure/repositories/PromStatRepository.ts b/blockchain-watcher/src/infrastructure/repositories/PromStatRepository.ts new file mode 100644 index 00000000..9df2cdcc --- /dev/null +++ b/blockchain-watcher/src/infrastructure/repositories/PromStatRepository.ts @@ -0,0 +1,55 @@ +import prometheus from "prom-client"; +import { StatRepository } from "../../domain/repositories"; + +export class PromStatRepository implements StatRepository { + private readonly registry: prometheus.Registry; + private counters: Map> = new Map(); + private gauges: Map> = new Map(); + + constructor(registry?: prometheus.Registry) { + this.registry = registry ?? new prometheus.Registry(); + } + + public report() { + return this.registry.metrics(); + } + public count(id: string, labels: Record): void { + const counter = this.getCounter(id, labels); + counter.inc(labels); + } + + public measure(id: string, value: bigint, labels: Record): void { + const gauge = this.getGauge(id, labels); + gauge.set(labels, Number(value)); + } + + private getCounter(id: string, labels: Record): prometheus.Counter { + this.counters.get(id) ?? + this.counters.set( + id, + new prometheus.Counter({ + name: id, + help: id, + registers: [this.registry], + labelNames: Object.keys(labels), + }) + ); + + return this.counters.get(id) as prometheus.Counter; + } + + private getGauge(id: string, labels: Record): prometheus.Gauge { + this.gauges.get(id) ?? + this.gauges.set( + id, + new prometheus.Gauge({ + name: id, + help: id, + registers: [this.registry], + labelNames: Object.keys(labels), + }) + ); + + return this.gauges.get(id) as prometheus.Gauge; + } +} diff --git a/blockchain-watcher/src/infrastructure/repositories/index.ts b/blockchain-watcher/src/infrastructure/repositories/index.ts index 4deebebe..f79ba8d1 100644 --- a/blockchain-watcher/src/infrastructure/repositories/index.ts +++ b/blockchain-watcher/src/infrastructure/repositories/index.ts @@ -10,3 +10,4 @@ if (!("toJSON" in BigInt.prototype)) { export * from "./FileMetadataRepo"; export * from "./SnsEventRepository"; export * from "./EvmJsonRPCBlockRepository"; +export * from "./PromStatRepository"; diff --git a/blockchain-watcher/src/infrastructure/rpc/HealthController.ts b/blockchain-watcher/src/infrastructure/rpc/HealthController.ts new file mode 100644 index 00000000..ec28f12a --- /dev/null +++ b/blockchain-watcher/src/infrastructure/rpc/HealthController.ts @@ -0,0 +1,13 @@ +import { StatRepository } from "../../domain/repositories"; + +export class HealthController { + private readonly statsRepo: StatRepository; + + constructor(statsRepo: StatRepository) { + this.statsRepo = statsRepo; + } + + metrics = async () => { + return this.statsRepo.report(); + }; +} diff --git a/blockchain-watcher/src/infrastructure/rpc/Server.ts b/blockchain-watcher/src/infrastructure/rpc/Server.ts new file mode 100644 index 00000000..bbb2e606 --- /dev/null +++ b/blockchain-watcher/src/infrastructure/rpc/Server.ts @@ -0,0 +1,40 @@ +import http from "http"; +import url from "url"; +import { HealthController } from "./HealthController"; +import log from "../log"; + +export class WebServer { + private server: http.Server; + private port: number; + + constructor(port: number, healthController: HealthController) { + this.port = port; + this.server = http.createServer(async (req, res) => { + const route = url.parse(req.url ?? "").pathname; + + if (route === "/metrics") { + // Return all metrics the Prometheus exposition format + res.setHeader("Content-Type", "text/plain"); + res.end(await healthController.metrics()); + } + + if (route === "/health") { + res.end("OK"); + } + + res.statusCode = 404; + res.end(); + }); + this.start(); + } + + start() { + this.server.listen(this.port, () => { + log.info(`Server started on port 8080`); + }); + } + + stop() { + this.server.close(); + } +} diff --git a/blockchain-watcher/src/start.ts b/blockchain-watcher/src/start.ts index a8b600c9..d44d433f 100644 --- a/blockchain-watcher/src/start.ts +++ b/blockchain-watcher/src/start.ts @@ -4,14 +4,37 @@ import { configuration } from "./infrastructure/config"; import { evmLogMessagePublishedMapper } from "./infrastructure/mappers/evmLogMessagePublishedMapper"; import { RepositoriesBuilder } from "./infrastructure/RepositoriesBuilder"; import log from "./infrastructure/log"; +import { WebServer } from "./infrastructure/rpc/Server"; +import { HealthController } from "./infrastructure/rpc/HealthController"; let repos: RepositoriesBuilder; +let server: WebServer; async function run(): Promise { log.info(`Starting: dryRunEnabled -> ${configuration.dryRun}`); repos = new RepositoriesBuilder(configuration); + await startServer(repos); + await startJobs(repos); + + // Just keep this running until killed + setInterval(() => { + log.info("Still running"); + }, 20_000); + + log.info("Started"); + + // Handle shutdown + process.on("SIGINT", handleShutdown); + process.on("SIGTERM", handleShutdown); +} + +const startServer = async (repos: RepositoriesBuilder) => { + server = new WebServer(configuration.port, new HealthController(repos.getStatsRepository())); +}; + +const startJobs = async (repos: RepositoriesBuilder) => { /** Job definition is hardcoded, but should be loaded from cfg or a data store soon enough */ const jobs = [ { @@ -21,11 +44,11 @@ async function run(): Promise { action: "PollEvmLogs", config: { fromBlock: 10012499n, - // toBlock: 10012999n, blockBatchSize: 100, commitment: "latest", interval: 15_000, addresses: ["0x706abc4E45D419950511e474C7B9Ed348A4a716c"], + chain: "ethereum", topics: [], }, }, @@ -49,6 +72,7 @@ async function run(): Promise { const pollEvmLogs = new PollEvmLogs( repos.getEvmBlockRepository("ethereum"), repos.getMetadataRepository(), + repos.getStatsRepository(), new PollEvmLogsConfig({ ...jobs[0].source.config, id: jobs[0].id }) ); @@ -72,25 +96,11 @@ async function run(): Promise { ); pollEvmLogs.start([handleEvmLogs.handle.bind(handleEvmLogs)]); - - // Just keep this running until killed - setInterval(() => { - log.info("Still running"); - }, 20_000); - - log.info("Started"); - - // Handle shutdown - process.on("SIGINT", handleShutdown); - process.on("SIGTERM", handleShutdown); -} +}; const handleShutdown = async () => { try { - await Promise.allSettled([ - repos.close(), - // call stop() on all the things - ]); + await Promise.allSettled([repos.close(), server.stop()]); process.exit(); } catch (error: unknown) { diff --git a/blockchain-watcher/test/domain/PollEvmLogs.test.ts b/blockchain-watcher/test/domain/PollEvmLogs.test.ts index 894dd8e1..d3b3bc54 100644 --- a/blockchain-watcher/test/domain/PollEvmLogs.test.ts +++ b/blockchain-watcher/test/domain/PollEvmLogs.test.ts @@ -5,7 +5,11 @@ import { PollEvmLogs, PollEvmLogsConfig, } from "../../src/domain/actions/PollEvmLogs"; -import { EvmBlockRepository, MetadataRepository } from "../../src/domain/repositories"; +import { + EvmBlockRepository, + MetadataRepository, + StatRepository, +} from "../../src/domain/repositories"; import { EvmBlock, EvmLog } from "../../src/domain/entities"; let cfg = PollEvmLogsConfig.fromBlock(0n); @@ -17,6 +21,8 @@ let metadataSaveSpy: jest.SpiedFunction[ let metadataRepo: MetadataRepository; let evmBlockRepo: EvmBlockRepository; +let statsRepo: StatRepository; + let handlers = { working: (logs: EvmLog[]) => Promise.resolve(), failing: (logs: EvmLog[]) => Promise.reject(), @@ -33,6 +39,7 @@ describe("PollEvmLogs", () => { const blocksAhead = 1n; givenEvmBlockRepository(currentHeight, blocksAhead); givenMetadataRepository(); + givenStatsRepository(); givenPollEvmLogs(); await whenPollEvmLogsStarts(); @@ -55,6 +62,7 @@ describe("PollEvmLogs", () => { const blocksAhead = 10n; givenEvmBlockRepository(lastExtractedBlock, blocksAhead); givenMetadataRepository({ lastBlock: lastExtractedBlock }); + givenStatsRepository(); givenPollEvmLogs(lastExtractedBlock - 10n); await whenPollEvmLogsStarts(); @@ -79,6 +87,7 @@ describe("PollEvmLogs", () => { const blocksAhead = 1n; givenEvmBlockRepository(currentHeight, blocksAhead); givenMetadataRepository(); + givenStatsRepository(); givenPollEvmLogs(currentHeight); await whenPollEvmLogsStarts(); @@ -137,9 +146,17 @@ const givenMetadataRepository = (data?: PollEvmLogsMetadata) => { metadataSaveSpy = jest.spyOn(metadataRepo, "save"); }; +const givenStatsRepository = () => { + statsRepo = { + count: () => {}, + measure: () => {}, + report: () => Promise.resolve(""), + }; +}; + const givenPollEvmLogs = (from?: bigint) => { cfg.setFromBlock(from); - pollEvmLogs = new PollEvmLogs(evmBlockRepo, metadataRepo, cfg); + pollEvmLogs = new PollEvmLogs(evmBlockRepo, metadataRepo, statsRepo, cfg); }; const whenPollEvmLogsStarts = async () => { diff --git a/deploy/blockchain-watcher/stateful-set.yaml b/deploy/blockchain-watcher/stateful-set.yaml index ac3ee9ec..d5224905 100644 --- a/deploy/blockchain-watcher/stateful-set.yaml +++ b/deploy/blockchain-watcher/stateful-set.yaml @@ -1,3 +1,20 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .NAME }} + namespace: {{ .NAMESPACE }} + labels: + app: {{ .NAME }} +spec: + selector: + app: {{ .NAME }} + ports: + - port: {{ .PORT }} + targetPort: {{ .PORT }} + name: {{ .NAME }} + protocol: TCP +--- apiVersion: apps/v1 kind: StatefulSet metadata: @@ -13,6 +30,9 @@ spec: metadata: labels: app: {{ .NAME }} + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .PORT }}" spec: restartPolicy: Always terminationGracePeriodSeconds: 30 @@ -49,4 +69,3 @@ spec: - name: metadata-volume persistentVolumeClaim: claimName: blockchain-watcher-metadata-pvc - \ No newline at end of file