Skip to content
This repository was archived by the owner on Mar 6, 2023. It is now read-only.

Commit e84d35e

Browse files
committed
prepare for 0.4.0 release
1 parent a00bcdc commit e84d35e

File tree

4 files changed

+519
-364
lines changed

4 files changed

+519
-364
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ node_modules
22
build
33
data
44
binaries
5+
.vscode

package.json

+33-35
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,35 @@
11
{
2-
"name": "polkadot-runtime-prom-exporter",
3-
"version": "0.2.0",
4-
"description": "Prometheus exporter for polkadot runtime metrics",
5-
"scripts": {
6-
"start:dev": "nodemon",
7-
"run": "yarn run build && node build/index.js",
8-
"build": "rimraf ./build && tsc",
9-
"binary": "yarn run build && yarn run binary-mac && yarn run binary-linux",
10-
"binary-mac": "./node_modules/.bin/nexe build/index.js --build -t mac -o binaries/polkadot-runtime-prom-exporter-mac",
11-
"binary-linux": "./node_modules/.bin/nexe build/index.js --build -t linux -o binaries/polkadot-runtime-prom-exporter-linux",
12-
"publish": "npm publish",
13-
"release": "gh release create v0.2.0 binaries/*-mac"
14-
},
15-
"author": "@kianenigma",
16-
"license": "ISC",
17-
"devDependencies": {
18-
"@babel/cli": "^7.13.14",
19-
"@babel/core": "^7.13.14",
20-
"@babel/preset-typescript": "^7.13.0",
21-
"@types/node": "^14.14.37",
22-
"nodemon": "^2.0.7",
23-
"rimraf": "^3.0.2",
24-
"ts-loader": "^8.1.0",
25-
"ts-node": "^9.1.1",
26-
"typescript": "^4.2.3",
27-
"webpack": "^5.28.0",
28-
"webpack-cli": "^4.6.0",
29-
"nexe": "4.0.0-beta.18"
30-
},
31-
"dependencies": {
32-
"@polkadot/api": "4.3.1",
33-
"bn.js": "^5.2.0",
34-
"dotenv": "^8.2.0",
35-
"prom-client": "^13.1.0"
36-
}
2+
"name": "polkadot-runtime-prom-exporter",
3+
"version": "0.4.0",
4+
"description": "Prometheus exporter for polkadot runtime metrics",
5+
"scripts": {
6+
"start:dev": "nodemon",
7+
"run": "yarn run build && node build/index.js",
8+
"build": "rimraf ./build && tsc",
9+
"binary": "yarn run build && rimraf ./binaries &&./node_modules/.bin/nexe build/index.js --build -o binaries/polkadot-runtime-prom-exporter",
10+
"publish": "npm publish",
11+
"release": "gh release create v0.4.0 binaries/*"
12+
},
13+
"author": "@kianenigma",
14+
"license": "ISC",
15+
"devDependencies": {
16+
"@babel/cli": "^7.13.16",
17+
"@babel/core": "^7.14.0",
18+
"@babel/preset-typescript": "^7.13.0",
19+
"@types/node": "^15.0.2",
20+
"nodemon": "^2.0.7",
21+
"rimraf": "^3.0.2",
22+
"ts-loader": "^9.1.1",
23+
"ts-node": "^9.1.1",
24+
"typescript": "^4.2.4",
25+
"webpack": "^5.36.2",
26+
"webpack-cli": "^4.6.0",
27+
"nexe": "4.0.0-beta.18"
28+
},
29+
"dependencies": {
30+
"@polkadot/api": "4.8.1",
31+
"bn.js": "^5.2.0",
32+
"dotenv": "^8.5.1",
33+
"prom-client": "^13.1.0"
34+
}
3735
}

src/index.ts

+197-28
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { ApiPromise, WsProvider } from "@polkadot/api";
2+
import { Header } from "@polkadot/types/interfaces";
23
import * as PromClient from "prom-client"
34
import * as http from "http";
45
import { config } from "dotenv";
5-
import { hostname } from "node:os";
6+
import BN from "bn.js";
7+
import { privateEncrypt } from "crypto";
68
config();
79

