adonisjs-server-stats 1.4.0 → 1.5.2
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/README.md +272 -142
- package/dist/configure.d.ts.map +1 -1
- package/dist/src/controller/debug_controller.d.ts +2 -2
- package/dist/src/controller/debug_controller.d.ts.map +1 -1
- package/dist/src/controller/server_stats_controller.d.ts +1 -1
- package/dist/src/controller/server_stats_controller.d.ts.map +1 -1
- package/dist/src/dashboard/chart_aggregator.d.ts.map +1 -1
- package/dist/src/dashboard/chart_aggregator.js +8 -8
- package/dist/src/dashboard/dashboard_controller.d.ts +12 -97
- package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_controller.js +244 -522
- package/dist/src/dashboard/dashboard_routes.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_routes.js +7 -2
- package/dist/src/dashboard/dashboard_store.d.ts +6 -3
- package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_store.js +54 -78
- package/dist/src/dashboard/integrations/cache_inspector.d.ts.map +1 -1
- package/dist/src/dashboard/integrations/queue_inspector.d.ts.map +1 -1
- package/dist/src/dashboard/migrator.d.ts.map +1 -1
- package/dist/src/dashboard/migrator.js +3 -1
- package/dist/src/dashboard/models/stats_event.d.ts +1 -1
- package/dist/src/dashboard/models/stats_event.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_query.d.ts +1 -1
- package/dist/src/dashboard/models/stats_query.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_request.d.ts +2 -2
- package/dist/src/dashboard/models/stats_request.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_request.js +1 -1
- package/dist/src/dashboard/models/stats_trace.d.ts +1 -1
- package/dist/src/dashboard/models/stats_trace.d.ts.map +1 -1
- package/dist/src/debug/debug_store.d.ts +6 -6
- package/dist/src/debug/debug_store.d.ts.map +1 -1
- package/dist/src/debug/debug_store.js +10 -10
- package/dist/src/debug/email_collector.d.ts +0 -9
- package/dist/src/debug/email_collector.d.ts.map +1 -1
- package/dist/src/debug/email_collector.js +6 -28
- package/dist/src/debug/event_collector.d.ts +1 -1
- package/dist/src/debug/event_collector.d.ts.map +1 -1
- package/dist/src/debug/event_collector.js +17 -17
- package/dist/src/debug/query_collector.d.ts +1 -1
- package/dist/src/debug/query_collector.d.ts.map +1 -1
- package/dist/src/debug/query_collector.js +13 -14
- package/dist/src/debug/ring_buffer.d.ts.map +1 -1
- package/dist/src/debug/route_inspector.d.ts +1 -1
- package/dist/src/debug/route_inspector.d.ts.map +1 -1
- package/dist/src/debug/route_inspector.js +12 -12
- package/dist/src/debug/trace_collector.d.ts.map +1 -1
- package/dist/src/debug/trace_collector.js +6 -5
- package/dist/src/edge/client/dashboard.css +516 -171
- package/dist/src/edge/client/dashboard.js +2756 -1662
- package/dist/src/edge/client/debug-panel.css +476 -133
- package/dist/src/edge/client/debug-panel.js +1496 -1043
- package/dist/src/edge/client/stats-bar.css +64 -30
- package/dist/src/edge/client/stats-bar.js +598 -319
- package/dist/src/edge/plugin.d.ts +1 -1
- package/dist/src/edge/plugin.d.ts.map +1 -1
- package/dist/src/edge/plugin.js +41 -59
- package/dist/src/edge/views/stats-bar.edge +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.d.ts +4 -4
- package/dist/src/middleware/request_tracking_middleware.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.js +7 -6
- package/dist/src/prometheus/prometheus_collector.d.ts +1 -1
- package/dist/src/prometheus/prometheus_collector.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.d.ts +1 -1
- package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.js +33 -32
- package/dist/src/types.d.ts +2 -2
- package/dist/src/utils/json_helpers.d.ts +8 -0
- package/dist/src/utils/json_helpers.d.ts.map +1 -0
- package/dist/src/utils/json_helpers.js +21 -0
- package/dist/src/utils/mail_helpers.d.ts +13 -0
- package/dist/src/utils/mail_helpers.d.ts.map +1 -0
- package/dist/src/utils/mail_helpers.js +26 -0
- package/dist/src/utils/math_helpers.d.ts +8 -0
- package/dist/src/utils/math_helpers.d.ts.map +1 -0
- package/dist/src/utils/math_helpers.js +11 -0
- package/dist/src/utils/time_helpers.d.ts +12 -0
- package/dist/src/utils/time_helpers.d.ts.map +1 -0
- package/dist/src/utils/time_helpers.js +32 -0
- package/dist/src/utils/transmit_client.d.ts +9 -0
- package/dist/src/utils/transmit_client.d.ts.map +1 -0
- package/dist/src/utils/transmit_client.js +20 -0
- package/package.json +35 -29
package/README.md
CHANGED
|
@@ -19,16 +19,16 @@ Zero frontend dependencies. Zero build step. Just `@serverStats()` and go.
|
|
|
19
19
|
|
|
20
20
|
**Debug toolbar** -- expandable panels for deep inspection:
|
|
21
21
|
|
|
22
|
-
| Queries
|
|
23
|
-
|
|
22
|
+
| Queries | Events |
|
|
23
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
24
24
|
|  |  |
|
|
25
25
|
|
|
26
|
-
| Routes
|
|
27
|
-
|
|
26
|
+
| Routes | Logs |
|
|
27
|
+
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
28
28
|
|  |  |
|
|
29
29
|
|
|
30
|
-
| Emails (custom pane)
|
|
31
|
-
|
|
30
|
+
| Emails (custom pane) |
|
|
31
|
+
| -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
32
32
|
|  |
|
|
33
33
|
|
|
34
34
|
## Features
|
|
@@ -74,9 +74,7 @@ providers: [
|
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
76
|
// start/kernel.ts
|
|
77
|
-
server.use([
|
|
78
|
-
() => import('adonisjs-server-stats/middleware'),
|
|
79
|
-
])
|
|
77
|
+
server.use([() => import('adonisjs-server-stats/middleware')])
|
|
80
78
|
```
|
|
81
79
|
|
|
82
80
|
### 3. Create config
|
|
@@ -87,11 +85,7 @@ import { defineConfig } from 'adonisjs-server-stats'
|
|
|
87
85
|
import { processCollector, systemCollector, httpCollector } from 'adonisjs-server-stats/collectors'
|
|
88
86
|
|
|
89
87
|
export default defineConfig({
|
|
90
|
-
collectors: [
|
|
91
|
-
processCollector(),
|
|
92
|
-
systemCollector(),
|
|
93
|
-
httpCollector(),
|
|
94
|
-
],
|
|
88
|
+
collectors: [processCollector(), systemCollector(), httpCollector()],
|
|
95
89
|
})
|
|
96
90
|
```
|
|
97
91
|
|
|
@@ -165,16 +159,13 @@ export default defineConfig({
|
|
|
165
159
|
})
|
|
166
160
|
```
|
|
167
161
|
|
|
168
|
-
### 4. Add
|
|
162
|
+
### 4. Add routes
|
|
169
163
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.get('/admin/api/server-stats', '#controllers/admin/server_stats_controller.index')
|
|
174
|
-
.use(middleware.superadmin()) // Replace with your own middleware
|
|
175
|
-
```
|
|
164
|
+
The package has three layers of functionality, each with its own routes:
|
|
165
|
+
|
|
166
|
+
#### Stats bar API route (required)
|
|
176
167
|
|
|
177
|
-
|
|
168
|
+
The stats bar polls this endpoint for live metrics. Create a controller and route:
|
|
178
169
|
|
|
179
170
|
```ts
|
|
180
171
|
// app/controllers/admin/server_stats_controller.ts
|
|
@@ -184,13 +175,154 @@ import type { StatsEngine } from 'adonisjs-server-stats'
|
|
|
184
175
|
|
|
185
176
|
export default class ServerStatsController {
|
|
186
177
|
async index({ response }: HttpContext) {
|
|
187
|
-
const engine = await app.container.make('server_stats.engine') as StatsEngine
|
|
178
|
+
const engine = (await app.container.make('server_stats.engine')) as StatsEngine
|
|
188
179
|
return response.json(engine.getLatestStats())
|
|
189
180
|
}
|
|
190
181
|
}
|
|
191
182
|
```
|
|
192
183
|
|
|
193
|
-
|
|
184
|
+
```ts
|
|
185
|
+
// start/routes.ts
|
|
186
|
+
router
|
|
187
|
+
.get('/admin/api/server-stats', '#controllers/admin/server_stats_controller.index')
|
|
188
|
+
.use(middleware.superadmin())
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
> The route path must match `endpoint` in your config (default: `/admin/api/server-stats`).
|
|
192
|
+
|
|
193
|
+
#### Debug toolbar routes (optional -- when `devToolbar.enabled: true`)
|
|
194
|
+
|
|
195
|
+
The debug toolbar panels fetch data from these API endpoints. Create a controller and routes:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
// app/controllers/admin/debug_controller.ts
|
|
199
|
+
import app from '@adonisjs/core/services/app'
|
|
200
|
+
import type { HttpContext } from '@adonisjs/core/http'
|
|
201
|
+
import type { DebugStore } from 'adonisjs-server-stats/debug'
|
|
202
|
+
|
|
203
|
+
export default class DebugController {
|
|
204
|
+
private async getStore(): Promise<DebugStore | null> {
|
|
205
|
+
try {
|
|
206
|
+
return (await app.container.make('debug.store')) as DebugStore
|
|
207
|
+
} catch {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async queries({ response }: HttpContext) {
|
|
213
|
+
const store = await this.getStore()
|
|
214
|
+
if (!store) return response.notFound({ error: 'Debug toolbar not enabled' })
|
|
215
|
+
return response.json({ queries: store.queries.getLatest(500), summary: store.queries.getSummary() })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async events({ response }: HttpContext) {
|
|
219
|
+
const store = await this.getStore()
|
|
220
|
+
if (!store) return response.notFound({ error: 'Debug toolbar not enabled' })
|
|
221
|
+
return response.json({ events: store.events.getLatest(200), total: store.events.getTotalCount() })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async routes({ response }: HttpContext) {
|
|
225
|
+
const store = await this.getStore()
|
|
226
|
+
if (!store) return response.notFound({ error: 'Debug toolbar not enabled' })
|
|
227
|
+
return response.json({ routes: store.routes.getRoutes(), total: store.routes.getRouteCount() })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async logs({ response }: HttpContext) {
|
|
231
|
+
const store = await this.getStore()
|
|
232
|
+
if (!store) return response.notFound({ error: 'Debug toolbar not enabled' })
|
|
233
|
+
return response.json({ logs: store.logs.getLatest(500), total: store.logs.getTotalCount() })
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async emails({ response }: HttpContext) {
|
|
237
|
+
const store = await this.getStore()
|
|
238
|
+
if (!store) return response.notFound({ error: 'Debug toolbar not enabled' })
|
|
239
|
+
const emails = store.emails.getLatest(100)
|
|
240
|
+
const stripped = emails.map(({ html, text, ...rest }) => rest)
|
|
241
|
+
return response.json({ emails: stripped, total: store.emails.getTotalCount() })
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async emailPreview({ params, response }: HttpContext) {
|
|
245
|
+
const store = await this.getStore()
|
|
246
|
+
if (!store) return response.notFound({ error: 'Debug toolbar not enabled' })
|
|
247
|
+
const html = store.emails.getEmailHtml(Number(params.id))
|
|
248
|
+
if (!html) return response.notFound({ error: 'Email not found' })
|
|
249
|
+
return response.header('Content-Type', 'text/html; charset=utf-8').send(html)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async traces({ response }: HttpContext) {
|
|
253
|
+
const store = await this.getStore()
|
|
254
|
+
if (!store) return response.notFound({ error: 'Debug toolbar not enabled' })
|
|
255
|
+
if (!store.traces) return response.json({ traces: [], total: 0 })
|
|
256
|
+
const traces = store.traces.getLatest(100)
|
|
257
|
+
const list = traces.map(({ spans, warnings, ...rest }: any) => ({
|
|
258
|
+
...rest,
|
|
259
|
+
warningCount: warnings.length,
|
|
260
|
+
}))
|
|
261
|
+
return response.json({ traces: list, total: store.traces.getTotalCount() })
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async traceDetail({ params, response }: HttpContext) {
|
|
265
|
+
const store = await this.getStore()
|
|
266
|
+
if (!store) return response.notFound({ error: 'Debug toolbar not enabled' })
|
|
267
|
+
if (!store.traces) return response.notFound({ error: 'Tracing not enabled' })
|
|
268
|
+
const trace = store.traces.getTrace(Number(params.id))
|
|
269
|
+
if (!trace) return response.notFound({ error: 'Trace not found' })
|
|
270
|
+
return response.json(trace)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// start/routes.ts
|
|
277
|
+
router
|
|
278
|
+
.group(() => {
|
|
279
|
+
router.get('queries', '#controllers/admin/debug_controller.queries')
|
|
280
|
+
router.get('events', '#controllers/admin/debug_controller.events')
|
|
281
|
+
router.get('routes', '#controllers/admin/debug_controller.routes')
|
|
282
|
+
router.get('logs', '#controllers/admin/debug_controller.logs')
|
|
283
|
+
router.get('emails', '#controllers/admin/debug_controller.emails')
|
|
284
|
+
router.get('emails/:id/preview', '#controllers/admin/debug_controller.emailPreview')
|
|
285
|
+
router.get('traces', '#controllers/admin/debug_controller.traces')
|
|
286
|
+
router.get('traces/:id', '#controllers/admin/debug_controller.traceDetail')
|
|
287
|
+
})
|
|
288
|
+
.prefix('/admin/api/debug')
|
|
289
|
+
.use(middleware.superadmin())
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### Dashboard routes (automatic -- when `devToolbar.dashboard: true`)
|
|
293
|
+
|
|
294
|
+
The full-page dashboard at `/__stats` **registers its own routes automatically** -- no manual route setup needed. The following routes are created under the configured `dashboardPath` (default: `/__stats`):
|
|
295
|
+
|
|
296
|
+
| Method | Path | Description |
|
|
297
|
+
|--------|------|-------------|
|
|
298
|
+
| GET | `/` | Dashboard page (HTML) |
|
|
299
|
+
| GET | `/api/overview` | Overview metrics |
|
|
300
|
+
| GET | `/api/overview/chart` | Time-series chart data |
|
|
301
|
+
| GET | `/api/requests` | Paginated request history |
|
|
302
|
+
| GET | `/api/requests/:id` | Request detail with queries/trace |
|
|
303
|
+
| GET | `/api/queries` | Paginated query list |
|
|
304
|
+
| GET | `/api/queries/grouped` | Queries grouped by normalized SQL |
|
|
305
|
+
| GET | `/api/queries/:id/explain` | EXPLAIN plan for a query |
|
|
306
|
+
| GET | `/api/events` | Paginated event list |
|
|
307
|
+
| GET | `/api/routes` | Route table |
|
|
308
|
+
| GET | `/api/logs` | Paginated log entries |
|
|
309
|
+
| GET | `/api/emails` | Paginated email list |
|
|
310
|
+
| GET | `/api/emails/:id/preview` | Email HTML preview |
|
|
311
|
+
| GET | `/api/traces` | Paginated trace list |
|
|
312
|
+
| GET | `/api/traces/:id` | Trace detail with spans |
|
|
313
|
+
| GET | `/api/cache` | Cache stats and key listing |
|
|
314
|
+
| GET | `/api/cache/:key` | Cache key detail |
|
|
315
|
+
| GET | `/api/jobs` | Job queue overview |
|
|
316
|
+
| GET | `/api/jobs/:id` | Job detail |
|
|
317
|
+
| POST | `/api/jobs/:id/retry` | Retry a failed job |
|
|
318
|
+
| GET | `/api/config` | App config (secrets redacted) |
|
|
319
|
+
| GET | `/api/filters` | Saved filters |
|
|
320
|
+
| POST | `/api/filters` | Create saved filter |
|
|
321
|
+
| DELETE | `/api/filters/:id` | Delete saved filter |
|
|
322
|
+
|
|
323
|
+
All dashboard routes are gated by the `shouldShow` callback if configured.
|
|
324
|
+
|
|
325
|
+
### 5. Render the stats bar
|
|
194
326
|
|
|
195
327
|
**Edge** (add before `</body>`):
|
|
196
328
|
|
|
@@ -204,36 +336,36 @@ export default class ServerStatsController {
|
|
|
204
336
|
|
|
205
337
|
### `ServerStatsConfig`
|
|
206
338
|
|
|
207
|
-
| Option | Type | Default | Description
|
|
208
|
-
|
|
209
|
-
| `intervalMs` | `number` | `3000` | Collection + broadcast interval (ms)
|
|
210
|
-
| `transport` | `'transmit' \| 'none'` | `'transmit'` | SSE transport. `'none'` = poll-only.
|
|
211
|
-
| `channelName` | `string` | `'admin/server-stats'` | Transmit channel name
|
|
212
|
-
| `endpoint` | `string \| false` | `'/admin/api/server-stats'` | HTTP endpoint. `false` to disable.
|
|
213
|
-
| `collectors` | `MetricCollector[]` | `[]` | Array of collector instances
|
|
214
|
-
| `skipInTest` | `boolean` | `true` | Skip collection during tests
|
|
215
|
-
| `onStats` | `(stats) => void` | -- | Callback after each collection tick
|
|
216
|
-
| `shouldShow` | `(ctx) => boolean` | -- | Per-request visibility guard
|
|
217
|
-
| `devToolbar` | `DevToolbarOptions` | -- | Dev toolbar configuration
|
|
339
|
+
| Option | Type | Default | Description |
|
|
340
|
+
| ------------- | ---------------------- | --------------------------- | ------------------------------------ |
|
|
341
|
+
| `intervalMs` | `number` | `3000` | Collection + broadcast interval (ms) |
|
|
342
|
+
| `transport` | `'transmit' \| 'none'` | `'transmit'` | SSE transport. `'none'` = poll-only. |
|
|
343
|
+
| `channelName` | `string` | `'admin/server-stats'` | Transmit channel name |
|
|
344
|
+
| `endpoint` | `string \| false` | `'/admin/api/server-stats'` | HTTP endpoint. `false` to disable. |
|
|
345
|
+
| `collectors` | `MetricCollector[]` | `[]` | Array of collector instances |
|
|
346
|
+
| `skipInTest` | `boolean` | `true` | Skip collection during tests |
|
|
347
|
+
| `onStats` | `(stats) => void` | -- | Callback after each collection tick |
|
|
348
|
+
| `shouldShow` | `(ctx) => boolean` | -- | Per-request visibility guard |
|
|
349
|
+
| `devToolbar` | `DevToolbarOptions` | -- | Dev toolbar configuration |
|
|
218
350
|
|
|
219
351
|
### `DevToolbarOptions`
|
|
220
352
|
|
|
221
|
-
| Option | Type
|
|
222
|
-
|
|
223
|
-
| `enabled` | `boolean`
|
|
224
|
-
| `maxQueries` | `number`
|
|
225
|
-
| `maxEvents` | `number`
|
|
226
|
-
| `maxEmails` | `number`
|
|
227
|
-
| `slowQueryThresholdMs` | `number`
|
|
228
|
-
| `persistDebugData` | `boolean \| string` | `false`
|
|
229
|
-
| `tracing` | `boolean`
|
|
230
|
-
| `maxTraces` | `number`
|
|
231
|
-
| `dashboard` | `boolean`
|
|
232
|
-
| `dashboardPath` | `string`
|
|
233
|
-
| `retentionDays` | `number`
|
|
234
|
-
| `dbPath` | `string`
|
|
235
|
-
| `excludeFromTracing` | `string[]`
|
|
236
|
-
| `panes` | `DebugPane[]`
|
|
353
|
+
| Option | Type | Default | Description |
|
|
354
|
+
| ---------------------- | ------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
355
|
+
| `enabled` | `boolean` | `false` | Enable the dev toolbar |
|
|
356
|
+
| `maxQueries` | `number` | `500` | Max SQL queries to buffer |
|
|
357
|
+
| `maxEvents` | `number` | `200` | Max events to buffer |
|
|
358
|
+
| `maxEmails` | `number` | `100` | Max emails to buffer |
|
|
359
|
+
| `slowQueryThresholdMs` | `number` | `100` | Slow query threshold (ms) |
|
|
360
|
+
| `persistDebugData` | `boolean \| string` | `false` | Persist debug data to disk across restarts. `true` writes to `.adonisjs/server-stats/debug-data.json`, or pass a custom path. |
|
|
361
|
+
| `tracing` | `boolean` | `false` | Enable per-request tracing with timeline visualization |
|
|
362
|
+
| `maxTraces` | `number` | `200` | Max request traces to buffer |
|
|
363
|
+
| `dashboard` | `boolean` | `false` | Enable the full-page dashboard (requires `better-sqlite3`) |
|
|
364
|
+
| `dashboardPath` | `string` | `'/__stats'` | URL path for the dashboard page |
|
|
365
|
+
| `retentionDays` | `number` | `7` | Days to keep historical data in SQLite |
|
|
366
|
+
| `dbPath` | `string` | `'.adonisjs/server-stats/dashboard.sqlite3'` | Path to the SQLite database file (relative to app root) |
|
|
367
|
+
| `excludeFromTracing` | `string[]` | `['/admin/api/debug', '/admin/api/server-stats']` | URL prefixes to exclude from tracing and dashboard persistence. Requests still count toward HTTP metrics but won't appear in the timeline or be stored. The stats endpoint is always excluded automatically. |
|
|
368
|
+
| `panes` | `DebugPane[]` | -- | Custom debug panel tabs |
|
|
237
369
|
|
|
238
370
|
---
|
|
239
371
|
|
|
@@ -243,27 +375,27 @@ Each collector is a factory function that returns a `MetricCollector`. All colle
|
|
|
243
375
|
|
|
244
376
|
### Built-in Collectors
|
|
245
377
|
|
|
246
|
-
| Collector | Metrics
|
|
247
|
-
|
|
248
|
-
| `processCollector()` | CPU %, event loop lag, heap/RSS memory, uptime, Node version
|
|
249
|
-
| `systemCollector()` | OS load averages, system memory, system uptime
|
|
250
|
-
| `httpCollector(opts?)` | Requests/sec, avg response time, error rate, active connections | optional
|
|
251
|
-
| `dbPoolCollector(opts?)` | Pool used/free/pending/max connections
|
|
252
|
-
| `redisCollector()` | Status, memory, clients, keys, hit rate
|
|
253
|
-
| `queueCollector(opts)` | Active/waiting/delayed/failed jobs, worker count
|
|
254
|
-
| `logCollector(opts)` | Errors/warnings/entries (5m window), entries/minute
|
|
255
|
-
| `appCollector()` | Online users, pending webhooks, pending emails
|
|
378
|
+
| Collector | Metrics | Options | Peer Deps |
|
|
379
|
+
| ------------------------ | --------------------------------------------------------------- | ------------ | ----------------- |
|
|
380
|
+
| `processCollector()` | CPU %, event loop lag, heap/RSS memory, uptime, Node version | none | -- |
|
|
381
|
+
| `systemCollector()` | OS load averages, system memory, system uptime | none | -- |
|
|
382
|
+
| `httpCollector(opts?)` | Requests/sec, avg response time, error rate, active connections | optional | -- |
|
|
383
|
+
| `dbPoolCollector(opts?)` | Pool used/free/pending/max connections | optional | `@adonisjs/lucid` |
|
|
384
|
+
| `redisCollector()` | Status, memory, clients, keys, hit rate | none | `@adonisjs/redis` |
|
|
385
|
+
| `queueCollector(opts)` | Active/waiting/delayed/failed jobs, worker count | **required** | `bullmq` |
|
|
386
|
+
| `logCollector(opts)` | Errors/warnings/entries (5m window), entries/minute | **required** | -- |
|
|
387
|
+
| `appCollector()` | Online users, pending webhooks, pending emails | none | `@adonisjs/lucid` |
|
|
256
388
|
|
|
257
389
|
### Collector Options
|
|
258
390
|
|
|
259
391
|
```ts
|
|
260
392
|
httpCollector({
|
|
261
|
-
maxRecords: 10_000,
|
|
262
|
-
windowMs: 60_000,
|
|
393
|
+
maxRecords: 10_000, // Circular buffer size (default: 10,000)
|
|
394
|
+
windowMs: 60_000, // Rolling window for rate calc (default: 60s)
|
|
263
395
|
})
|
|
264
396
|
|
|
265
397
|
dbPoolCollector({
|
|
266
|
-
connectionName: 'postgres',
|
|
398
|
+
connectionName: 'postgres', // Lucid connection name (default: 'postgres')
|
|
267
399
|
})
|
|
268
400
|
|
|
269
401
|
queueCollector({
|
|
@@ -305,7 +437,7 @@ function diskCollector(): MetricCollector {
|
|
|
305
437
|
export default defineConfig({
|
|
306
438
|
collectors: [
|
|
307
439
|
processCollector(),
|
|
308
|
-
diskCollector(),
|
|
440
|
+
diskCollector(), // mix with built-in collectors
|
|
309
441
|
],
|
|
310
442
|
})
|
|
311
443
|
```
|
|
@@ -369,6 +501,7 @@ The `@serverStats()` Edge tag renders a self-contained stats bar with inline HTM
|
|
|
369
501
|
```
|
|
370
502
|
|
|
371
503
|
Features:
|
|
504
|
+
|
|
372
505
|
- Polls the stats API at the configured interval
|
|
373
506
|
- Color-coded thresholds (green/amber/red)
|
|
374
507
|
- SVG sparkline charts with gradient fills
|
|
@@ -392,37 +525,32 @@ export default defineConfig({
|
|
|
392
525
|
maxEvents: 200,
|
|
393
526
|
maxEmails: 100,
|
|
394
527
|
slowQueryThresholdMs: 100,
|
|
395
|
-
persistDebugData: true,
|
|
396
|
-
tracing: true,
|
|
528
|
+
persistDebugData: true, // or a custom path: 'custom/debug.json'
|
|
529
|
+
tracing: true, // enable per-request timeline
|
|
397
530
|
},
|
|
398
531
|
})
|
|
399
532
|
```
|
|
400
533
|
|
|
401
|
-
Register the debug API routes
|
|
402
|
-
|
|
403
|
-
```ts
|
|
404
|
-
// start/routes.ts
|
|
405
|
-
router
|
|
406
|
-
.group(() => {
|
|
407
|
-
router.get('queries', '#controllers/admin/debug_controller.queries')
|
|
408
|
-
router.get('events', '#controllers/admin/debug_controller.events')
|
|
409
|
-
router.get('routes', '#controllers/admin/debug_controller.routes')
|
|
410
|
-
router.get('emails', '#controllers/admin/debug_controller.emails')
|
|
411
|
-
router.get('emails/:id/preview', '#controllers/admin/debug_controller.emailPreview')
|
|
412
|
-
router.get('traces', '#controllers/admin/debug_controller.traces')
|
|
413
|
-
router.get('traces/:id', '#controllers/admin/debug_controller.traceDetail')
|
|
414
|
-
})
|
|
415
|
-
.prefix('/admin/api/debug')
|
|
416
|
-
.use(middleware.admin())
|
|
417
|
-
```
|
|
534
|
+
Register the debug API routes (see [step 4](#4-add-routes) for the full controller and route setup).
|
|
418
535
|
|
|
419
536
|
### Built-in Emails Tab
|
|
420
537
|
|
|
421
538
|
The debug toolbar captures all emails sent via AdonisJS mail (`mail:sending`, `mail:sent`, `mail:queued`, `queued:mail:error` events). Click any email row to preview its HTML in an iframe.
|
|
422
539
|
|
|
540
|
+
> **Note:** Email previews are rendered in an iframe. If your app uses `@adonisjs/shield` with the default `X-Frame-Options: DENY` header, the preview will be blocked. Change it to `SAMEORIGIN` in your shield config:
|
|
541
|
+
>
|
|
542
|
+
> ```ts
|
|
543
|
+
> // config/shield.ts
|
|
544
|
+
> xFrame: {
|
|
545
|
+
> enabled: true,
|
|
546
|
+
> action: 'SAMEORIGIN',
|
|
547
|
+
> },
|
|
548
|
+
> ```
|
|
549
|
+
|
|
423
550
|
### Persistent Debug Data
|
|
424
551
|
|
|
425
552
|
Enable `persistDebugData: true` to save queries, events, and emails to `.adonisjs/server-stats/debug-data.json`. You can also pass a custom path (relative to app root) like `persistDebugData: 'custom/debug.json'`. Data is:
|
|
553
|
+
|
|
426
554
|
- **Loaded** on server startup (before collectors start)
|
|
427
555
|
- **Flushed** every 30 seconds (handles crashes)
|
|
428
556
|
- **Saved** on graceful shutdown
|
|
@@ -449,14 +577,14 @@ GET /organizations/create 286ms
|
|
|
449
577
|
|
|
450
578
|
#### Span categories
|
|
451
579
|
|
|
452
|
-
| Category | Color | Auto-captured
|
|
453
|
-
|
|
454
|
-
| DB | Purple | `db:query` events
|
|
580
|
+
| Category | Color | Auto-captured |
|
|
581
|
+
| -------- | ------ | ---------------------- |
|
|
582
|
+
| DB | Purple | `db:query` events |
|
|
455
583
|
| Request | Blue | Full request lifecycle |
|
|
456
|
-
| Mail | Green | --
|
|
457
|
-
| Event | Amber | --
|
|
458
|
-
| View | Cyan | --
|
|
459
|
-
| Custom | Gray | Via `trace()` helper
|
|
584
|
+
| Mail | Green | -- |
|
|
585
|
+
| Event | Amber | -- |
|
|
586
|
+
| View | Cyan | -- |
|
|
587
|
+
| Custom | Gray | Via `trace()` helper |
|
|
460
588
|
|
|
461
589
|
#### Custom spans
|
|
462
590
|
|
|
@@ -527,19 +655,19 @@ devToolbar: {
|
|
|
527
655
|
|
|
528
656
|
#### Dashboard Sections
|
|
529
657
|
|
|
530
|
-
| Section
|
|
531
|
-
|
|
532
|
-
| **Overview** | Performance cards (avg/p95 response time, req/min, error rate) with sparkline charts and configurable time ranges (1h/6h/24h/7d)
|
|
533
|
-
| **Requests** | Paginated request history with method, URL, status, duration. Click for detail view with associated queries and trace
|
|
658
|
+
| Section | Description |
|
|
659
|
+
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
660
|
+
| **Overview** | Performance cards (avg/p95 response time, req/min, error rate) with sparkline charts and configurable time ranges (1h/6h/24h/7d) |
|
|
661
|
+
| **Requests** | Paginated request history with method, URL, status, duration. Click for detail view with associated queries and trace |
|
|
534
662
|
| **Queries** | All captured SQL queries with duration, model, connection. Grouped view shows query patterns by normalized SQL. EXPLAIN plan support for SELECT queries |
|
|
535
|
-
| **Events** | Application events captured from the AdonisJS emitter
|
|
536
|
-
| **Routes** | Full route table with methods, patterns, handlers, and middleware stacks
|
|
537
|
-
| **Logs** | Log history with level filtering, text search, and structured JSON field search (e.g. filter by `userId = 5`)
|
|
538
|
-
| **Emails** | Email history with sender, recipient, subject, status. Click for HTML preview in iframe
|
|
539
|
-
| **Timeline** | Per-request waterfall timeline (requires `tracing: true`)
|
|
540
|
-
| **Cache** | Redis key browser with SCAN-based listing, type-aware detail view, and server stats (requires `@adonisjs/redis`)
|
|
541
|
-
| **Jobs** | Queue overview with job listing, detail, and retry for failed jobs (requires `@rlanz/bull-queue`)
|
|
542
|
-
| **Config** | Sanitized view of app configuration and environment variables. Secrets are auto-redacted
|
|
663
|
+
| **Events** | Application events captured from the AdonisJS emitter |
|
|
664
|
+
| **Routes** | Full route table with methods, patterns, handlers, and middleware stacks |
|
|
665
|
+
| **Logs** | Log history with level filtering, text search, and structured JSON field search (e.g. filter by `userId = 5`) |
|
|
666
|
+
| **Emails** | Email history with sender, recipient, subject, status. Click for HTML preview in iframe |
|
|
667
|
+
| **Timeline** | Per-request waterfall timeline (requires `tracing: true`) |
|
|
668
|
+
| **Cache** | Redis key browser with SCAN-based listing, type-aware detail view, and server stats (requires `@adonisjs/redis`) |
|
|
669
|
+
| **Jobs** | Queue overview with job listing, detail, and retry for failed jobs (requires `@rlanz/bull-queue`) |
|
|
670
|
+
| **Config** | Sanitized view of app configuration and environment variables. Secrets are auto-redacted |
|
|
543
671
|
|
|
544
672
|
#### Access Control
|
|
545
673
|
|
|
@@ -562,12 +690,14 @@ When the dashboard is enabled, the debug panel gains link icons on query, event,
|
|
|
562
690
|
#### Real-Time Updates
|
|
563
691
|
|
|
564
692
|
The dashboard supports real-time updates via two mechanisms:
|
|
693
|
+
|
|
565
694
|
- **Transmit (SSE)**: If `@adonisjs/transmit` is installed, the dashboard subscribes to `server-stats/dashboard` for live overview updates
|
|
566
695
|
- **Polling fallback**: If Transmit is not available, the dashboard polls the API at a configurable interval
|
|
567
696
|
|
|
568
697
|
#### Data Storage
|
|
569
698
|
|
|
570
699
|
The dashboard uses a dedicated SQLite database (separate from your app's database) with 8 tables prefixed with `server_stats_`. The database is:
|
|
700
|
+
|
|
571
701
|
- **Auto-migrated** on startup (no manual migration step)
|
|
572
702
|
- **Self-contained** -- uses its own Knex connection, never touches your app's migration history
|
|
573
703
|
- **Self-cleaning** -- old data is automatically purged based on `retentionDays`
|
|
@@ -640,40 +770,40 @@ async webhooks({ response }: HttpContext) {
|
|
|
640
770
|
|
|
641
771
|
#### `DebugPane` Options
|
|
642
772
|
|
|
643
|
-
| Option | Type | Default | Description
|
|
644
|
-
|
|
645
|
-
| `id` | `string` | -- | Unique identifier (also default data key)
|
|
646
|
-
| `label` | `string` | -- | Tab display name
|
|
647
|
-
| `endpoint` | `string` | -- | API endpoint URL
|
|
648
|
-
| `columns` | `DebugPaneColumn[]` | -- | Column definitions
|
|
649
|
-
| `search` | `{ placeholder }` | -- | Enable search bar
|
|
650
|
-
| `dataKey` | `string` | `id` | JSON key for data array (dot notation OK)
|
|
651
|
-
| `fetchOnce` | `boolean` | `false` | Cache after first fetch
|
|
652
|
-
| `clearable` | `boolean` | `false` | Show Clear button
|
|
773
|
+
| Option | Type | Default | Description |
|
|
774
|
+
| ----------- | ------------------- | ------- | ----------------------------------------- |
|
|
775
|
+
| `id` | `string` | -- | Unique identifier (also default data key) |
|
|
776
|
+
| `label` | `string` | -- | Tab display name |
|
|
777
|
+
| `endpoint` | `string` | -- | API endpoint URL |
|
|
778
|
+
| `columns` | `DebugPaneColumn[]` | -- | Column definitions |
|
|
779
|
+
| `search` | `{ placeholder }` | -- | Enable search bar |
|
|
780
|
+
| `dataKey` | `string` | `id` | JSON key for data array (dot notation OK) |
|
|
781
|
+
| `fetchOnce` | `boolean` | `false` | Cache after first fetch |
|
|
782
|
+
| `clearable` | `boolean` | `false` | Show Clear button |
|
|
653
783
|
|
|
654
784
|
#### `DebugPaneColumn` Options
|
|
655
785
|
|
|
656
|
-
| Option | Type | Default | Description
|
|
657
|
-
|
|
658
|
-
| `key` | `string` | -- | JSON field name
|
|
659
|
-
| `label` | `string` | -- | Column header text
|
|
660
|
-
| `width` | `string` | auto | CSS width (e.g. `'60px'`)
|
|
661
|
-
| `format` | `DebugPaneFormatType` | `'text'` | Cell format (see table below)
|
|
662
|
-
| `searchable` | `boolean` | `false` | Include in search filtering
|
|
663
|
-
| `filterable` | `boolean` | `false` | Click to set as search filter
|
|
664
|
-
| `badgeColorMap` | `Record<string, string>` | -- | Value-to-color map for `badge` format
|
|
786
|
+
| Option | Type | Default | Description |
|
|
787
|
+
| --------------- | ------------------------ | -------- | ------------------------------------- |
|
|
788
|
+
| `key` | `string` | -- | JSON field name |
|
|
789
|
+
| `label` | `string` | -- | Column header text |
|
|
790
|
+
| `width` | `string` | auto | CSS width (e.g. `'60px'`) |
|
|
791
|
+
| `format` | `DebugPaneFormatType` | `'text'` | Cell format (see table below) |
|
|
792
|
+
| `searchable` | `boolean` | `false` | Include in search filtering |
|
|
793
|
+
| `filterable` | `boolean` | `false` | Click to set as search filter |
|
|
794
|
+
| `badgeColorMap` | `Record<string, string>` | -- | Value-to-color map for `badge` format |
|
|
665
795
|
|
|
666
796
|
#### Format Types
|
|
667
797
|
|
|
668
|
-
| Format | Renders As
|
|
669
|
-
|
|
670
|
-
| `text` | Escaped plain text
|
|
671
|
-
| `time` | `HH:MM:SS.mmm`
|
|
672
|
-
| `timeAgo` | `3s ago`, `2m ago`
|
|
673
|
-
| `duration` | `X.XXms` with color coding
|
|
674
|
-
| `method` | HTTP method pill badge
|
|
675
|
-
| `json` | Compact preview, click to expand
|
|
676
|
-
| `badge` | Colored pill via `badgeColorMap`
|
|
798
|
+
| Format | Renders As | Expected Input |
|
|
799
|
+
| ---------- | -------------------------------- | ----------------------- |
|
|
800
|
+
| `text` | Escaped plain text | any |
|
|
801
|
+
| `time` | `HH:MM:SS.mmm` | Unix timestamp (ms) |
|
|
802
|
+
| `timeAgo` | `3s ago`, `2m ago` | Unix timestamp (ms) |
|
|
803
|
+
| `duration` | `X.XXms` with color coding | number (ms) |
|
|
804
|
+
| `method` | HTTP method pill badge | `'GET'`, `'POST'`, etc. |
|
|
805
|
+
| `json` | Compact preview, click to expand | object or array |
|
|
806
|
+
| `badge` | Colored pill via `badgeColorMap` | string |
|
|
677
807
|
|
|
678
808
|
Badge colors: `green`, `amber`, `red`, `blue`, `purple`, `muted`
|
|
679
809
|
|
|
@@ -704,6 +834,7 @@ Gauges are updated automatically on each collection tick.
|
|
|
704
834
|
The log stream module watches a JSON log file and broadcasts new entries via Transmit (SSE).
|
|
705
835
|
|
|
706
836
|
**Two purposes:**
|
|
837
|
+
|
|
707
838
|
1. Provides error/warning counts to the stats bar via `logCollector()`
|
|
708
839
|
2. Broadcasts individual log entries to a Transmit channel via `LogStreamProvider`
|
|
709
840
|
|
|
@@ -778,7 +909,6 @@ import type {
|
|
|
778
909
|
QueueRedisConnection,
|
|
779
910
|
LogCollectorOptions,
|
|
780
911
|
} from 'adonisjs-server-stats/collectors'
|
|
781
|
-
|
|
782
912
|
```
|
|
783
913
|
|
|
784
914
|
---
|
|
@@ -787,16 +917,16 @@ import type {
|
|
|
787
917
|
|
|
788
918
|
All integrations use lazy `import()` -- missing peer deps won't crash the app. The corresponding collector simply returns defaults.
|
|
789
919
|
|
|
790
|
-
| Dependency | Required By
|
|
791
|
-
|
|
792
|
-
| `@adonisjs/core` | Everything (required)
|
|
793
|
-
| `@adonisjs/lucid` | `dbPoolCollector`, `appCollector`, dashboard
|
|
794
|
-
| `@adonisjs/redis` | `redisCollector`, dashboard cache inspector
|
|
795
|
-
| `@adonisjs/transmit` | Provider (SSE broadcast), dashboard real-time
|
|
796
|
-
| `@julr/adonisjs-prometheus` | `serverStatsCollector`
|
|
797
|
-
| `bullmq` | `queueCollector`
|
|
798
|
-
| `better-sqlite3` | Dashboard (`dashboard: true`)
|
|
799
|
-
| `edge.js` | Edge tag
|
|
920
|
+
| Dependency | Required By |
|
|
921
|
+
| --------------------------- | --------------------------------------------- |
|
|
922
|
+
| `@adonisjs/core` | Everything (required) |
|
|
923
|
+
| `@adonisjs/lucid` | `dbPoolCollector`, `appCollector`, dashboard |
|
|
924
|
+
| `@adonisjs/redis` | `redisCollector`, dashboard cache inspector |
|
|
925
|
+
| `@adonisjs/transmit` | Provider (SSE broadcast), dashboard real-time |
|
|
926
|
+
| `@julr/adonisjs-prometheus` | `serverStatsCollector` |
|
|
927
|
+
| `bullmq` | `queueCollector` |
|
|
928
|
+
| `better-sqlite3` | Dashboard (`dashboard: true`) |
|
|
929
|
+
| `edge.js` | Edge tag |
|
|
800
930
|
|
|
801
931
|
## License
|
|
802
932
|
|
package/dist/configure.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../configure.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"configure.d.ts","sourceRoot":"","sources":["../configure.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,SAAS,MAAM,mCAAmC,CAAA;AAE9D,wBAAsB,SAAS,CAAC,OAAO,EAAE,SAAS,iBAWjD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { DebugStore } from '../debug/debug_store.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
3
|
export default class DebugController {
|
|
4
4
|
private store;
|
|
5
5
|
constructor(store: DebugStore);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"debug_controller.d.ts","sourceRoot":"","sources":["../../../src/controller/debug_controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"debug_controller.d.ts","sourceRoot":"","sources":["../../../src/controller/debug_controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEtD,MAAM,CAAC,OAAO,OAAO,eAAe;IACtB,OAAO,CAAC,KAAK;gBAAL,KAAK,EAAE,UAAU;IAE/B,OAAO,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAMjC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAKhC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAKhC,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAOhC,YAAY,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,WAAW;IAS9C,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;IAahC,WAAW,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,WAAW;CAWpD"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
1
|
import type { StatsEngine } from '../engine/stats_engine.js';
|
|
2
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
3
3
|
export default class ServerStatsController {
|
|
4
4
|
private engine;
|
|
5
5
|
constructor(engine: StatsEngine);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server_stats_controller.d.ts","sourceRoot":"","sources":["../../../src/controller/server_stats_controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"server_stats_controller.d.ts","sourceRoot":"","sources":["../../../src/controller/server_stats_controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEtD,MAAM,CAAC,OAAO,OAAO,qBAAqB;IAC5B,OAAO,CAAC,MAAM;gBAAN,MAAM,EAAE,WAAW;IAEjC,KAAK,CAAC,EAAE,QAAQ,EAAE,EAAE,WAAW;CAItC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart_aggregator.d.ts","sourceRoot":"","sources":["../../../src/dashboard/chart_aggregator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chart_aggregator.d.ts","sourceRoot":"","sources":["../../../src/dashboard/chart_aggregator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEhC;;;;;;;;;;GAUG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,KAAK,CAA8C;gBAE/C,EAAE,EAAE,IAAI;IAIpB,KAAK,IAAI,IAAI;IAQb,IAAI,IAAI,IAAI;YAOE,SAAS;CA0DxB"}
|