1
1
import { ApiPromise , WsProvider } from "@polkadot/api" ;
2
+ import { Header } from "@polkadot/types/interfaces" ;
2
3
import * as PromClient from "prom-client"
3
4
import * as http from "http" ;
4
5
import { config } from "dotenv" ;
5
- import { hostname } from "node:os" ;
6
+ import BN from "bn.js" ;
7
+ import { privateEncrypt } from "crypto" ;
6
8
config ( ) ;
7
9
8
10
const WS_PROVIDER = process . env . WS_PROVIDER || "ws://localhost:9944" ;
9
11
const PORT = process . env . PORT || 8080 ;
10
12
13
+ const SECONDS = 1000 ;
14
+ const MINUTES = 60 * SECONDS ;
15
+ const HOURS = 60 * MINUTES ;
16
+ const DAYS = 24 * HOURS ;
17
+
11
18
const registry = new PromClient . Registry ( ) ;
12
19
registry . setDefaultLabels ( {
13
20
app : 'runtime-metrics'
@@ -18,7 +25,7 @@ const finalizedHeadMetric = new PromClient.Gauge({
18
25
help : "finalized head of the chain."
19
26
} )
20
27
21
- const WeightMetric = new PromClient . Gauge ( {
28
+ const weightMetric = new PromClient . Gauge ( {
22
29
name : "runtime_weight" ,
23
30
help : "weight of the block; labeled by dispatch class." ,
24
31
labelNames : [ "class" ]
@@ -40,40 +47,203 @@ const numExtrinsicsMetric = new PromClient.Gauge({
40
47
labelNames : [ "type" ]
41
48
} )
42
49
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
+
43
91
registry . registerMetric ( finalizedHeadMetric ) ;
44
- registry . registerMetric ( WeightMetric ) ;
92
+ registry . registerMetric ( weightMetric ) ;
45
93
registry . registerMetric ( timestampMetric ) ;
46
94
registry . registerMetric ( blockLengthMetric ) ;
47
95
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
+ }
48
226
49
227
async function update ( ) {
50
228
const provider = new WsProvider ( WS_PROVIDER ) ;
51
229
const api = await ApiPromise . create ( { provider } ) ;
52
230
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 ) ) ;
77
247
}
78
248
79
249
const server = http . createServer ( async ( req , res ) => {
@@ -112,5 +282,4 @@ const server = http.createServer(async (req, res) => {
112
282
// @ts -ignore
113
283
server . listen ( PORT , "0.0.0.0" ) ;
114
284
console . log ( `Server listening on port ${ PORT } ` )
115
-
116
285
update ( ) . catch ( console . error ) ;
0 commit comments