Repository navigation
Expand file tree
/
Copy pathmetrics.ts
More file actions
301 lines (262 loc) · 11.4 KB
/
Copy pathmetrics.ts
File metadata and controls
301 lines (262 loc) · 11.4 KB
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
import { Registry, Counter, Gauge, Histogram } from 'prom-client'
export const register = new Registry()
// Standard metrics
register.setDefaultLabels({
app: 'lens'
})
// Ingest metrics are network-scoped. One ingester set runs per enabled network
// against one shared Registry, so without `network` the testnet loop and the
// mainnet loop write to the SAME series for the same pair — the gauge case
// being worse than a merge, because `last_trade_timestamp.set()` is a
// last-writer-wins overwrite rather than an additive increment. That made the
// only staleness signal Lens exports unusable on a dual-network deployment.
//
// `network` is deliberately the ONLY added label. See docs/ingest-metrics.md:
// series count is `pairs x networks`, which is bounded by configuration.
// Do NOT add `issuer` or a raw pool id here — the cardinality notes in that
// doc explain why those labels do not belong on these metrics.
export const ingesterRestarts = new Counter({
name: 'ingester_restarts_total',
help: 'Times an ingester crashed and was restarted, by ingester and network',
// Bounded: one series per (venue, network) pair the process actually runs,
// the same shape as the metrics below. A crash message is deliberately NOT a
// label — it is unbounded and arrives from upstream.
labelNames: ['ingester', 'network'],
registers: [register]
})
export const trades_ingested_total = new Counter({
name: 'trades_ingested_total',
help: 'Total number of trades ingested from SDEX/AMM',
labelNames: ['pair', 'network'],
registers: [register]
})
export const amm_snapshots_total = new Counter({
name: 'amm_snapshots_total',
help: 'Total number of AMM pool snapshots captured',
labelNames: ['pool', 'network'],
registers: [register]
})
export const price_snapshots_total = new Counter({
name: 'price_snapshots_total',
help: 'Total number of 1-minute price snapshots appended',
labelNames: ['network'],
registers: [register]
})
export const blend_pool_errors_total = new Counter({
name: 'blend_pool_errors_total',
help: 'Total number of Blend pool read failures',
labelNames: ['pool', 'network'],
registers: [register],
})
export const blend_rates_ingested_total = new Counter({
name: 'blend_rates_ingested_total',
help: 'Total number of Blend pool rate records ingested',
labelNames: ['pool', 'network'],
registers: [register],
})
export const ingester_abandoned_cycles_total = new Counter({
name: 'ingester_abandoned_cycles_total',
help: 'Total number of ingester cycles abandoned after exceeding their per-cycle deadline',
// `venue` is the ingester loop's own name (SDEX, AMM, Soroswap, Aquarius,
// Snapshot) — a bounded set hard-coded in the adapter registry, exactly like
// the network factor above. Together they give `venues x networks` series:
// this is the label #351 asks for, so an on-call can tell WHICH loop is
// stalling on WHICH network without an unbounded dimension ever appearing.
labelNames: ['venue', 'network'],
registers: [register]
})
export const price_points_pruned_total = new Counter({
name: 'price_points_pruned_total',
help: 'Total number of old price points pruned from the database',
labelNames: ['network'],
registers: [register]
})
export const price_points_rolled_up_total = new Counter({
name: 'price_points_rolled_up_total',
help: 'Total number of old price points rolled up into aggregates before pruning',
labelNames: ['network'],
registers: [register]
})
export const price_requests_total = new Counter({
name: 'price_requests_total',
help: 'Total number of price API requests served',
registers: [register]
})
export const active_subscriptions = new Gauge({
name: 'active_subscriptions',
help: 'Currently active GraphQL priceUpdated subscriptions, by pair',
// Deliberately the only label. `pair` is what the subscriber asked for, so
// the distribution across pairs is visible; `network` stays out because the
// gauge would double the series for the same routing decision (#373).
// Unlike the ws_* metrics in ws/fanout.ts this label is subscriber-supplied
// and theoretically unbounded — a client can mint one series per distinct
// pair string. Guarded in practice: only registered deployments accept
// subscriptions, and a pair that is not watched never receives data.
labelNames: ['pair'],
registers: [register]
})
export const price_rejected_total = new Counter({
name: 'price_rejected_total',
help: 'Total number of prices rejected due to plausibility sanity bounds',
labelNames: ['pair', 'network', 'venue', 'reason'],
registers: [register]
})
export const api_key_auth_failures_total = new Counter({
name: 'api_key_auth_failures_total',
help: 'Total number of API key authentication failures, by bounded reason',
labelNames: ['reason'],
registers: [register]
})
export const admin_auth_failures_total = new Counter({
name: 'admin_auth_failures_total',
help: 'Total number of admin authentication failures',
registers: [register]
})
export const api_key_cache_lookups_total = new Counter({
name: 'api_key_cache_lookups_total',
help: 'API key auth cache lookups by result. Hit rate = hit / (hit + miss).',
// `result` is bounded to hit|miss, so cardinality is fixed at two series.
// A hit is served entirely from the in-process cache with no Postgres query;
// a miss falls through to `prisma.apiKey.findUnique`.
labelNames: ['result'],
registers: [register]
})
export const rate_limit_rejections_total = new Counter({
name: 'rate_limit_rejections_total',
help: 'Total number of requests rejected by the per-key or fallback IP rate limiter',
labelNames: ['scope'],
registers: [register]
})
export const global_shed_requests_total = new Counter({
name: 'global_shed_requests_total',
help: 'Total number of requests shed by the per-instance global request ceiling',
registers: [register]
})
export const x402_payments_received_total = new Counter({
name: 'x402_payments_received_total',
help: 'Total number of valid x402 payments received',
registers: [register]
})
export const webhook_delivery_failures_total = new Counter({
name: 'webhook_delivery_failures_total',
help: 'Permanent webhook delivery failures by bounded failure category',
labelNames: ['category'],
registers: [register]
})
export const process_crashes_total = new Counter({
name: 'process_crashes_total',
help: 'Total number of fatal process errors, by event',
labelNames: ['event'],
registers: [register]
})
export const last_trade_timestamp = new Gauge({
name: 'last_trade_timestamp',
help: 'Unix timestamp of the last trade ingested for a pair',
labelNames: ['pair', 'network'],
registers: [register]
})
export const horizon_rate_limit_remaining = new Gauge({
name: 'horizon_rate_limit_remaining',
help: 'Remaining Horizon API requests in the current rate-limit window',
labelNames: ['network'],
registers: [register],
})
// Ingestion health is labelled only by the finite adapter registry and the
// two supported networks. Do not add pair or pool identifiers here: they make
// the series count grow with user configuration and on-chain pool creation.
export const ingest_last_success_timestamp = new Gauge({
name: 'ingest_last_success_timestamp',
help: 'Unix timestamp of the last successful price write by venue and network',
labelNames: ['venue', 'network'],
registers: [register],
})
export const ingest_cycle_duration_seconds = new Histogram({
name: 'ingest_cycle_duration_seconds',
help: 'Duration of a venue ingestion cycle in seconds',
labelNames: ['venue', 'network'],
// Venue polls are mostly sub-second, but Horizon and Soroswap requests can
// take several seconds. These buckets expose normal cycles and slow calls.
buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60],
registers: [register],
})
export const ingest_errors_total = new Counter({
name: 'ingest_errors_total',
help: 'Ingestion cycle errors by venue, network and bounded error category',
labelNames: ['venue', 'network', 'category'],
registers: [register],
})
/** Record a successful persisted price write without high-cardinality labels. */
export function markIngestSuccess(venue: string, network: string): void {
ingest_last_success_timestamp.set({ venue, network }, Math.floor(Date.now() / 1000))
}
export const http_requests_total = new Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests served, by method, route template and status class',
// `route` is the matched Fastify route TEMPLATE (e.g. /price/:assetA/:assetB),
// never the resolved URL — resolving it would mint a new time series per
// asset pair, address and cursor and eventually overwhelm Prometheus.
// `status_class` is 2xx/4xx/5xx rather than the exact code for the same
// reason; add an exact-status label only if a concrete need appears.
labelNames: ['method', 'route', 'status_class'],
registers: [register]
})
export const http_request_duration_seconds = new Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds, by method and route template',
labelNames: ['method', 'route'],
// Buckets are chosen for THIS service, not prom-client's defaults. The
// defaults top out at 10s and cluster below 1s, which suits fast in-process
// handlers; Lens handlers are not that. A typical request does a Redis cache
// lookup (single-digit ms) or, on a miss, a Postgres aggregate query plus —
// for /price and /route — Horizon/Soroswap network round-trips. So we want
// fine resolution across two regions:
// 5ms–50ms cache hits, the common fast path; enough granularity to see
// the Redis path regress before users notice.
// 100ms–2.5s DB aggregates and upstream venue calls, where p95 actually
// lives and where an SLO would be set.
// The 5s and 10s buckets exist to catch upstream stalls short of a timeout;
// anything slower falls in +Inf and shows up as a saturated p99.
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
registers: [register]
})
export const db_query_duration_seconds = new Histogram({
name: 'db_query_duration_seconds',
help: 'Duration of database queries in seconds',
buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
registers: [register]
})
// Database connection pool utilisation metrics
export const db_pool_connections_in_use = new Gauge({
name: 'db_pool_connections_in_use',
help: 'Number of database connections currently checked out / in use',
registers: [register],
})
export const db_pool_connections_idle = new Gauge({
name: 'db_pool_connections_idle',
help: 'Number of idle database connections available in the pool',
registers: [register],
})
export const db_pool_connections_waiting = new Gauge({
name: 'db_pool_connections_waiting',
help: 'Number of queued requests currently waiting for a database connection',
registers: [register],
})
export const db_pool_exhaustion_errors_total = new Counter({
name: 'db_pool_exhaustion_errors_total',
help: 'Total number of database pool acquisition timeouts or exhaustion errors',
registers: [register],
})
let _poolMetricsUpdater: (() => void) | null = null
export function setPoolMetricsUpdater(updater: () => void) {
_poolMetricsUpdater = updater
}
export async function getMetrics() {
if (_poolMetricsUpdater) {
try {
_poolMetricsUpdater()
} catch {
// Best-effort metric collection
}
}
return await register.metrics()
}