envio 3.3.0 → 3.3.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -7
- package/src/ChainFetching.res +4 -32
- package/src/ChainFetching.res.mjs +2 -12
- package/src/ChainState.res +73 -53
- package/src/ChainState.res.mjs +61 -30
- package/src/ChainState.resi +13 -17
- package/src/Config.res +5 -0
- package/src/Config.res.mjs +1 -0
- package/src/CrossChainState.res +2 -8
- package/src/CrossChainState.res.mjs +6 -8
- package/src/CrossChainState.resi +1 -0
- package/src/EffectState.res +183 -44
- package/src/EffectState.res.mjs +134 -26
- package/src/EffectState.resi +26 -3
- package/src/EventProcessing.res +14 -9
- package/src/EventProcessing.res.mjs +7 -7
- package/src/FetchState.res +18 -29
- package/src/FetchState.res.mjs +20 -37
- package/src/IndexerState.res +280 -31
- package/src/IndexerState.res.mjs +234 -25
- package/src/IndexerState.resi +22 -0
- package/src/LoadLayer.res +19 -65
- package/src/LoadLayer.res.mjs +13 -55
- package/src/Main.res +42 -23
- package/src/Main.res.mjs +32 -35
- package/src/Metrics.res +943 -66
- package/src/Metrics.res.mjs +367 -50
- package/src/Persistence.res +3 -0
- package/src/PgStorage.res +3 -8
- package/src/PgStorage.res.mjs +3 -4
- package/src/PruneStaleHistory.res +1 -1
- package/src/PruneStaleHistory.res.mjs +1 -2
- package/src/Rollback.res +4 -5
- package/src/Rollback.res.mjs +2 -3
- package/src/SimulateItems.res.mjs +3 -21
- package/src/TestIndexer.res.mjs +4 -23
- package/src/TestIndexerProxyStorage.res +1 -0
- package/src/TestIndexerProxyStorage.res.mjs +1 -1
- package/src/Writing.res +3 -5
- package/src/Writing.res.mjs +3 -6
- package/src/bindings/ClickHouse.res +101 -17
- package/src/bindings/ClickHouse.res.mjs +58 -19
- package/src/bindings/NodeJs.res +64 -0
- package/src/bindings/NodeJs.res.mjs +6 -0
- package/src/sources/HyperSyncClient.res +5 -11
- package/src/sources/SourceManager.res +72 -27
- package/src/sources/SourceManager.res.mjs +81 -11
- package/src/sources/SourceManager.resi +14 -0
- package/src/tui/Tui.res +1 -1
- package/src/tui/Tui.res.mjs +2 -2
- package/src/Prometheus.res +0 -789
- package/src/Prometheus.res.mjs +0 -847
- package/src/bindings/PromClient.res +0 -72
- package/src/bindings/PromClient.res.mjs +0 -38
package/src/Metrics.res
CHANGED
|
@@ -1,74 +1,951 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
type chainMetrics = {
|
|
2
|
+
chainId: float,
|
|
3
|
+
poweredByHyperSync: bool,
|
|
4
|
+
firstEventBlockNumber: option<int>,
|
|
5
|
+
latestProcessedBlock: option<int>,
|
|
6
|
+
timestampCaughtUpToHeadOrEndblock: option<Date.t>,
|
|
7
|
+
numEventsProcessed: float,
|
|
8
|
+
latestFetchedBlockNumber: int,
|
|
9
|
+
// Clamped to endBlock once the chain has processed to it.
|
|
10
|
+
knownHeight: int,
|
|
11
|
+
numBatchesFetched: int,
|
|
12
|
+
startBlock: int,
|
|
13
|
+
endBlock: option<int>,
|
|
14
|
+
numAddresses: int,
|
|
15
|
+
isReady: bool,
|
|
16
|
+
// Raw source height, unlike knownHeight which is clamped to endBlock.
|
|
17
|
+
sourceBlockNumber: int,
|
|
18
|
+
// Raw committed progress (may be -1), unlike the optional latestProcessedBlock.
|
|
19
|
+
progressBlockNumber: int,
|
|
20
|
+
progressLatencyMs: option<int>,
|
|
21
|
+
concurrency: int,
|
|
22
|
+
partitionsCount: int,
|
|
23
|
+
bufferSize: int,
|
|
24
|
+
// Raw buffer block (may be -1), unlike the clamped latestFetchedBlockNumber.
|
|
25
|
+
bufferBlockNumber: int,
|
|
26
|
+
idleSeconds: float,
|
|
27
|
+
waitingForNewBlockSeconds: float,
|
|
28
|
+
queryingSeconds: float,
|
|
29
|
+
blockRangeFetchSeconds: float,
|
|
30
|
+
blockRangeParseSeconds: float,
|
|
31
|
+
blockRangeFetchCount: float,
|
|
32
|
+
blockRangeFetchedEvents: float,
|
|
33
|
+
blockRangeFetchedBlocks: float,
|
|
34
|
+
reorgCount: int,
|
|
35
|
+
reorgDetectedBlock: option<int>,
|
|
36
|
+
rollbackTargetBlock: option<int>,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type handlerMetrics = {
|
|
40
|
+
contract: string,
|
|
41
|
+
event: string,
|
|
42
|
+
processingSeconds: float,
|
|
43
|
+
processingCount: float,
|
|
44
|
+
preloadSeconds: float,
|
|
45
|
+
preloadCount: float,
|
|
46
|
+
preloadSecondsTotal: float,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type effectMetrics = {
|
|
50
|
+
effect: string,
|
|
51
|
+
scope: string,
|
|
52
|
+
callSeconds: float,
|
|
53
|
+
callSecondsTotal: float,
|
|
54
|
+
callCount: float,
|
|
55
|
+
activeCallsCount: int,
|
|
56
|
+
queueCount: int,
|
|
57
|
+
queueWaitSeconds: float,
|
|
58
|
+
invalidationsCount: float,
|
|
59
|
+
// None for effects that don't persist their cache.
|
|
60
|
+
cacheCount: option<int>,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type storageLoadMetrics = {
|
|
64
|
+
operation: string,
|
|
65
|
+
storage: string,
|
|
66
|
+
seconds: float,
|
|
67
|
+
secondsTotal: float,
|
|
68
|
+
count: float,
|
|
69
|
+
whereSize: float,
|
|
70
|
+
size: float,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type storageWriteMetrics = {
|
|
74
|
+
storage: string,
|
|
75
|
+
seconds: float,
|
|
76
|
+
count: int,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type historyPruneMetrics = {
|
|
80
|
+
entity: string,
|
|
81
|
+
seconds: float,
|
|
82
|
+
count: int,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type sourceRequestMetrics = {
|
|
86
|
+
source: string,
|
|
87
|
+
chainId: int,
|
|
88
|
+
method: string,
|
|
89
|
+
count: int,
|
|
90
|
+
seconds: float,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type sourceHeightMetrics = {
|
|
94
|
+
source: string,
|
|
95
|
+
chainId: int,
|
|
96
|
+
height: int,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type t = {
|
|
100
|
+
startTime: Date.t,
|
|
101
|
+
targetBufferSize: int,
|
|
102
|
+
isInReorgThreshold: bool,
|
|
103
|
+
rollbackEnabled: bool,
|
|
104
|
+
maxBatchSize: int,
|
|
105
|
+
preloadSeconds: float,
|
|
106
|
+
processingSeconds: float,
|
|
107
|
+
rollbackSeconds: float,
|
|
108
|
+
rollbackCount: int,
|
|
109
|
+
rollbackEventsCount: float,
|
|
110
|
+
chains: array<chainMetrics>,
|
|
111
|
+
handlers: array<handlerMetrics>,
|
|
112
|
+
effects: array<effectMetrics>,
|
|
113
|
+
storageLoads: array<storageLoadMetrics>,
|
|
114
|
+
storageWrites: array<storageWriteMetrics>,
|
|
115
|
+
historyPrunes: array<historyPruneMetrics>,
|
|
116
|
+
sourceRequests: array<sourceRequestMetrics>,
|
|
117
|
+
sourceHeights: array<sourceHeightMetrics>,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Prometheus floats keep at most 3 decimals; integral values render without a
|
|
121
|
+
// fractional part.
|
|
122
|
+
@inline
|
|
123
|
+
let formatValue = (value: float) => (Math.round(value *. 1000.) /. 1000.)->Float.toString
|
|
124
|
+
|
|
125
|
+
// Prometheus exposition-format escaping for label values: a raw `"`, `\` or
|
|
126
|
+
// newline in a user-supplied name (contract, event, effect, ...) would
|
|
127
|
+
// otherwise break the scrape.
|
|
128
|
+
let escapeLabelValue = (value: string) =>
|
|
129
|
+
if value->String.includes("\\") || value->String.includes("\"") || value->String.includes("\n") {
|
|
130
|
+
value
|
|
131
|
+
->String.replaceAll("\\", "\\\\")
|
|
132
|
+
->String.replaceAll("\"", "\\\"")
|
|
133
|
+
->String.replaceAll("\n", "\\n")
|
|
36
134
|
} else {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
135
|
+
value
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Accumulate into one string rather than building a lines array to join: `++`
|
|
139
|
+
// compiles to JS `+=`, which V8 grows as a ConsString instead of recopying.
|
|
140
|
+
type builder = {mutable out: string}
|
|
141
|
+
|
|
142
|
+
// Begin a metric: HELP/TYPE header, with a blank line between metrics.
|
|
143
|
+
let block = (b: builder, ~name, ~help, ~kind) => {
|
|
144
|
+
let header = `# HELP ${name} ${help}\n# TYPE ${name} ${kind}`
|
|
145
|
+
b.out = b.out === "" ? header : b.out ++ "\n\n" ++ header
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let sample = (b: builder, ~name, ~labels="", ~value) =>
|
|
149
|
+
b.out = b.out ++ "\n" ++ name ++ labels ++ " " ++ formatValue(value)
|
|
150
|
+
|
|
151
|
+
// One metric with a sample per entry. Entries carry their pre-rendered label
|
|
152
|
+
// string so the same list feeds several metrics without re-rendering labels.
|
|
153
|
+
let series = (
|
|
154
|
+
b: builder,
|
|
155
|
+
~name,
|
|
156
|
+
~help,
|
|
157
|
+
~kind,
|
|
158
|
+
~entries: array<(string, 'a)>,
|
|
159
|
+
~value: 'a => float,
|
|
160
|
+
) => {
|
|
161
|
+
b->block(~name, ~help, ~kind)
|
|
162
|
+
for i in 0 to entries->Array.length - 1 {
|
|
163
|
+
let (labels, entry) = entries->Array.getUnsafe(i)
|
|
164
|
+
b->sample(~name, ~labels, ~value=value(entry))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Same as series, but an entry can opt out of its sample (e.g. a counter that
|
|
169
|
+
// was never incremented or a gauge that was never observed).
|
|
170
|
+
let seriesOpt = (
|
|
171
|
+
b: builder,
|
|
172
|
+
~name,
|
|
173
|
+
~help,
|
|
174
|
+
~kind,
|
|
175
|
+
~entries: array<(string, 'a)>,
|
|
176
|
+
~value: 'a => option<float>,
|
|
177
|
+
) => {
|
|
178
|
+
b->block(~name, ~help, ~kind)
|
|
179
|
+
for i in 0 to entries->Array.length - 1 {
|
|
180
|
+
let (labels, entry) = entries->Array.getUnsafe(i)
|
|
181
|
+
switch value(entry) {
|
|
182
|
+
| Some(value) => b->sample(~name, ~labels, ~value)
|
|
183
|
+
| None => ()
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let single = (b: builder, ~name, ~help, ~kind, ~value) => {
|
|
189
|
+
b->block(~name, ~help, ~kind)
|
|
190
|
+
b->sample(~name, ~value)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let renderMetrics = (b: builder, metrics: t) => {
|
|
194
|
+
let chains = metrics.chains->Array.map(m => (`{chainId="${m.chainId->Float.toString}"}`, m))
|
|
195
|
+
let handlers =
|
|
196
|
+
metrics.handlers->Array.map(s => (
|
|
197
|
+
`{contract="${s.contract->escapeLabelValue}",event="${s.event->escapeLabelValue}"}`,
|
|
198
|
+
s,
|
|
199
|
+
))
|
|
200
|
+
let effects =
|
|
201
|
+
metrics.effects->Array.map(s => (
|
|
202
|
+
`{effect="${s.effect->escapeLabelValue}",scope="${s.scope->escapeLabelValue}"}`,
|
|
203
|
+
s,
|
|
204
|
+
))
|
|
205
|
+
let storageLoads =
|
|
206
|
+
metrics.storageLoads->Array.map(s => (
|
|
207
|
+
`{operation="${s.operation->escapeLabelValue}",storage="${s.storage->escapeLabelValue}"}`,
|
|
208
|
+
s,
|
|
209
|
+
))
|
|
210
|
+
let storageWrites =
|
|
211
|
+
metrics.storageWrites->Array.map(s => (`{storage="${s.storage->escapeLabelValue}"}`, s))
|
|
212
|
+
let historyPrunes =
|
|
213
|
+
metrics.historyPrunes->Array.map(s => (`{entity="${s.entity->escapeLabelValue}"}`, s))
|
|
214
|
+
// Two sources can share a name (e.g. primary and fallback RPC urls on the
|
|
215
|
+
// same host), so aggregate by label set — duplicate samples would make
|
|
216
|
+
// Prometheus reject the scrape.
|
|
217
|
+
let sourceRequests = {
|
|
218
|
+
let byLabels: dict<sourceRequestMetrics> = Dict.make()
|
|
219
|
+
metrics.sourceRequests->Array.forEach(s => {
|
|
220
|
+
let labels = `{source="${s.source->escapeLabelValue}",chainId="${s.chainId->Int.toString}",method="${s.method->escapeLabelValue}"}`
|
|
221
|
+
switch byLabels->Utils.Dict.dangerouslyGetNonOption(labels) {
|
|
222
|
+
| Some(existing) =>
|
|
223
|
+
byLabels->Dict.set(
|
|
224
|
+
labels,
|
|
225
|
+
{...existing, count: existing.count + s.count, seconds: existing.seconds +. s.seconds},
|
|
226
|
+
)
|
|
227
|
+
| None => byLabels->Dict.set(labels, s)
|
|
51
228
|
}
|
|
52
229
|
})
|
|
53
|
-
|
|
230
|
+
byLabels->Dict.toArray
|
|
54
231
|
}
|
|
232
|
+
let sources = {
|
|
233
|
+
let byLabels: dict<int> = Dict.make()
|
|
234
|
+
metrics.sourceHeights->Array.forEach(s => {
|
|
235
|
+
let labels = `{source="${s.source->escapeLabelValue}",chainId="${s.chainId->Int.toString}"}`
|
|
236
|
+
switch byLabels->Utils.Dict.dangerouslyGetNonOption(labels) {
|
|
237
|
+
| Some(existing) if existing >= s.height => ()
|
|
238
|
+
| _ => byLabels->Dict.set(labels, s.height)
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
byLabels->Dict.toArray
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
b->single(
|
|
245
|
+
~name="envio_preload_seconds",
|
|
246
|
+
~help="Cumulative time spent on preloading entities during batch processing.",
|
|
247
|
+
~kind="counter",
|
|
248
|
+
~value=metrics.preloadSeconds,
|
|
249
|
+
)
|
|
250
|
+
b->single(
|
|
251
|
+
~name="envio_processing_seconds",
|
|
252
|
+
~help="Cumulative time spent executing event handlers during batch processing.",
|
|
253
|
+
~kind="counter",
|
|
254
|
+
~value=metrics.processingSeconds,
|
|
255
|
+
)
|
|
256
|
+
b->series(
|
|
257
|
+
~name="envio_progress_ready",
|
|
258
|
+
~help="Whether the chain is fully synced to the head.",
|
|
259
|
+
~kind="gauge",
|
|
260
|
+
~entries=chains,
|
|
261
|
+
~value=m => m.isReady ? 1. : 0.,
|
|
262
|
+
)
|
|
263
|
+
// Keep legacy metric name for backward compatibility
|
|
264
|
+
b->single(
|
|
265
|
+
~name="hyperindex_synced_to_head",
|
|
266
|
+
~help="All chains fully synced",
|
|
267
|
+
~kind="gauge",
|
|
268
|
+
~value=metrics.chains->Utils.Array.notEmpty && metrics.chains->Array.every(m => m.isReady)
|
|
269
|
+
? 1.
|
|
270
|
+
: 0.,
|
|
271
|
+
)
|
|
272
|
+
b->series(
|
|
273
|
+
~name="envio_processing_handler_seconds",
|
|
274
|
+
~help="Cumulative time spent inside individual event handler executions.",
|
|
275
|
+
~kind="counter",
|
|
276
|
+
~entries=handlers,
|
|
277
|
+
~value=s => s.processingSeconds,
|
|
278
|
+
)
|
|
279
|
+
b->series(
|
|
280
|
+
~name="envio_processing_handler_total",
|
|
281
|
+
~help="Total number of individual event handler executions.",
|
|
282
|
+
~kind="counter",
|
|
283
|
+
~entries=handlers,
|
|
284
|
+
~value=s => s.processingCount,
|
|
285
|
+
)
|
|
286
|
+
b->series(
|
|
287
|
+
~name="envio_preload_handler_seconds",
|
|
288
|
+
~help="Wall-clock time spent inside individual preload handler executions.",
|
|
289
|
+
~kind="counter",
|
|
290
|
+
~entries=handlers,
|
|
291
|
+
~value=s => s.preloadSeconds,
|
|
292
|
+
)
|
|
293
|
+
b->series(
|
|
294
|
+
~name="envio_preload_handler_total",
|
|
295
|
+
~help="Total number of individual preload handler executions.",
|
|
296
|
+
~kind="counter",
|
|
297
|
+
~entries=handlers,
|
|
298
|
+
~value=s => s.preloadCount,
|
|
299
|
+
)
|
|
300
|
+
b->series(
|
|
301
|
+
~name="envio_preload_handler_seconds_total",
|
|
302
|
+
~help="Cumulative time spent inside individual preload handler executions. Can exceed wall-clock time due to parallel execution.",
|
|
303
|
+
~kind="counter",
|
|
304
|
+
~entries=handlers,
|
|
305
|
+
~value=s => s.preloadSecondsTotal,
|
|
306
|
+
)
|
|
307
|
+
b->series(
|
|
308
|
+
~name="envio_fetching_block_range_seconds",
|
|
309
|
+
~help="Cumulative time spent fetching block ranges.",
|
|
310
|
+
~kind="counter",
|
|
311
|
+
~entries=chains,
|
|
312
|
+
~value=m => m.blockRangeFetchSeconds,
|
|
313
|
+
)
|
|
314
|
+
b->series(
|
|
315
|
+
~name="envio_fetching_block_range_parse_seconds",
|
|
316
|
+
~help="Cumulative time spent parsing block range fetch responses.",
|
|
317
|
+
~kind="counter",
|
|
318
|
+
~entries=chains,
|
|
319
|
+
~value=m => m.blockRangeParseSeconds,
|
|
320
|
+
)
|
|
321
|
+
b->series(
|
|
322
|
+
~name="envio_fetching_block_range_total",
|
|
323
|
+
~help="Total number of block range fetch operations.",
|
|
324
|
+
~kind="counter",
|
|
325
|
+
~entries=chains,
|
|
326
|
+
~value=m => m.blockRangeFetchCount,
|
|
327
|
+
)
|
|
328
|
+
b->series(
|
|
329
|
+
~name="envio_fetching_block_range_events_total",
|
|
330
|
+
~help="Cumulative number of events fetched across all block range operations.",
|
|
331
|
+
~kind="counter",
|
|
332
|
+
~entries=chains,
|
|
333
|
+
~value=m => m.blockRangeFetchedEvents,
|
|
334
|
+
)
|
|
335
|
+
b->series(
|
|
336
|
+
~name="envio_fetching_block_range_size",
|
|
337
|
+
~help="Cumulative number of blocks covered across all block range fetch operations.",
|
|
338
|
+
~kind="counter",
|
|
339
|
+
~entries=chains,
|
|
340
|
+
~value=m => m.blockRangeFetchedBlocks,
|
|
341
|
+
)
|
|
342
|
+
b->series(
|
|
343
|
+
~name="envio_indexing_known_height",
|
|
344
|
+
~help="The latest known block number reported by the active indexing source. This value may lag behind the actual chain height, as it is updated only when needed.",
|
|
345
|
+
~kind="gauge",
|
|
346
|
+
~entries=chains,
|
|
347
|
+
~value=m => m.sourceBlockNumber->Int.toFloat,
|
|
348
|
+
)
|
|
349
|
+
b->single(
|
|
350
|
+
~name="envio_process_start_time_seconds",
|
|
351
|
+
~help="Start time of the process since unix epoch in seconds.",
|
|
352
|
+
~kind="gauge",
|
|
353
|
+
~value=metrics.startTime->Date.getTime /. 1000.,
|
|
354
|
+
)
|
|
355
|
+
b->series(
|
|
356
|
+
~name="envio_indexing_concurrency",
|
|
357
|
+
~help="The number of executing concurrent queries to the chain data-source.",
|
|
358
|
+
~kind="gauge",
|
|
359
|
+
~entries=chains,
|
|
360
|
+
~value=m => m.concurrency->Int.toFloat,
|
|
361
|
+
)
|
|
362
|
+
b->series(
|
|
363
|
+
~name="envio_indexing_partitions",
|
|
364
|
+
~help="The number of partitions used to split fetching logic by addresses and block ranges.",
|
|
365
|
+
~kind="gauge",
|
|
366
|
+
~entries=chains,
|
|
367
|
+
~value=m => m.partitionsCount->Int.toFloat,
|
|
368
|
+
)
|
|
369
|
+
b->series(
|
|
370
|
+
~name="envio_indexing_idle_seconds",
|
|
371
|
+
~help="The time the indexer source syncing has been idle. A high value may indicate the source sync is a bottleneck.",
|
|
372
|
+
~kind="counter",
|
|
373
|
+
~entries=chains,
|
|
374
|
+
~value=m => m.idleSeconds,
|
|
375
|
+
)
|
|
376
|
+
b->series(
|
|
377
|
+
~name="envio_indexing_source_waiting_seconds",
|
|
378
|
+
~help="The time the indexer has been waiting for new blocks.",
|
|
379
|
+
~kind="counter",
|
|
380
|
+
~entries=chains,
|
|
381
|
+
~value=m => m.waitingForNewBlockSeconds,
|
|
382
|
+
)
|
|
383
|
+
b->series(
|
|
384
|
+
~name="envio_indexing_source_querying_seconds",
|
|
385
|
+
~help="The time spent performing queries to the chain data-source.",
|
|
386
|
+
~kind="counter",
|
|
387
|
+
~entries=chains,
|
|
388
|
+
~value=m => m.queryingSeconds,
|
|
389
|
+
)
|
|
390
|
+
b->series(
|
|
391
|
+
~name="envio_indexing_buffer_size",
|
|
392
|
+
~help="The current number of items in the indexing buffer.",
|
|
393
|
+
~kind="gauge",
|
|
394
|
+
~entries=chains,
|
|
395
|
+
~value=m => m.bufferSize->Int.toFloat,
|
|
396
|
+
)
|
|
397
|
+
b->single(
|
|
398
|
+
~name="envio_indexing_target_buffer_size",
|
|
399
|
+
~help="The indexer-wide target buffer size shared across all chains. The actual number of items in the queue may exceed this value, but the indexer always tries to keep the buffer filled up to this target.",
|
|
400
|
+
~kind="gauge",
|
|
401
|
+
~value=metrics.targetBufferSize->Int.toFloat,
|
|
402
|
+
)
|
|
403
|
+
b->series(
|
|
404
|
+
~name="envio_indexing_buffer_block",
|
|
405
|
+
~help="The highest block number that has been fully fetched by the indexer.",
|
|
406
|
+
~kind="gauge",
|
|
407
|
+
~entries=chains,
|
|
408
|
+
~value=m => m.bufferBlockNumber->Int.toFloat,
|
|
409
|
+
)
|
|
410
|
+
b->seriesOpt(
|
|
411
|
+
~name="envio_indexing_end_block",
|
|
412
|
+
~help="The block number to stop indexing at. (inclusive)",
|
|
413
|
+
~kind="gauge",
|
|
414
|
+
~entries=chains,
|
|
415
|
+
~value=m => m.endBlock->Option.map(Int.toFloat),
|
|
416
|
+
)
|
|
417
|
+
b->series(
|
|
418
|
+
~name="envio_source_request_total",
|
|
419
|
+
~help="The number of requests made to data sources.",
|
|
420
|
+
~kind="counter",
|
|
421
|
+
~entries=sourceRequests,
|
|
422
|
+
~value=s => s.count->Int.toFloat,
|
|
423
|
+
)
|
|
424
|
+
// Skips a method's seconds line when it has no timing (e.g. heightSubscription,
|
|
425
|
+
// which only ever records a count).
|
|
426
|
+
b->seriesOpt(
|
|
427
|
+
~name="envio_source_request_seconds_total",
|
|
428
|
+
~help="Cumulative time spent on data source requests.",
|
|
429
|
+
~kind="counter",
|
|
430
|
+
~entries=sourceRequests,
|
|
431
|
+
~value=s => s.seconds !== 0. ? Some(s.seconds) : None,
|
|
432
|
+
)
|
|
433
|
+
b->series(
|
|
434
|
+
~name="envio_source_known_height",
|
|
435
|
+
~help="The latest known block number reported by the source. This value may lag behind the actual chain height, as it is updated only when queried.",
|
|
436
|
+
~kind="gauge",
|
|
437
|
+
~entries=sources,
|
|
438
|
+
~value=height => height->Int.toFloat,
|
|
439
|
+
)
|
|
440
|
+
b->seriesOpt(
|
|
441
|
+
~name="envio_reorg_detected_total",
|
|
442
|
+
~help="Total number of reorgs detected",
|
|
443
|
+
~kind="counter",
|
|
444
|
+
~entries=chains,
|
|
445
|
+
~value=m => m.reorgCount > 0 ? Some(m.reorgCount->Int.toFloat) : None,
|
|
446
|
+
)
|
|
447
|
+
b->seriesOpt(
|
|
448
|
+
~name="envio_reorg_detected_block",
|
|
449
|
+
~help="The block number where reorg was detected the last time. This doesn't mean that the block was reorged, this is simply where we found block hash to be different.",
|
|
450
|
+
~kind="gauge",
|
|
451
|
+
~entries=chains,
|
|
452
|
+
~value=m => m.reorgDetectedBlock->Option.map(Int.toFloat),
|
|
453
|
+
)
|
|
454
|
+
b->single(
|
|
455
|
+
~name="envio_reorg_threshold",
|
|
456
|
+
~help="Whether indexing is currently within the reorg threshold",
|
|
457
|
+
~kind="gauge",
|
|
458
|
+
~value=metrics.isInReorgThreshold ? 1. : 0.,
|
|
459
|
+
)
|
|
460
|
+
b->single(
|
|
461
|
+
~name="envio_rollback_enabled",
|
|
462
|
+
~help="Whether rollback on reorg is enabled",
|
|
463
|
+
~kind="gauge",
|
|
464
|
+
~value=metrics.rollbackEnabled ? 1. : 0.,
|
|
465
|
+
)
|
|
466
|
+
b->single(
|
|
467
|
+
~name="envio_rollback_seconds",
|
|
468
|
+
~help="Rollback on reorg total time.",
|
|
469
|
+
~kind="counter",
|
|
470
|
+
~value=metrics.rollbackSeconds,
|
|
471
|
+
)
|
|
472
|
+
b->single(
|
|
473
|
+
~name="envio_rollback_total",
|
|
474
|
+
~help="Number of successful rollbacks on reorg",
|
|
475
|
+
~kind="counter",
|
|
476
|
+
~value=metrics.rollbackCount->Int.toFloat,
|
|
477
|
+
)
|
|
478
|
+
b->single(
|
|
479
|
+
~name="envio_rollback_events",
|
|
480
|
+
~help="Number of events rollbacked on reorg",
|
|
481
|
+
~kind="counter",
|
|
482
|
+
~value=metrics.rollbackEventsCount,
|
|
483
|
+
)
|
|
484
|
+
b->series(
|
|
485
|
+
~name="envio_rollback_history_prune_seconds",
|
|
486
|
+
~help="The total time spent pruning entity history which is not in the reorg threshold.",
|
|
487
|
+
~kind="counter",
|
|
488
|
+
~entries=historyPrunes,
|
|
489
|
+
~value=s => s.seconds,
|
|
490
|
+
)
|
|
491
|
+
b->series(
|
|
492
|
+
~name="envio_rollback_history_prune_total",
|
|
493
|
+
~help="Number of successful entity history prunes",
|
|
494
|
+
~kind="counter",
|
|
495
|
+
~entries=historyPrunes,
|
|
496
|
+
~value=s => s.count->Int.toFloat,
|
|
497
|
+
)
|
|
498
|
+
b->seriesOpt(
|
|
499
|
+
~name="envio_rollback_target_block",
|
|
500
|
+
~help="The block number reorg was rollbacked to the last time.",
|
|
501
|
+
~kind="gauge",
|
|
502
|
+
~entries=chains,
|
|
503
|
+
~value=m => m.rollbackTargetBlock->Option.map(Int.toFloat),
|
|
504
|
+
)
|
|
505
|
+
b->single(
|
|
506
|
+
~name="envio_processing_max_batch_size",
|
|
507
|
+
~help="The maximum number of items to process in a single batch.",
|
|
508
|
+
~kind="gauge",
|
|
509
|
+
~value=metrics.maxBatchSize->Int.toFloat,
|
|
510
|
+
)
|
|
511
|
+
b->series(
|
|
512
|
+
~name="envio_progress_block",
|
|
513
|
+
~help="The block number of the latest block processed and stored in the database.",
|
|
514
|
+
~kind="gauge",
|
|
515
|
+
~entries=chains,
|
|
516
|
+
~value=m => m.progressBlockNumber->Int.toFloat,
|
|
517
|
+
)
|
|
518
|
+
b->series(
|
|
519
|
+
~name="envio_progress_events",
|
|
520
|
+
~help="The number of events processed and reflected in the database.",
|
|
521
|
+
~kind="gauge",
|
|
522
|
+
~entries=chains,
|
|
523
|
+
~value=m => m.numEventsProcessed,
|
|
524
|
+
)
|
|
525
|
+
b->seriesOpt(
|
|
526
|
+
~name="envio_progress_latency",
|
|
527
|
+
~help="The latency in milliseconds between the latest processed event creation and the time it was written to storage.",
|
|
528
|
+
~kind="gauge",
|
|
529
|
+
~entries=chains,
|
|
530
|
+
~value=m => m.progressLatencyMs->Option.map(Int.toFloat),
|
|
531
|
+
)
|
|
532
|
+
// Effects that were never called (e.g. seeded from the persisted cache only)
|
|
533
|
+
// get no call samples.
|
|
534
|
+
let ifCalled = (s: effectMetrics, value) =>
|
|
535
|
+
s.callCount > 0. || s.activeCallsCount > 0 ? Some(value) : None
|
|
536
|
+
b->seriesOpt(
|
|
537
|
+
~name="envio_effect_call_seconds",
|
|
538
|
+
~help="Processing time taken to call the Effect function.",
|
|
539
|
+
~kind="counter",
|
|
540
|
+
~entries=effects,
|
|
541
|
+
~value=s => s->ifCalled(s.callSeconds),
|
|
542
|
+
)
|
|
543
|
+
b->seriesOpt(
|
|
544
|
+
~name="envio_effect_call_seconds_total",
|
|
545
|
+
~help="Cumulative time spent calling the Effect function during the indexing process.",
|
|
546
|
+
~kind="counter",
|
|
547
|
+
~entries=effects,
|
|
548
|
+
~value=s => s->ifCalled(s.callSecondsTotal),
|
|
549
|
+
)
|
|
550
|
+
b->seriesOpt(
|
|
551
|
+
~name="envio_effect_call_total",
|
|
552
|
+
~help="Cumulative number of resolved Effect function calls during the indexing process.",
|
|
553
|
+
~kind="counter",
|
|
554
|
+
~entries=effects,
|
|
555
|
+
~value=s => s->ifCalled(s.callCount),
|
|
556
|
+
)
|
|
557
|
+
b->seriesOpt(
|
|
558
|
+
~name="envio_effect_active_calls",
|
|
559
|
+
~help="The number of Effect function calls that are currently running.",
|
|
560
|
+
~kind="gauge",
|
|
561
|
+
~entries=effects,
|
|
562
|
+
~value=s => s->ifCalled(s.activeCallsCount->Int.toFloat),
|
|
563
|
+
)
|
|
564
|
+
// Only effects that persist their cache get a sample (including a zero for
|
|
565
|
+
// an existing but empty persisted table); a plain effect gets none.
|
|
566
|
+
b->seriesOpt(
|
|
567
|
+
~name="envio_effect_cache",
|
|
568
|
+
~help="The number of items in the effect cache.",
|
|
569
|
+
~kind="gauge",
|
|
570
|
+
~entries=effects,
|
|
571
|
+
~value=s => s.cacheCount->Option.map(Int.toFloat),
|
|
572
|
+
)
|
|
573
|
+
// Unlike the rest of the effect metrics, invalidations and queue waits keep
|
|
574
|
+
// the effect-only label set, aggregated across scopes.
|
|
575
|
+
let effectTotals = {
|
|
576
|
+
let byEffect = Dict.make()
|
|
577
|
+
metrics.effects->Array.forEach(s => {
|
|
578
|
+
switch byEffect->Utils.Dict.dangerouslyGetNonOption(s.effect) {
|
|
579
|
+
| Some((invalidations, queueWaitSeconds)) =>
|
|
580
|
+
byEffect->Dict.set(
|
|
581
|
+
s.effect,
|
|
582
|
+
(invalidations +. s.invalidationsCount, queueWaitSeconds +. s.queueWaitSeconds),
|
|
583
|
+
)
|
|
584
|
+
| None => byEffect->Dict.set(s.effect, (s.invalidationsCount, s.queueWaitSeconds))
|
|
585
|
+
}
|
|
586
|
+
})
|
|
587
|
+
let entries = []
|
|
588
|
+
byEffect->Utils.Dict.forEachWithKey((totals, effect) =>
|
|
589
|
+
entries->Array.push((`{effect="${effect->escapeLabelValue}"}`, totals))
|
|
590
|
+
)
|
|
591
|
+
entries
|
|
592
|
+
}
|
|
593
|
+
b->seriesOpt(
|
|
594
|
+
~name="envio_effect_cache_invalidations",
|
|
595
|
+
~help="The number of effect cache invalidations.",
|
|
596
|
+
~kind="counter",
|
|
597
|
+
~entries=effectTotals,
|
|
598
|
+
~value=((invalidations, _)) => invalidations > 0. ? Some(invalidations) : None,
|
|
599
|
+
)
|
|
600
|
+
b->seriesOpt(
|
|
601
|
+
~name="envio_effect_queue",
|
|
602
|
+
~help="The number of effect calls waiting in the rate limit queue.",
|
|
603
|
+
~kind="gauge",
|
|
604
|
+
~entries=effects,
|
|
605
|
+
~value=s =>
|
|
606
|
+
s.queueCount > 0 || s.queueWaitSeconds > 0. ? Some(s.queueCount->Int.toFloat) : None,
|
|
607
|
+
)
|
|
608
|
+
b->seriesOpt(
|
|
609
|
+
~name="envio_effect_queue_wait_seconds",
|
|
610
|
+
~help="The time spent waiting in the rate limit queue.",
|
|
611
|
+
~kind="counter",
|
|
612
|
+
~entries=effectTotals,
|
|
613
|
+
~value=((_, queueWaitSeconds)) => queueWaitSeconds !== 0. ? Some(queueWaitSeconds) : None,
|
|
614
|
+
)
|
|
615
|
+
b->series(
|
|
616
|
+
~name="envio_storage_load_seconds",
|
|
617
|
+
~help="Processing time taken to load data from storage.",
|
|
618
|
+
~kind="counter",
|
|
619
|
+
~entries=storageLoads,
|
|
620
|
+
~value=s => s.seconds,
|
|
621
|
+
)
|
|
622
|
+
b->series(
|
|
623
|
+
~name="envio_storage_load_seconds_total",
|
|
624
|
+
~help="Cumulative time spent loading data from storage during the indexing process.",
|
|
625
|
+
~kind="counter",
|
|
626
|
+
~entries=storageLoads,
|
|
627
|
+
~value=s => s.secondsTotal,
|
|
628
|
+
)
|
|
629
|
+
b->series(
|
|
630
|
+
~name="envio_storage_load_total",
|
|
631
|
+
~help="Cumulative number of successful storage load operations during the indexing process.",
|
|
632
|
+
~kind="counter",
|
|
633
|
+
~entries=storageLoads,
|
|
634
|
+
~value=s => s.count,
|
|
635
|
+
)
|
|
636
|
+
b->series(
|
|
637
|
+
~name="envio_storage_load_where_size",
|
|
638
|
+
~help="Cumulative number of filter conditions ('where' items) used in storage load operations during the indexing process.",
|
|
639
|
+
~kind="counter",
|
|
640
|
+
~entries=storageLoads,
|
|
641
|
+
~value=s => s.whereSize,
|
|
642
|
+
)
|
|
643
|
+
b->series(
|
|
644
|
+
~name="envio_storage_load_size",
|
|
645
|
+
~help="Cumulative number of records loaded from storage during the indexing process.",
|
|
646
|
+
~kind="counter",
|
|
647
|
+
~entries=storageLoads,
|
|
648
|
+
~value=s => s.size,
|
|
649
|
+
)
|
|
650
|
+
b->series(
|
|
651
|
+
~name="envio_storage_write_seconds",
|
|
652
|
+
~help="Cumulative time spent writing batch data to storage.",
|
|
653
|
+
~kind="counter",
|
|
654
|
+
~entries=storageWrites,
|
|
655
|
+
~value=s => s.seconds,
|
|
656
|
+
)
|
|
657
|
+
b->series(
|
|
658
|
+
~name="envio_storage_write_total",
|
|
659
|
+
~help="Cumulative number of successful storage write operations during the indexing process.",
|
|
660
|
+
~kind="counter",
|
|
661
|
+
~entries=storageWrites,
|
|
662
|
+
~value=s => s.count->Int.toFloat,
|
|
663
|
+
)
|
|
664
|
+
b->series(
|
|
665
|
+
~name="envio_indexing_addresses",
|
|
666
|
+
~help="The number of addresses indexed on chain. Includes both static and dynamic addresses.",
|
|
667
|
+
~kind="gauge",
|
|
668
|
+
~entries=chains,
|
|
669
|
+
~value=m => m.numAddresses->Int.toFloat,
|
|
670
|
+
)
|
|
55
671
|
}
|
|
56
672
|
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
673
|
+
let contentType = "text/plain; version=0.0.4; charset=utf-8"
|
|
674
|
+
|
|
675
|
+
let collect = (~metrics: option<t>) => {
|
|
676
|
+
let b = {out: ""}
|
|
677
|
+
b->series(
|
|
678
|
+
~name="envio_info",
|
|
679
|
+
~help="Information about the indexer",
|
|
680
|
+
~kind="gauge",
|
|
681
|
+
~entries=[(`{version="${Utils.EnvioPackage.value.version->escapeLabelValue}"}`, ())],
|
|
682
|
+
~value=() => 1.,
|
|
683
|
+
)
|
|
684
|
+
switch metrics {
|
|
685
|
+
| Some(metrics) => b->renderMetrics(metrics)
|
|
686
|
+
| None => ()
|
|
687
|
+
}
|
|
688
|
+
b.out ++ "\n"
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Node.js runtime metrics for /metrics/runtime, read from process/perf_hooks
|
|
692
|
+
// at scrape time. Uses the same metric names prom-client's default collectors
|
|
693
|
+
// exposed here before, so existing dashboards keep working.
|
|
694
|
+
|
|
695
|
+
type gcStat = {mutable count: float, mutable seconds: float}
|
|
696
|
+
type runtimeCollectors = {
|
|
697
|
+
eventLoopDelay: NodeJs.PerfHooks.intervalHistogram,
|
|
698
|
+
// Cumulative GC pause count/time per kind, fed by a "gc" PerformanceObserver.
|
|
699
|
+
gcStats: dict<gcStat>,
|
|
700
|
+
// Fixed at collector start; recomputing per scrape lets rounding/clock jitter
|
|
701
|
+
// move it and read as a phantom restart.
|
|
702
|
+
processStartTimeSeconds: float,
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// perf_hooks GC kind constants: NODE_PERFORMANCE_GC_{MINOR,MAJOR,INCREMENTAL,WEAKCB}.
|
|
706
|
+
let gcKindName = kind =>
|
|
707
|
+
switch kind {
|
|
708
|
+
| 1 => "minor"
|
|
709
|
+
| 4 => "major"
|
|
710
|
+
| 8 => "incremental"
|
|
711
|
+
| 16 => "weakcb"
|
|
712
|
+
| _ => "unknown"
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Started on the first scrape rather than at module load, so importing envio
|
|
716
|
+
// (e.g. from the CLI) never spins up samplers.
|
|
717
|
+
let runtimeCollectors = ref(None)
|
|
718
|
+
|
|
719
|
+
let getRuntimeCollectors = () =>
|
|
720
|
+
switch runtimeCollectors.contents {
|
|
721
|
+
| Some(collectors) => collectors
|
|
722
|
+
| None =>
|
|
723
|
+
let eventLoopDelay = NodeJs.PerfHooks.monitorEventLoopDelay(~options={resolution: 10})
|
|
724
|
+
let _ = eventLoopDelay->NodeJs.PerfHooks.enable
|
|
725
|
+
let gcStats = Dict.make()
|
|
726
|
+
let observer = NodeJs.PerfHooks.makePerformanceObserver(list =>
|
|
727
|
+
list
|
|
728
|
+
->NodeJs.PerfHooks.getEntries
|
|
729
|
+
->Array.forEach(entry => {
|
|
730
|
+
let kind = gcKindName(entry.detail["kind"])
|
|
731
|
+
let stat = switch gcStats->Utils.Dict.dangerouslyGetNonOption(kind) {
|
|
732
|
+
| Some(stat) => stat
|
|
733
|
+
| None =>
|
|
734
|
+
let stat = {count: 0., seconds: 0.}
|
|
735
|
+
gcStats->Dict.set(kind, stat)
|
|
736
|
+
stat
|
|
737
|
+
}
|
|
738
|
+
stat.count = stat.count +. 1.
|
|
739
|
+
stat.seconds = stat.seconds +. entry.duration /. 1000.
|
|
740
|
+
})
|
|
741
|
+
)
|
|
742
|
+
observer->NodeJs.PerfHooks.observe({entryTypes: ["gc"]})
|
|
743
|
+
let collectors = {
|
|
744
|
+
eventLoopDelay,
|
|
745
|
+
gcStats,
|
|
746
|
+
processStartTimeSeconds: Date.now() /. 1000. -. NodeJs.Process.uptime(),
|
|
747
|
+
}
|
|
748
|
+
runtimeCollectors := Some(collectors)
|
|
749
|
+
collectors
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Called at server startup so GC pauses and event-loop delays are covered
|
|
753
|
+
// from the beginning of the run, not from the first scrape.
|
|
754
|
+
let startRuntimeCollectors = () => getRuntimeCollectors()->ignore
|
|
755
|
+
|
|
756
|
+
let collectRuntime = () => {
|
|
757
|
+
let b = {out: ""}
|
|
758
|
+
let memory = NodeJs.Process.memoryUsage()
|
|
759
|
+
let cpu = NodeJs.Process.cpuUsage()
|
|
760
|
+
let elu = NodeJs.PerfHooks.performance->NodeJs.PerfHooks.eventLoopUtilization
|
|
761
|
+
let {eventLoopDelay, gcStats, processStartTimeSeconds} = getRuntimeCollectors()
|
|
762
|
+
b->single(
|
|
763
|
+
~name="process_cpu_user_seconds_total",
|
|
764
|
+
~help="Total user CPU time spent in seconds.",
|
|
765
|
+
~kind="counter",
|
|
766
|
+
~value=cpu.user /. 1_000_000.,
|
|
767
|
+
)
|
|
768
|
+
b->single(
|
|
769
|
+
~name="process_cpu_system_seconds_total",
|
|
770
|
+
~help="Total system CPU time spent in seconds.",
|
|
771
|
+
~kind="counter",
|
|
772
|
+
~value=cpu.system /. 1_000_000.,
|
|
773
|
+
)
|
|
774
|
+
b->single(
|
|
775
|
+
~name="process_cpu_seconds_total",
|
|
776
|
+
~help="Total user and system CPU time spent in seconds.",
|
|
777
|
+
~kind="counter",
|
|
778
|
+
~value=(cpu.user +. cpu.system) /. 1_000_000.,
|
|
779
|
+
)
|
|
780
|
+
b->single(
|
|
781
|
+
~name="process_start_time_seconds",
|
|
782
|
+
~help="Start time of the process since unix epoch in seconds.",
|
|
783
|
+
~kind="gauge",
|
|
784
|
+
~value=processStartTimeSeconds,
|
|
785
|
+
)
|
|
786
|
+
b->single(
|
|
787
|
+
~name="process_resident_memory_bytes",
|
|
788
|
+
~help="Resident memory size in bytes.",
|
|
789
|
+
~kind="gauge",
|
|
790
|
+
~value=memory.rss,
|
|
791
|
+
)
|
|
792
|
+
b->single(
|
|
793
|
+
~name="nodejs_heap_size_total_bytes",
|
|
794
|
+
~help="Process heap size from Node.js in bytes.",
|
|
795
|
+
~kind="gauge",
|
|
796
|
+
~value=memory.heapTotal,
|
|
797
|
+
)
|
|
798
|
+
b->single(
|
|
799
|
+
~name="nodejs_heap_size_used_bytes",
|
|
800
|
+
~help="Process heap size used from Node.js in bytes.",
|
|
801
|
+
~kind="gauge",
|
|
802
|
+
~value=memory.heapUsed,
|
|
803
|
+
)
|
|
804
|
+
b->single(
|
|
805
|
+
~name="nodejs_external_memory_bytes",
|
|
806
|
+
~help="Node.js external memory size in bytes.",
|
|
807
|
+
~kind="gauge",
|
|
808
|
+
~value=memory.external_,
|
|
809
|
+
)
|
|
810
|
+
b->single(
|
|
811
|
+
~name="nodejs_eventloop_utilization",
|
|
812
|
+
~help="Ratio of time the event loop is active, since process start.",
|
|
813
|
+
~kind="gauge",
|
|
814
|
+
~value=elu.utilization,
|
|
815
|
+
)
|
|
816
|
+
// Nanoseconds in the histogram; reset after rendering so each scrape reports
|
|
817
|
+
// the delay distribution since the previous one, matching prom-client. With
|
|
818
|
+
// no samples yet (e.g. the first scrape, which starts the sampler) the
|
|
819
|
+
// histogram reports NaN means and a sentinel min — render 0 instead.
|
|
820
|
+
let hasLagSamples = eventLoopDelay.max > 0.
|
|
821
|
+
let nsToSeconds = ns => hasLagSamples && !(ns->Float.isNaN) ? ns /. 1_000_000_000. : 0.
|
|
822
|
+
b->single(
|
|
823
|
+
~name="nodejs_eventloop_lag_mean_seconds",
|
|
824
|
+
~help="The mean of the recorded event loop delays.",
|
|
825
|
+
~kind="gauge",
|
|
826
|
+
~value=eventLoopDelay.mean->nsToSeconds,
|
|
827
|
+
)
|
|
828
|
+
b->single(
|
|
829
|
+
~name="nodejs_eventloop_lag_min_seconds",
|
|
830
|
+
~help="The minimum recorded event loop delay.",
|
|
831
|
+
~kind="gauge",
|
|
832
|
+
~value=eventLoopDelay.min->nsToSeconds,
|
|
833
|
+
)
|
|
834
|
+
b->single(
|
|
835
|
+
~name="nodejs_eventloop_lag_max_seconds",
|
|
836
|
+
~help="The maximum recorded event loop delay.",
|
|
837
|
+
~kind="gauge",
|
|
838
|
+
~value=eventLoopDelay.max->nsToSeconds,
|
|
839
|
+
)
|
|
840
|
+
b->single(
|
|
841
|
+
~name="nodejs_eventloop_lag_stddev_seconds",
|
|
842
|
+
~help="The standard deviation of the recorded event loop delays.",
|
|
843
|
+
~kind="gauge",
|
|
844
|
+
~value=eventLoopDelay.stddev->nsToSeconds,
|
|
845
|
+
)
|
|
846
|
+
b->single(
|
|
847
|
+
~name="nodejs_eventloop_lag_p50_seconds",
|
|
848
|
+
~help="The 50th percentile of the recorded event loop delays.",
|
|
849
|
+
~kind="gauge",
|
|
850
|
+
~value=eventLoopDelay->NodeJs.PerfHooks.percentile(50)->nsToSeconds,
|
|
851
|
+
)
|
|
852
|
+
b->single(
|
|
853
|
+
~name="nodejs_eventloop_lag_p90_seconds",
|
|
854
|
+
~help="The 90th percentile of the recorded event loop delays.",
|
|
855
|
+
~kind="gauge",
|
|
856
|
+
~value=eventLoopDelay->NodeJs.PerfHooks.percentile(90)->nsToSeconds,
|
|
857
|
+
)
|
|
858
|
+
b->single(
|
|
859
|
+
~name="nodejs_eventloop_lag_p99_seconds",
|
|
860
|
+
~help="The 99th percentile of the recorded event loop delays.",
|
|
861
|
+
~kind="gauge",
|
|
862
|
+
~value=eventLoopDelay->NodeJs.PerfHooks.percentile(99)->nsToSeconds,
|
|
863
|
+
)
|
|
864
|
+
eventLoopDelay->NodeJs.PerfHooks.reset
|
|
865
|
+
let heapSpaces =
|
|
866
|
+
NodeJs.V8.getHeapSpaceStatistics()->Array.map(s => (
|
|
867
|
+
`{space="${s.spaceName->String.replace("_space", "")}"}`,
|
|
868
|
+
s,
|
|
869
|
+
))
|
|
870
|
+
b->series(
|
|
871
|
+
~name="nodejs_heap_space_size_total_bytes",
|
|
872
|
+
~help="Process heap space size total from Node.js in bytes.",
|
|
873
|
+
~kind="gauge",
|
|
874
|
+
~entries=heapSpaces,
|
|
875
|
+
~value=s => s.spaceSize,
|
|
876
|
+
)
|
|
877
|
+
b->series(
|
|
878
|
+
~name="nodejs_heap_space_size_used_bytes",
|
|
879
|
+
~help="Process heap space size used from Node.js in bytes.",
|
|
880
|
+
~kind="gauge",
|
|
881
|
+
~entries=heapSpaces,
|
|
882
|
+
~value=s => s.spaceUsedSize,
|
|
883
|
+
)
|
|
884
|
+
b->series(
|
|
885
|
+
~name="nodejs_heap_space_size_available_bytes",
|
|
886
|
+
~help="Process heap space size available from Node.js in bytes.",
|
|
887
|
+
~kind="gauge",
|
|
888
|
+
~entries=heapSpaces,
|
|
889
|
+
~value=s => s.spaceAvailableSize,
|
|
890
|
+
)
|
|
891
|
+
let activeResources = {
|
|
892
|
+
let byType = Dict.make()
|
|
893
|
+
NodeJs.Process.getActiveResourcesInfo()->Array.forEach(resource => {
|
|
894
|
+
let label = `{type="${resource->escapeLabelValue}"}`
|
|
895
|
+
byType->Dict.set(
|
|
896
|
+
label,
|
|
897
|
+
byType->Utils.Dict.dangerouslyGetNonOption(label)->Option.getOr(0.) +. 1.,
|
|
898
|
+
)
|
|
899
|
+
})
|
|
900
|
+
byType->Dict.toArray
|
|
73
901
|
}
|
|
902
|
+
b->series(
|
|
903
|
+
~name="nodejs_active_resources",
|
|
904
|
+
~help="Number of active resources that are currently keeping the event loop alive, grouped by async resource type.",
|
|
905
|
+
~kind="gauge",
|
|
906
|
+
~entries=activeResources,
|
|
907
|
+
~value=count => count,
|
|
908
|
+
)
|
|
909
|
+
b->single(
|
|
910
|
+
~name="nodejs_active_resources_total",
|
|
911
|
+
~help="Total number of active resources.",
|
|
912
|
+
~kind="gauge",
|
|
913
|
+
~value=activeResources->Array.reduce(0., (acc, (_, count)) => acc +. count),
|
|
914
|
+
)
|
|
915
|
+
let gcEntries = []
|
|
916
|
+
gcStats->Utils.Dict.forEachWithKey((stat, kind) =>
|
|
917
|
+
gcEntries->Array.push((`{kind="${kind}"}`, stat))
|
|
918
|
+
)
|
|
919
|
+
b->series(
|
|
920
|
+
~name="nodejs_gc_duration_seconds_sum",
|
|
921
|
+
~help="Cumulative garbage collection pause time by kind, one of major, minor, incremental or weakcb.",
|
|
922
|
+
~kind="counter",
|
|
923
|
+
~entries=gcEntries,
|
|
924
|
+
~value=s => s.seconds,
|
|
925
|
+
)
|
|
926
|
+
b->series(
|
|
927
|
+
~name="nodejs_gc_duration_seconds_count",
|
|
928
|
+
~help="Number of garbage collection pauses by kind, one of major, minor, incremental or weakcb.",
|
|
929
|
+
~kind="counter",
|
|
930
|
+
~entries=gcEntries,
|
|
931
|
+
~value=s => s.count,
|
|
932
|
+
)
|
|
933
|
+
let version = NodeJs.Process.version
|
|
934
|
+
let versionParts = version->String.replace("v", "")->String.split(".")
|
|
935
|
+
let versionPart = i => versionParts->Array.get(i)->Option.getOr("0")
|
|
936
|
+
b->series(
|
|
937
|
+
~name="nodejs_version_info",
|
|
938
|
+
~help="Node.js version info.",
|
|
939
|
+
~kind="gauge",
|
|
940
|
+
~entries=[
|
|
941
|
+
(
|
|
942
|
+
`{version="${version}",major="${versionPart(0)}",minor="${versionPart(
|
|
943
|
+
1,
|
|
944
|
+
)}",patch="${versionPart(2)}"}`,
|
|
945
|
+
(),
|
|
946
|
+
),
|
|
947
|
+
],
|
|
948
|
+
~value=() => 1.,
|
|
949
|
+
)
|
|
950
|
+
b.out ++ "\n"
|
|
74
951
|
}
|