810
const WS_PROVIDER = process.env.WS_PROVIDER || "ws://localhost:9944";
911
const PORT = process.env.PORT || 8080;
1012

13+
const SECONDS = 1000;
14+
const MINUTES = 60 * SECONDS;
15+
const HOURS = 60 * MINUTES;
16+
const DAYS = 24 * HOURS;
17+
1118
const registry = new PromClient.Registry();
1219
registry.setDefaultLabels({
1320
app: 'runtime-metrics'
@@ -18,7 +25,7 @@ const finalizedHeadMetric = new PromClient.Gauge({
1825
help: "finalized head of the chain."
1926
})
2027

21-
const WeightMetric = new PromClient.Gauge({
28+
const weightMetric = new PromClient.Gauge({
2229
name: "runtime_weight",
2330
help: "weight of the block; labeled by dispatch class.",
2431
labelNames: [ "class" ]
@@ -40,40 +47,203 @@ const numExtrinsicsMetric = new PromClient.Gauge({
4047
labelNames: ["type"]
4148
})
4249

50+
const totalIssuanceMetric = new PromClient.Gauge({
51+
name: "runtime_total_issuance",
52+
help: "the total issuance of the runtime, updated per block",
53+
})
54+
55+
const nominatorCountMetric = new PromClient.Gauge({
56+
name: "runtime_nominator_count",
57+
help: "Total number of nominators in staking system",
58+
labelNames: ["type"],
59+
})
60+
61+
const validatorCountMetric = new PromClient.Gauge({
62+
name: "runtime_validator_count",
63+
help: "Total number of validators in staking system",
64+
labelNames: ["type"],
65+
})
66+
67+
const stakeMetric = new PromClient.Gauge({
68+
name: "runtime_stake",
69+
help: "Total amount of staked tokens",
70+
labelNames: ["type"],
71+
})
72+
73+
const ledgerMetric = new PromClient.Gauge({
74+
name: "runtime_staking_ledger",
75+
help: "the entire staking ledger data",
76+
labelNames: ["type"],
77+
})
78+
79+
const councilMetric = new PromClient.Gauge({
80+
name: "runtime_council",
81+
help: "Metrics of the council elections pallet.",
82+
labelNames: ["entity"],
83+
})
84+
85+
const multiPhaseSolution = new PromClient.Gauge({
86+
name: "runtime_multi_phase_election_unsigned",
87+
help: "Stats of the latest multi_phase submission, only unsigned.",
88+
labelNames: ["measure"]
89+
})
90+
4391
registry.registerMetric(finalizedHeadMetric);
44-
registry.registerMetric(WeightMetric);
92+
registry.registerMetric(weightMetric);
4593
registry.registerMetric(timestampMetric);
4694
registry.registerMetric(blockLengthMetric);
4795
registry.registerMetric(numExtrinsicsMetric);
96+
registry.registerMetric(totalIssuanceMetric);
97+
registry.registerMetric(nominatorCountMetric);
98+
registry.registerMetric(validatorCountMetric);
99+
registry.registerMetric(stakeMetric);
100+
registry.registerMetric(ledgerMetric);
101+
registry.registerMetric(councilMetric);
102+
registry.registerMetric(multiPhaseSolution);
103+
104+
async function stakingHourly(api: ApiPromise) {
105+
let currentEra = (await api.query.staking.currentEra()).unwrapOrDefault();
106+
let [validators, nominators, exposures] = await Promise.all([
107+
api.query.staking.validators.entries(),
108+
api.query.staking.nominators.entries(),
109+
api.query.staking.erasStakers.entries(currentEra),
110+
]);
111+
112+
validatorCountMetric.set({ type: "candidate" }, validators.length);
113+
nominatorCountMetric.set({ type: "candidate" }, nominators.length);
114+
115+
// @ts-ignore
116+
let totalExposedNominators = exposures.map(([_, expo]) => expo.others.length).reduce((prev, next) => prev + next);
117+
let totalExposedValidators = exposures.length;
118+
119+
// @ts-ignore
120+
let totalSelfStake = exposures.map(([_, expo]) => expo.own.toBn().div(api.decimalPoints).toNumber()).reduce((prev, next) => prev + next);
121+
// @ts-ignore
122+
let totalOtherStake = exposures.map(([_, expo]) => (expo.total.toBn().sub(expo.own.toBn())).div(api.decimalPoints).toNumber()).reduce((prev, next) => prev + next);
123+
let totalStake = totalOtherStake + totalSelfStake;
124+
125+
stakeMetric.set({ type: "self" }, totalSelfStake);
126+
stakeMetric.set({ type: "other" }, totalOtherStake);
127+
stakeMetric.set({ type: "all" }, totalStake);
128+
129+
validatorCountMetric.set({ type: "exposed" }, totalExposedValidators);
130+
nominatorCountMetric.set({ type: "exposed" }, totalExposedNominators);
131+
}
132+
133+
async function stakingDaily(api: ApiPromise) {
134+
let ledgers = await api.query.staking.ledger.entries();
135+
// @ts-ignore
136+
let decimalPoints = api.decimalPoints;
137+
138+
let totalBondedAccounts = ledgers.length;
139+
let totalBondedStake = ledgers.map(([_, ledger]) =>
140+
ledger.unwrapOrDefault().total.toBn().div(decimalPoints).toNumber()
141+
).reduce((prev, next) => prev + next)
142+
let totalUnbondingChunks = ledgers.map(([_, ledger]) =>
143+
ledger.unwrapOrDefault().unlocking.map((unlocking) =>
144+
unlocking.value.toBn().div(decimalPoints).toNumber()
145+
).reduce((prev, next) => prev + next)
146+
).reduce((prev, next) => prev + next)
147+
148+
ledgerMetric.set({ type: "bonded_accounts" }, totalBondedAccounts)
149+
ledgerMetric.set({ type: "bonded_stake" }, totalBondedStake)
150+
ledgerMetric.set({ type: "unbonding_stake" }, totalUnbondingChunks)
151+
}
152+
153+
async function council(api: ApiPromise) {
154+
if (api.query.electionsPhragmen) {
155+
let [voters, candidates] = await Promise.all([
156+
api.query.electionsPhragmen.voting.entries(),
157+
api.query.electionsPhragmen.candidates(),
158+
]);
159+
councilMetric.set( { entity: "voters" }, voters.length);
160+
// @ts-ignore
161+
councilMetric.set( { entity: "candidates" }, candidates.length);
162+
} else if (api.query.phragmenElection) {
163+
let [voters, candidates] = await Promise.all([
164+
api.query.phragmenElection.voting.entries(),
165+
api.query.phragmenElection.candidates(),
166+
]);
167+
councilMetric.set({ entity: "voters" }, voters.length);
168+
// @ts-ignore
169+
councilMetric.set({ entity: "candidates" }, candidates.length);
170+
}
171+
}
172+
173+
async function perDay(api: ApiPromise) {
174+
let stakingPromise = stakingDaily(api);
175+
Promise.all([stakingPromise])
176+
console.log(`updated daily metrics at ${new Date()}`)
177+
}
178+
179+
async function perHour(api: ApiPromise) {
180+
let stakingPromise = stakingHourly(api);
181+
let councilPromise = council(api);
182+
183+
Promise.all([stakingPromise, councilPromise])
184+
console.log(`updated hourly metrics at ${new Date()}`)
185+
}
186+
187+
async function perBlock(api: ApiPromise, header: Header) {
188+
let number = header.number;
189+
const [weight, timestamp, signed_block] = await Promise.all([
190+
api.query.system.blockWeight(),
191+
api.query.timestamp.now(),
192+
await api.rpc.chain.getBlock(header.hash)
193+
]);
194+
const blockLength = signed_block.block.encodedLength;
195+
196+
finalizedHeadMetric.set(number.toNumber());
197+
weightMetric.set({ class: "normal" }, weight.normal.toNumber());
198+
weightMetric.set({ class: "operational" }, weight.operational.toNumber());
199+
weightMetric.set({ class: "mandatory" }, weight.mandatory.toNumber());
200+
timestampMetric.set(timestamp.toNumber() / 1000);
201+
blockLengthMetric.set(blockLength);
202+
203+
const signedLength = signed_block.block.extrinsics.filter((ext) => ext.isSigned).length
204+
const unsignedLength = signed_block.block.extrinsics.length - signedLength;
205+
numExtrinsicsMetric.set({ type: "singed" }, signedLength);
206+
numExtrinsicsMetric.set({ type: "unsigned" }, unsignedLength);
207+
208+
// update issuance
209+
let issuance = (await api.query.balances.totalIssuance()).toBn();
210+
// @ts-ignore
211+
let issuancesScaled = issuance.div(api.decimalPoints);
212+
totalIssuanceMetric.set(issuancesScaled.toNumber())
213+
214+
// check if we had an election-provider solution in this block.
215+
for (let ext of signed_block.block.extrinsics) {
216+
if (ext.meta.name.toHuman().startsWith('submit_unsigned')) {
217+
let length = ext.encodedLength;
218+
let weight = (await api.rpc.payment.queryInfo(ext.toHex())).weight.toNumber()
219+
multiPhaseSolution.set({ 'measure': 'weight' }, weight);
220+
multiPhaseSolution.set({ 'measure': 'length' }, length);
221+
}
222+
}
223+
224+
console.log(`updated metrics according to #${number}`)
225+
}
48226

49227
async function update() {
50228
const provider = new WsProvider(WS_PROVIDER);
51229
const api = await ApiPromise.create( { provider });
52230

53-
// only look into finalized blocks.
54-
const _unsubscribe = await api.rpc.chain.subscribeFinalizedHeads(async (header) => {
55-
let number = header.number;
56-
const [weight, timestamp, signed_block] = await Promise.all([
57-
api.query.system.blockWeight(),
58-
api.query.timestamp.now(),
59-
await api.rpc.chain.getBlock(header.hash)
60-
]);
61-
const blockLength = signed_block.block.encodedLength;
62-
63-
finalizedHeadMetric.set(number.toNumber());
64-
WeightMetric.set({ class: "normal" }, weight.normal.toNumber());
65-
WeightMetric.set({ class: "operational" }, weight.operational.toNumber());
66-
WeightMetric.set({ class: "mandatory" }, weight.mandatory.toNumber());
67-
timestampMetric.set(timestamp.toNumber() / 1000);
68-
blockLengthMetric.set(blockLength);
69-
70-
const signedLength = signed_block.block.extrinsics.filter((ext) => ext.isSigned).length
71-
const unsignedLength = signed_block.block.extrinsics.length - signedLength;
72-
numExtrinsicsMetric.set({ type: "singed" }, signedLength);
73-
numExtrinsicsMetric.set({ type: "unsigned" }, unsignedLength);
74-
75-
console.log(`updated state according to #${number}`)
76-
});
231+
const decimalPoints =
232+
(await api.rpc.system.chain()).toString().toLowerCase() == "polkadot" ?
233+
new BN(Math.pow(10, 10)) :
234+
new BN(Math.pow(10, 12));
235+
236+
// @ts-ignore
237+
api.decimalPoints = decimalPoints;
238+
239+
// update stuff per hour
240+
const _perHour = setInterval(() => perHour(api), MINUTES * 1);
241+
242+
// update stuff daily
243+
// const _perDay = setInterval(() => perDay(api), 3 * MINUTES / 2);
244+
245+
// update stuff per block.
246+
const _perBlock = api.rpc.chain.subscribeFinalizedHeads((header) => perBlock(api, header));
77247
}
78248

79249
const server = http.createServer(async (req, res) => {
@@ -112,5 +282,4 @@ const server = http.createServer(async (req, res) => {
112282
// @ts-ignore
113283
server.listen(PORT, "0.0.0.0");
114284
console.log(`Server listening on port ${PORT}`)
115-
116285
update().catch(console.error);

0 commit comments

Comments
 (0)