adonisjs-server-stats 1.3.2 → 1.5.1
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 +114 -116
- 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 +258 -489
- 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 +39 -3
- package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_store.js +145 -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 +555 -165
- package/dist/src/edge/client/dashboard.js +2797 -1556
- 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 +31 -31
- 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
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
2
|
import { dirname, join } from 'node:path';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { safeParseJson, safeParseJsonArray } from '../utils/json_helpers.js';
|
|
5
|
+
import { round, clamp } from '../utils/math_helpers.js';
|
|
6
|
+
import { loadTransmitClient } from '../utils/transmit_client.js';
|
|
5
7
|
import { CacheInspector } from './integrations/cache_inspector.js';
|
|
6
|
-
import { QueueInspector } from './integrations/queue_inspector.js';
|
|
7
8
|
import { ConfigInspector } from './integrations/config_inspector.js';
|
|
9
|
+
import { QueueInspector } from './integrations/queue_inspector.js';
|
|
8
10
|
const EDGE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'edge');
|
|
11
|
+
function emptyPage(page, perPage) {
|
|
12
|
+
return { data: [], total: 0, page, perPage };
|
|
13
|
+
}
|
|
9
14
|
/**
|
|
10
15
|
* Controller for the full-page dashboard.
|
|
11
16
|
*
|
|
12
17
|
* Serves the dashboard HTML page and all JSON API endpoints.
|
|
13
|
-
*
|
|
14
|
-
* the in-memory DebugStore, and the AdonisJS application instance.
|
|
18
|
+
* Delegates all data access to the DashboardStore.
|
|
15
19
|
*/
|
|
16
20
|
export default class DashboardController {
|
|
17
21
|
dashboardStore;
|
|
@@ -34,17 +38,10 @@ export default class DashboardController {
|
|
|
34
38
|
// ---------------------------------------------------------------------------
|
|
35
39
|
// Page
|
|
36
40
|
// ---------------------------------------------------------------------------
|
|
37
|
-
/**
|
|
38
|
-
* GET {dashboardPath} — Render the dashboard Edge template.
|
|
39
|
-
*
|
|
40
|
-
* Reads the dashboard CSS/JS assets and passes them as template state
|
|
41
|
-
* along with configuration for tracing and custom panes.
|
|
42
|
-
*/
|
|
43
41
|
async page(ctx) {
|
|
44
42
|
if (!this.checkAccess(ctx)) {
|
|
45
43
|
return ctx.response.forbidden({ error: 'Access denied' });
|
|
46
44
|
}
|
|
47
|
-
// Lazily read and cache the CSS/JS assets
|
|
48
45
|
if (!this.cachedCss) {
|
|
49
46
|
this.cachedCss = readFileSync(join(EDGE_DIR, 'client', 'dashboard.css'), 'utf-8');
|
|
50
47
|
}
|
|
@@ -52,7 +49,13 @@ export default class DashboardController {
|
|
|
52
49
|
this.cachedJs = readFileSync(join(EDGE_DIR, 'client', 'dashboard.js'), 'utf-8');
|
|
53
50
|
}
|
|
54
51
|
if (this.cachedTransmitClient === null) {
|
|
55
|
-
this.cachedTransmitClient = this.
|
|
52
|
+
this.cachedTransmitClient = loadTransmitClient(this.app.makePath('package.json'));
|
|
53
|
+
if (this.cachedTransmitClient) {
|
|
54
|
+
console.log('[server-stats] Transmit client loaded for dashboard');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.log('[server-stats] Dashboard will use polling. Install @adonisjs/transmit-client for real-time updates.');
|
|
58
|
+
}
|
|
56
59
|
}
|
|
57
60
|
const config = this.app.config.get('server_stats');
|
|
58
61
|
const toolbarConfig = config?.devToolbar || {};
|
|
@@ -77,26 +80,21 @@ export default class DashboardController {
|
|
|
77
80
|
// ---------------------------------------------------------------------------
|
|
78
81
|
// Overview
|
|
79
82
|
// ---------------------------------------------------------------------------
|
|
80
|
-
/**
|
|
81
|
-
* GET {dashboardPath}/api/overview — Overview metrics cards.
|
|
82
|
-
*/
|
|
83
83
|
async overview({ request, response }) {
|
|
84
|
-
|
|
85
|
-
if (!db)
|
|
86
|
-
return response.json(emptyOverview());
|
|
87
|
-
try {
|
|
84
|
+
return this.withDb(response, emptyOverview(), async () => {
|
|
88
85
|
const range = request.qs().range || '1h';
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
const [overview, widgets, sparklineData] = await Promise.all([
|
|
87
|
+
this.dashboardStore.getOverviewMetrics(range),
|
|
88
|
+
this.dashboardStore.getOverviewWidgets(range),
|
|
89
|
+
this.dashboardStore.getSparklineData(range),
|
|
90
|
+
]);
|
|
91
91
|
if (!overview)
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const sparklineData = metrics.slice(-15);
|
|
99
|
-
return response.json({
|
|
92
|
+
return emptyOverview();
|
|
93
|
+
const [cacheStats, jobQueueStatus] = await Promise.all([
|
|
94
|
+
this.fetchCacheOverview(),
|
|
95
|
+
this.fetchQueueOverview(),
|
|
96
|
+
]);
|
|
97
|
+
return {
|
|
100
98
|
...overview,
|
|
101
99
|
sparklines: {
|
|
102
100
|
avgResponseTime: sparklineData.map((m) => m.avg_duration),
|
|
@@ -104,26 +102,17 @@ export default class DashboardController {
|
|
|
104
102
|
requestsPerMinute: sparklineData.map((m) => m.request_count),
|
|
105
103
|
errorRate: sparklineData.map((m) => m.request_count > 0 ? round((m.error_count / m.request_count) * 100) : 0),
|
|
106
104
|
},
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
105
|
+
...widgets,
|
|
106
|
+
cacheStats,
|
|
107
|
+
jobQueueStatus,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
112
110
|
}
|
|
113
|
-
/**
|
|
114
|
-
* GET {dashboardPath}/api/overview/chart — Chart data with time range.
|
|
115
|
-
*/
|
|
116
111
|
async overviewChart({ request, response }) {
|
|
117
|
-
const db = this.dashboardStore.getDb();
|
|
118
|
-
if (!db)
|
|
119
|
-
return response.json({ buckets: [] });
|
|
120
112
|
const range = request.qs().range || '1h';
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.where('bucket', '>=', cutoff)
|
|
125
|
-
.orderBy('bucket', 'asc');
|
|
126
|
-
return response.json({
|
|
113
|
+
return this.withDb(response, { range, buckets: [] }, async () => {
|
|
114
|
+
const buckets = await this.dashboardStore.getChartData(range);
|
|
115
|
+
return {
|
|
127
116
|
range,
|
|
128
117
|
buckets: buckets.map((b) => ({
|
|
129
118
|
bucket: b.bucket,
|
|
@@ -133,70 +122,42 @@ export default class DashboardController {
|
|
|
133
122
|
errorCount: b.error_count,
|
|
134
123
|
queryCount: b.query_count,
|
|
135
124
|
})),
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return response.json({ buckets: [] });
|
|
140
|
-
}
|
|
125
|
+
};
|
|
126
|
+
});
|
|
141
127
|
}
|
|
142
128
|
// ---------------------------------------------------------------------------
|
|
143
129
|
// Requests
|
|
144
130
|
// ---------------------------------------------------------------------------
|
|
145
|
-
/**
|
|
146
|
-
* GET {dashboardPath}/api/requests — Paginated request history.
|
|
147
|
-
*/
|
|
148
131
|
async requests({ request, response }) {
|
|
149
|
-
const db = this.dashboardStore.getDb();
|
|
150
|
-
if (!db)
|
|
151
|
-
return response.json({ data: [], total: 0 });
|
|
152
132
|
const qs = request.qs();
|
|
153
133
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
154
134
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
query = query.where('status_code', Number(qs.status));
|
|
161
|
-
if (qs.url)
|
|
162
|
-
query = query.where('url', 'like', `%${qs.url}%`);
|
|
163
|
-
const countResult = await query.clone().count('* as total').first();
|
|
164
|
-
const total = countResult?.total ?? 0;
|
|
165
|
-
const data = await query
|
|
166
|
-
.orderBy('created_at', 'desc')
|
|
167
|
-
.limit(perPage)
|
|
168
|
-
.offset((page - 1) * perPage);
|
|
169
|
-
return response.json({
|
|
170
|
-
data: data.map(formatRequest),
|
|
171
|
-
total,
|
|
172
|
-
page,
|
|
173
|
-
perPage,
|
|
135
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
136
|
+
const result = await this.dashboardStore.getRequests(page, perPage, {
|
|
137
|
+
method: qs.method ? qs.method.toUpperCase() : undefined,
|
|
138
|
+
url: qs.url || undefined,
|
|
139
|
+
status: qs.status ? Number(qs.status) : undefined,
|
|
174
140
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
141
|
+
return {
|
|
142
|
+
data: result.data.map(formatRequest),
|
|
143
|
+
total: result.total,
|
|
144
|
+
page: result.page,
|
|
145
|
+
perPage: result.perPage,
|
|
146
|
+
};
|
|
147
|
+
});
|
|
179
148
|
}
|
|
180
|
-
/**
|
|
181
|
-
* GET {dashboardPath}/api/requests/:id — Single request with trace.
|
|
182
|
-
*/
|
|
183
149
|
async requestDetail({ params, response }) {
|
|
184
|
-
|
|
185
|
-
if (!db)
|
|
150
|
+
if (!this.dashboardStore.isReady()) {
|
|
186
151
|
return response.notFound({ error: 'Not found' });
|
|
152
|
+
}
|
|
187
153
|
try {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
if (!req)
|
|
154
|
+
const detail = await this.dashboardStore.getRequestDetail(Number(params.id));
|
|
155
|
+
if (!detail)
|
|
191
156
|
return response.notFound({ error: 'Request not found' });
|
|
192
|
-
const [queries, trace] = await Promise.all([
|
|
193
|
-
db('server_stats_queries').where('request_id', id).orderBy('created_at', 'asc'),
|
|
194
|
-
db('server_stats_traces').where('request_id', id).first(),
|
|
195
|
-
]);
|
|
196
157
|
return response.json({
|
|
197
|
-
...formatRequest(
|
|
198
|
-
queries: queries.map(formatQuery),
|
|
199
|
-
trace: trace ? formatTrace(trace) : null,
|
|
158
|
+
...formatRequest(detail),
|
|
159
|
+
queries: (detail.queries || []).map(formatQuery),
|
|
160
|
+
trace: detail.trace ? formatTrace(detail.trace) : null,
|
|
200
161
|
});
|
|
201
162
|
}
|
|
202
163
|
catch {
|
|
@@ -206,73 +167,33 @@ export default class DashboardController {
|
|
|
206
167
|
// ---------------------------------------------------------------------------
|
|
207
168
|
// Queries
|
|
208
169
|
// ---------------------------------------------------------------------------
|
|
209
|
-
/**
|
|
210
|
-
* GET {dashboardPath}/api/queries — Paginated query history.
|
|
211
|
-
*/
|
|
212
170
|
async queries({ request, response }) {
|
|
213
|
-
const db = this.dashboardStore.getDb();
|
|
214
|
-
if (!db)
|
|
215
|
-
return response.json({ data: [], total: 0 });
|
|
216
171
|
const qs = request.qs();
|
|
217
172
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
218
173
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (qs.method)
|
|
226
|
-
query = query.where('method', qs.method);
|
|
227
|
-
if (qs.connection)
|
|
228
|
-
query = query.where('connection', qs.connection);
|
|
229
|
-
const countResult = await query.clone().count('* as total').first();
|
|
230
|
-
const total = countResult?.total ?? 0;
|
|
231
|
-
const data = await query
|
|
232
|
-
.orderBy('created_at', 'desc')
|
|
233
|
-
.limit(perPage)
|
|
234
|
-
.offset((page - 1) * perPage);
|
|
235
|
-
return response.json({
|
|
236
|
-
data: data.map(formatQuery),
|
|
237
|
-
total,
|
|
238
|
-
page,
|
|
239
|
-
perPage,
|
|
174
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
175
|
+
const result = await this.dashboardStore.getQueries(page, perPage, {
|
|
176
|
+
durationMin: qs.duration_min ? Number(qs.duration_min) : undefined,
|
|
177
|
+
model: qs.model || undefined,
|
|
178
|
+
method: qs.method || undefined,
|
|
179
|
+
connection: qs.connection || undefined,
|
|
240
180
|
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
181
|
+
return {
|
|
182
|
+
data: result.data.map(formatQuery),
|
|
183
|
+
total: result.total,
|
|
184
|
+
page: result.page,
|
|
185
|
+
perPage: result.perPage,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
245
188
|
}
|
|
246
|
-
/**
|
|
247
|
-
* GET {dashboardPath}/api/queries/grouped — Grouped by normalized SQL.
|
|
248
|
-
*/
|
|
249
189
|
async queriesGrouped({ request, response }) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const sort = qs.sort || 'total_duration';
|
|
256
|
-
try {
|
|
257
|
-
const validSorts = {
|
|
258
|
-
count: 'count',
|
|
259
|
-
avg_duration: 'avg_duration',
|
|
260
|
-
total_duration: 'total_duration',
|
|
261
|
-
};
|
|
262
|
-
const orderCol = validSorts[sort] || 'total_duration';
|
|
263
|
-
const groups = await db('server_stats_queries')
|
|
264
|
-
.select('sql_normalized')
|
|
265
|
-
.select(db.raw('COUNT(*) as count'))
|
|
266
|
-
.select(db.raw('AVG(duration) as avg_duration'))
|
|
267
|
-
.select(db.raw('MIN(duration) as min_duration'))
|
|
268
|
-
.select(db.raw('MAX(duration) as max_duration'))
|
|
269
|
-
.select(db.raw('SUM(duration) as total_duration'))
|
|
270
|
-
.groupBy('sql_normalized')
|
|
271
|
-
.orderBy(orderCol, 'desc')
|
|
272
|
-
.limit(limit);
|
|
273
|
-
// Calculate total time for percentage
|
|
190
|
+
return this.withDb(response, { groups: [] }, async () => {
|
|
191
|
+
const qs = request.qs();
|
|
192
|
+
const limit = clamp(Number(qs.limit) || 50, 1, 200);
|
|
193
|
+
const sort = qs.sort || 'total_duration';
|
|
194
|
+
const groups = await this.dashboardStore.getQueriesGrouped(limit, sort);
|
|
274
195
|
const totalTime = groups.reduce((sum, g) => sum + (g.total_duration || 0), 0);
|
|
275
|
-
return
|
|
196
|
+
return {
|
|
276
197
|
groups: groups.map((g) => ({
|
|
277
198
|
sqlNormalized: g.sql_normalized,
|
|
278
199
|
count: g.count,
|
|
@@ -282,30 +203,23 @@ export default class DashboardController {
|
|
|
282
203
|
totalDuration: round(g.total_duration),
|
|
283
204
|
percentOfTotal: totalTime > 0 ? round((g.total_duration / totalTime) * 100) : 0,
|
|
284
205
|
})),
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
return response.json({ groups: [] });
|
|
289
|
-
}
|
|
206
|
+
};
|
|
207
|
+
});
|
|
290
208
|
}
|
|
291
|
-
/**
|
|
292
|
-
* GET {dashboardPath}/api/queries/:id/explain — Run EXPLAIN on a query.
|
|
293
|
-
*/
|
|
294
209
|
async queryExplain({ params, response }) {
|
|
295
|
-
|
|
296
|
-
if (!db)
|
|
210
|
+
if (!this.dashboardStore.isReady()) {
|
|
297
211
|
return response.notFound({ error: 'Not found' });
|
|
212
|
+
}
|
|
298
213
|
try {
|
|
214
|
+
const db = this.dashboardStore.getDb();
|
|
299
215
|
const id = Number(params.id);
|
|
300
216
|
const query = await db('server_stats_queries').where('id', id).first();
|
|
301
217
|
if (!query)
|
|
302
218
|
return response.notFound({ error: 'Query not found' });
|
|
303
|
-
// Only allow EXPLAIN on SELECT queries
|
|
304
219
|
const sqlTrimmed = query.sql_text.trim().toUpperCase();
|
|
305
220
|
if (!sqlTrimmed.startsWith('SELECT')) {
|
|
306
221
|
return response.badRequest({ error: 'EXPLAIN is only supported for SELECT queries' });
|
|
307
222
|
}
|
|
308
|
-
// Run EXPLAIN on the app's default database connection (not the stats connection)
|
|
309
223
|
let appDb;
|
|
310
224
|
try {
|
|
311
225
|
const lucid = await this.app.container.make('lucid.db');
|
|
@@ -314,7 +228,6 @@ export default class DashboardController {
|
|
|
314
228
|
catch {
|
|
315
229
|
return response.serviceUnavailable({ error: 'App database connection not available' });
|
|
316
230
|
}
|
|
317
|
-
// Parse stored bindings and pass them to the EXPLAIN query
|
|
318
231
|
let bindings = [];
|
|
319
232
|
if (query.bindings) {
|
|
320
233
|
try {
|
|
@@ -325,22 +238,15 @@ export default class DashboardController {
|
|
|
325
238
|
}
|
|
326
239
|
}
|
|
327
240
|
const explainResult = await appDb.raw(`EXPLAIN (FORMAT JSON) ${query.sql_text}`, bindings);
|
|
328
|
-
// PostgreSQL with Knex: result is { rows: [...] }
|
|
329
|
-
// Each row has a "QUERY PLAN" key containing the JSON plan
|
|
330
241
|
let plan = [];
|
|
331
242
|
const rawRows = explainResult?.rows ?? (Array.isArray(explainResult) ? explainResult : []);
|
|
332
243
|
if (rawRows.length > 0 && rawRows[0]['QUERY PLAN']) {
|
|
333
|
-
// EXPLAIN (FORMAT JSON) returns a single row with a JSON array
|
|
334
244
|
plan = rawRows[0]['QUERY PLAN'];
|
|
335
245
|
}
|
|
336
246
|
else {
|
|
337
247
|
plan = rawRows;
|
|
338
248
|
}
|
|
339
|
-
return response.json({
|
|
340
|
-
queryId: id,
|
|
341
|
-
sql: query.sql_text,
|
|
342
|
-
plan,
|
|
343
|
-
});
|
|
249
|
+
return response.json({ queryId: id, sql: query.sql_text, plan });
|
|
344
250
|
}
|
|
345
251
|
catch (error) {
|
|
346
252
|
return response.internalServerError({
|
|
@@ -352,49 +258,31 @@ export default class DashboardController {
|
|
|
352
258
|
// ---------------------------------------------------------------------------
|
|
353
259
|
// Events
|
|
354
260
|
// ---------------------------------------------------------------------------
|
|
355
|
-
/**
|
|
356
|
-
* GET {dashboardPath}/api/events — Paginated event history.
|
|
357
|
-
*/
|
|
358
261
|
async events({ request, response }) {
|
|
359
|
-
const db = this.dashboardStore.getDb();
|
|
360
|
-
if (!db)
|
|
361
|
-
return response.json({ data: [], total: 0 });
|
|
362
262
|
const qs = request.qs();
|
|
363
263
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
364
264
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const data = await query
|
|
372
|
-
.orderBy('created_at', 'desc')
|
|
373
|
-
.limit(perPage)
|
|
374
|
-
.offset((page - 1) * perPage);
|
|
375
|
-
return response.json({
|
|
376
|
-
data: data.map((e) => ({
|
|
265
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
266
|
+
const result = await this.dashboardStore.getEvents(page, perPage, {
|
|
267
|
+
eventName: qs.event_name || undefined,
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
data: result.data.map((e) => ({
|
|
377
271
|
id: e.id,
|
|
378
272
|
requestId: e.request_id,
|
|
379
273
|
eventName: e.event_name,
|
|
380
274
|
data: safeParseJson(e.data),
|
|
381
275
|
createdAt: e.created_at,
|
|
382
276
|
})),
|
|
383
|
-
total,
|
|
384
|
-
page,
|
|
385
|
-
perPage,
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
catch {
|
|
389
|
-
return response.json({ data: [], total: 0 });
|
|
390
|
-
}
|
|
277
|
+
total: result.total,
|
|
278
|
+
page: result.page,
|
|
279
|
+
perPage: result.perPage,
|
|
280
|
+
};
|
|
281
|
+
});
|
|
391
282
|
}
|
|
392
283
|
// ---------------------------------------------------------------------------
|
|
393
284
|
// Routes
|
|
394
285
|
// ---------------------------------------------------------------------------
|
|
395
|
-
/**
|
|
396
|
-
* GET {dashboardPath}/api/routes — Route table (delegates to DebugStore).
|
|
397
|
-
*/
|
|
398
286
|
async routes({ response }) {
|
|
399
287
|
const routes = this.debugStore.routes.getRoutes();
|
|
400
288
|
return response.json({
|
|
@@ -405,46 +293,34 @@ export default class DashboardController {
|
|
|
405
293
|
// ---------------------------------------------------------------------------
|
|
406
294
|
// Logs
|
|
407
295
|
// ---------------------------------------------------------------------------
|
|
408
|
-
/**
|
|
409
|
-
* GET {dashboardPath}/api/logs — Paginated logs with structured search.
|
|
410
|
-
*/
|
|
411
296
|
async logs({ request, response }) {
|
|
412
|
-
const db = this.dashboardStore.getDb();
|
|
413
|
-
if (!db)
|
|
414
|
-
return response.json({ data: [], total: 0 });
|
|
415
297
|
const qs = request.qs();
|
|
416
298
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
417
299
|
const perPage = clamp(Number(qs.perPage) || 50, 1, 200);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const data = await query
|
|
443
|
-
.orderBy('created_at', 'desc')
|
|
444
|
-
.limit(perPage)
|
|
445
|
-
.offset((page - 1) * perPage);
|
|
446
|
-
return response.json({
|
|
447
|
-
data: data.map((l) => ({
|
|
300
|
+
// Build structured filters from query string
|
|
301
|
+
const structured = [];
|
|
302
|
+
if (qs.field && qs.value !== undefined) {
|
|
303
|
+
const op = qs.operator || 'equals';
|
|
304
|
+
const operatorMap = {
|
|
305
|
+
equals: 'equals',
|
|
306
|
+
contains: 'contains',
|
|
307
|
+
starts_with: 'startsWith',
|
|
308
|
+
};
|
|
309
|
+
structured.push({
|
|
310
|
+
field: qs.field,
|
|
311
|
+
operator: operatorMap[op] || 'equals',
|
|
312
|
+
value: qs.value,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
316
|
+
const result = await this.dashboardStore.getLogs(page, perPage, {
|
|
317
|
+
level: qs.level || undefined,
|
|
318
|
+
search: qs.message || undefined,
|
|
319
|
+
requestId: qs.request_id || undefined,
|
|
320
|
+
structured: structured.length > 0 ? structured : undefined,
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
data: result.data.map((l) => ({
|
|
448
324
|
id: l.id,
|
|
449
325
|
level: l.level,
|
|
450
326
|
message: l.message,
|
|
@@ -452,75 +328,43 @@ export default class DashboardController {
|
|
|
452
328
|
data: safeParseJson(l.data),
|
|
453
329
|
createdAt: l.created_at,
|
|
454
330
|
})),
|
|
455
|
-
total,
|
|
456
|
-
page,
|
|
457
|
-
perPage,
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
catch {
|
|
461
|
-
return response.json({ data: [], total: 0 });
|
|
462
|
-
}
|
|
331
|
+
total: result.total,
|
|
332
|
+
page: result.page,
|
|
333
|
+
perPage: result.perPage,
|
|
334
|
+
};
|
|
335
|
+
});
|
|
463
336
|
}
|
|
464
337
|
// ---------------------------------------------------------------------------
|
|
465
338
|
// Emails
|
|
466
339
|
// ---------------------------------------------------------------------------
|
|
467
|
-
/**
|
|
468
|
-
* GET {dashboardPath}/api/emails — Paginated email history.
|
|
469
|
-
*/
|
|
470
340
|
async emails({ request, response }) {
|
|
471
|
-
const db = this.dashboardStore.getDb();
|
|
472
|
-
if (!db)
|
|
473
|
-
return response.json({ data: [], total: 0 });
|
|
474
341
|
const qs = request.qs();
|
|
475
342
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
476
343
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const data = await query
|
|
493
|
-
.select('id', 'from_addr', 'to_addr', 'cc', 'bcc', 'subject', 'mailer', 'status', 'message_id', 'attachment_count', 'created_at')
|
|
494
|
-
.orderBy('created_at', 'desc')
|
|
495
|
-
.limit(perPage)
|
|
496
|
-
.offset((page - 1) * perPage);
|
|
497
|
-
return response.json({
|
|
498
|
-
data: data.map(formatEmail),
|
|
499
|
-
total,
|
|
500
|
-
page,
|
|
501
|
-
perPage,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
catch {
|
|
505
|
-
return response.json({ data: [], total: 0 });
|
|
506
|
-
}
|
|
344
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
345
|
+
const result = await this.dashboardStore.getEmails(page, perPage, {
|
|
346
|
+
from: qs.from || undefined,
|
|
347
|
+
to: qs.to || undefined,
|
|
348
|
+
subject: qs.subject || undefined,
|
|
349
|
+
status: qs.status || undefined,
|
|
350
|
+
mailer: qs.mailer || undefined,
|
|
351
|
+
}, true);
|
|
352
|
+
return {
|
|
353
|
+
data: result.data.map(formatEmail),
|
|
354
|
+
total: result.total,
|
|
355
|
+
page: result.page,
|
|
356
|
+
perPage: result.perPage,
|
|
357
|
+
};
|
|
358
|
+
});
|
|
507
359
|
}
|
|
508
|
-
/**
|
|
509
|
-
* GET {dashboardPath}/api/emails/:id/preview — Email HTML preview.
|
|
510
|
-
*/
|
|
511
360
|
async emailPreview({ params, response }) {
|
|
512
|
-
|
|
513
|
-
if (!db)
|
|
361
|
+
if (!this.dashboardStore.isReady()) {
|
|
514
362
|
return response.notFound({ error: 'Not found' });
|
|
363
|
+
}
|
|
515
364
|
try {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
.where('id', id)
|
|
519
|
-
.select('html', 'text_body')
|
|
520
|
-
.first();
|
|
521
|
-
if (!email)
|
|
365
|
+
const html = await this.dashboardStore.getEmailHtml(Number(params.id));
|
|
366
|
+
if (!html)
|
|
522
367
|
return response.notFound({ error: 'Email not found' });
|
|
523
|
-
const html = email.html || email.text_body || '<p>No content</p>';
|
|
524
368
|
return response.header('Content-Type', 'text/html; charset=utf-8').send(html);
|
|
525
369
|
}
|
|
526
370
|
catch {
|
|
@@ -530,27 +374,14 @@ export default class DashboardController {
|
|
|
530
374
|
// ---------------------------------------------------------------------------
|
|
531
375
|
// Traces
|
|
532
376
|
// ---------------------------------------------------------------------------
|
|
533
|
-
/**
|
|
534
|
-
* GET {dashboardPath}/api/traces — Paginated trace list (lightweight).
|
|
535
|
-
*/
|
|
536
377
|
async traces({ request, response }) {
|
|
537
|
-
const db = this.dashboardStore.getDb();
|
|
538
|
-
if (!db)
|
|
539
|
-
return response.json({ data: [], total: 0 });
|
|
540
378
|
const qs = request.qs();
|
|
541
379
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
542
380
|
const perPage = clamp(Number(qs.perPage) || 25, 1, 100);
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const data = await db('server_stats_traces')
|
|
548
|
-
.select('id', 'request_id', 'method', 'url', 'status_code', 'total_duration', 'span_count', 'warnings', 'created_at')
|
|
549
|
-
.orderBy('created_at', 'desc')
|
|
550
|
-
.limit(perPage)
|
|
551
|
-
.offset((page - 1) * perPage);
|
|
552
|
-
return response.json({
|
|
553
|
-
data: data.map((t) => ({
|
|
381
|
+
return this.withDb(response, emptyPage(page, perPage), async () => {
|
|
382
|
+
const result = await this.dashboardStore.getTraces(page, perPage);
|
|
383
|
+
return {
|
|
384
|
+
data: result.data.map((t) => ({
|
|
554
385
|
id: t.id,
|
|
555
386
|
requestId: t.request_id,
|
|
556
387
|
method: t.method,
|
|
@@ -561,25 +392,18 @@ export default class DashboardController {
|
|
|
561
392
|
warningCount: safeParseJsonArray(t.warnings).length,
|
|
562
393
|
createdAt: t.created_at,
|
|
563
394
|
})),
|
|
564
|
-
total,
|
|
565
|
-
page,
|
|
566
|
-
perPage,
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
catch {
|
|
570
|
-
return response.json({ data: [], total: 0 });
|
|
571
|
-
}
|
|
395
|
+
total: result.total,
|
|
396
|
+
page: result.page,
|
|
397
|
+
perPage: result.perPage,
|
|
398
|
+
};
|
|
399
|
+
});
|
|
572
400
|
}
|
|
573
|
-
/**
|
|
574
|
-
* GET {dashboardPath}/api/traces/:id — Single trace with full spans.
|
|
575
|
-
*/
|
|
576
401
|
async traceDetail({ params, response }) {
|
|
577
|
-
|
|
578
|
-
if (!db)
|
|
402
|
+
if (!this.dashboardStore.isReady()) {
|
|
579
403
|
return response.notFound({ error: 'Not found' });
|
|
404
|
+
}
|
|
580
405
|
try {
|
|
581
|
-
const
|
|
582
|
-
const trace = await db('server_stats_traces').where('id', id).first();
|
|
406
|
+
const trace = await this.dashboardStore.getTraceDetail(Number(params.id));
|
|
583
407
|
if (!trace)
|
|
584
408
|
return response.notFound({ error: 'Trace not found' });
|
|
585
409
|
return response.json(formatTrace(trace));
|
|
@@ -591,11 +415,8 @@ export default class DashboardController {
|
|
|
591
415
|
// ---------------------------------------------------------------------------
|
|
592
416
|
// Cache
|
|
593
417
|
// ---------------------------------------------------------------------------
|
|
594
|
-
/**
|
|
595
|
-
* GET {dashboardPath}/api/cache — Cache stats and key list.
|
|
596
|
-
*/
|
|
597
418
|
async cacheStats({ request, response }) {
|
|
598
|
-
const inspector = await this.
|
|
419
|
+
const inspector = await this.getInspector('cache');
|
|
599
420
|
if (!inspector) {
|
|
600
421
|
return response.json({ available: false, stats: null, keys: [] });
|
|
601
422
|
}
|
|
@@ -619,11 +440,8 @@ export default class DashboardController {
|
|
|
619
440
|
return response.json({ available: false, stats: null, keys: [] });
|
|
620
441
|
}
|
|
621
442
|
}
|
|
622
|
-
/**
|
|
623
|
-
* GET {dashboardPath}/api/cache/:key — Single cache key detail.
|
|
624
|
-
*/
|
|
625
443
|
async cacheKey({ params, response }) {
|
|
626
|
-
const inspector = await this.
|
|
444
|
+
const inspector = await this.getInspector('cache');
|
|
627
445
|
if (!inspector) {
|
|
628
446
|
return response.notFound({ error: 'Cache not available' });
|
|
629
447
|
}
|
|
@@ -641,11 +459,8 @@ export default class DashboardController {
|
|
|
641
459
|
// ---------------------------------------------------------------------------
|
|
642
460
|
// Jobs / Queue
|
|
643
461
|
// ---------------------------------------------------------------------------
|
|
644
|
-
/**
|
|
645
|
-
* GET {dashboardPath}/api/jobs — Job list with status filter.
|
|
646
|
-
*/
|
|
647
462
|
async jobs({ request, response }) {
|
|
648
|
-
const inspector = await this.
|
|
463
|
+
const inspector = await this.getInspector('queue');
|
|
649
464
|
if (!inspector) {
|
|
650
465
|
return response.json({ available: false, overview: null, jobs: [], total: 0 });
|
|
651
466
|
}
|
|
@@ -671,11 +486,8 @@ export default class DashboardController {
|
|
|
671
486
|
return response.json({ available: false, overview: null, jobs: [], total: 0 });
|
|
672
487
|
}
|
|
673
488
|
}
|
|
674
|
-
/**
|
|
675
|
-
* GET {dashboardPath}/api/jobs/:id — Single job detail.
|
|
676
|
-
*/
|
|
677
489
|
async jobDetail({ params, response }) {
|
|
678
|
-
const inspector = await this.
|
|
490
|
+
const inspector = await this.getInspector('queue');
|
|
679
491
|
if (!inspector) {
|
|
680
492
|
return response.notFound({ error: 'Queue not available' });
|
|
681
493
|
}
|
|
@@ -689,11 +501,8 @@ export default class DashboardController {
|
|
|
689
501
|
return response.notFound({ error: 'Job not found' });
|
|
690
502
|
}
|
|
691
503
|
}
|
|
692
|
-
/**
|
|
693
|
-
* POST {dashboardPath}/api/jobs/:id/retry — Retry a failed job.
|
|
694
|
-
*/
|
|
695
504
|
async jobRetry({ params, response }) {
|
|
696
|
-
const inspector = await this.
|
|
505
|
+
const inspector = await this.getInspector('queue');
|
|
697
506
|
if (!inspector) {
|
|
698
507
|
return response.notFound({ error: 'Queue not available' });
|
|
699
508
|
}
|
|
@@ -711,9 +520,6 @@ export default class DashboardController {
|
|
|
711
520
|
// ---------------------------------------------------------------------------
|
|
712
521
|
// Config
|
|
713
522
|
// ---------------------------------------------------------------------------
|
|
714
|
-
/**
|
|
715
|
-
* GET {dashboardPath}/api/config — Sanitized app config and env vars.
|
|
716
|
-
*/
|
|
717
523
|
async config({ response }) {
|
|
718
524
|
const configData = this.configInspector.getConfig();
|
|
719
525
|
const envData = this.configInspector.getEnvVars();
|
|
@@ -725,17 +531,10 @@ export default class DashboardController {
|
|
|
725
531
|
// ---------------------------------------------------------------------------
|
|
726
532
|
// Saved Filters
|
|
727
533
|
// ---------------------------------------------------------------------------
|
|
728
|
-
/**
|
|
729
|
-
* GET {dashboardPath}/api/filters — List saved filter presets.
|
|
730
|
-
*/
|
|
731
534
|
async savedFilters({ response }) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
return
|
|
735
|
-
try {
|
|
736
|
-
const filters = await db('server_stats_saved_filters')
|
|
737
|
-
.orderBy('created_at', 'desc');
|
|
738
|
-
return response.json({
|
|
535
|
+
return this.withDb(response, { filters: [] }, async () => {
|
|
536
|
+
const filters = await this.dashboardStore.getSavedFilters();
|
|
537
|
+
return {
|
|
739
538
|
filters: filters.map((f) => ({
|
|
740
539
|
id: f.id,
|
|
741
540
|
name: f.name,
|
|
@@ -743,53 +542,38 @@ export default class DashboardController {
|
|
|
743
542
|
filterConfig: safeParseJson(f.filter_config),
|
|
744
543
|
createdAt: f.created_at,
|
|
745
544
|
})),
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
catch {
|
|
749
|
-
return response.json({ filters: [] });
|
|
750
|
-
}
|
|
545
|
+
};
|
|
546
|
+
});
|
|
751
547
|
}
|
|
752
|
-
/**
|
|
753
|
-
* POST {dashboardPath}/api/filters — Create a saved filter preset.
|
|
754
|
-
*/
|
|
755
548
|
async createSavedFilter({ request, response }) {
|
|
756
|
-
|
|
757
|
-
if (!db)
|
|
549
|
+
if (!this.dashboardStore.isReady()) {
|
|
758
550
|
return response.serviceUnavailable({ error: 'Database not available' });
|
|
551
|
+
}
|
|
759
552
|
try {
|
|
760
553
|
const body = request.body();
|
|
761
|
-
const name = body
|
|
762
|
-
const section = body.section;
|
|
763
|
-
const filterConfig = body.filterConfig;
|
|
554
|
+
const { name, section, filterConfig } = body;
|
|
764
555
|
if (!name || !section || !filterConfig) {
|
|
765
|
-
return response.badRequest({
|
|
556
|
+
return response.badRequest({
|
|
557
|
+
error: 'Missing required fields: name, section, filterConfig',
|
|
558
|
+
});
|
|
766
559
|
}
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
return response.json(
|
|
773
|
-
id,
|
|
774
|
-
name,
|
|
775
|
-
section,
|
|
776
|
-
filterConfig: typeof filterConfig === 'string' ? safeParseJson(filterConfig) : filterConfig,
|
|
777
|
-
});
|
|
560
|
+
const configObj = typeof filterConfig === 'string' ? safeParseJson(filterConfig) : filterConfig;
|
|
561
|
+
const result = await this.dashboardStore.createSavedFilter(name, section, configObj);
|
|
562
|
+
if (!result) {
|
|
563
|
+
return response.serviceUnavailable({ error: 'Database not available' });
|
|
564
|
+
}
|
|
565
|
+
return response.json(result);
|
|
778
566
|
}
|
|
779
567
|
catch {
|
|
780
568
|
return response.internalServerError({ error: 'Failed to create filter' });
|
|
781
569
|
}
|
|
782
570
|
}
|
|
783
|
-
/**
|
|
784
|
-
* DELETE {dashboardPath}/api/filters/:id — Delete a saved filter preset.
|
|
785
|
-
*/
|
|
786
571
|
async deleteSavedFilter({ params, response }) {
|
|
787
|
-
|
|
788
|
-
if (!db)
|
|
572
|
+
if (!this.dashboardStore.isReady()) {
|
|
789
573
|
return response.serviceUnavailable({ error: 'Database not available' });
|
|
574
|
+
}
|
|
790
575
|
try {
|
|
791
|
-
const
|
|
792
|
-
const deleted = await db('server_stats_saved_filters').where('id', id).del();
|
|
576
|
+
const deleted = await this.dashboardStore.deleteSavedFilter(Number(params.id));
|
|
793
577
|
if (!deleted)
|
|
794
578
|
return response.notFound({ error: 'Filter not found' });
|
|
795
579
|
return response.json({ success: true });
|
|
@@ -802,8 +586,19 @@ export default class DashboardController {
|
|
|
802
586
|
// Private helpers
|
|
803
587
|
// ---------------------------------------------------------------------------
|
|
804
588
|
/**
|
|
805
|
-
*
|
|
589
|
+
* Wraps a store call with null-guard + try/catch boilerplate.
|
|
590
|
+
* Returns emptyValue if the store is not ready or the fn throws.
|
|
806
591
|
*/
|
|
592
|
+
async withDb(response, emptyValue, fn) {
|
|
593
|
+
if (!this.dashboardStore.isReady())
|
|
594
|
+
return response.json(emptyValue);
|
|
595
|
+
try {
|
|
596
|
+
return response.json(await fn());
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
return response.json(emptyValue);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
807
602
|
checkAccess(ctx) {
|
|
808
603
|
const config = this.app.config.get('server_stats');
|
|
809
604
|
if (!config?.shouldShow)
|
|
@@ -815,81 +610,90 @@ export default class DashboardController {
|
|
|
815
610
|
return false;
|
|
816
611
|
}
|
|
817
612
|
}
|
|
818
|
-
/**
|
|
819
|
-
* Get the configured dashboard path.
|
|
820
|
-
*/
|
|
821
613
|
getDashboardPath() {
|
|
822
614
|
const config = this.app.config.get('server_stats');
|
|
823
615
|
return config?.devToolbar?.dashboardPath ?? '/__stats';
|
|
824
616
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
617
|
+
async getInspector(type) {
|
|
618
|
+
if (type === 'cache') {
|
|
619
|
+
if (this.cacheAvailable === false)
|
|
620
|
+
return null;
|
|
621
|
+
if (this.cacheInspector)
|
|
622
|
+
return this.cacheInspector;
|
|
623
|
+
try {
|
|
624
|
+
const available = await CacheInspector.isAvailable(this.app);
|
|
625
|
+
this.cacheAvailable = available;
|
|
626
|
+
if (!available)
|
|
627
|
+
return null;
|
|
628
|
+
const redis = await this.app.container.make('redis');
|
|
629
|
+
this.cacheInspector = new CacheInspector(redis);
|
|
630
|
+
return this.cacheInspector;
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
this.cacheAvailable = false;
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
if (this.queueAvailable === false)
|
|
639
|
+
return null;
|
|
640
|
+
if (this.queueInspector)
|
|
641
|
+
return this.queueInspector;
|
|
642
|
+
try {
|
|
643
|
+
const available = await QueueInspector.isAvailable(this.app);
|
|
644
|
+
this.queueAvailable = available;
|
|
645
|
+
if (!available)
|
|
646
|
+
return null;
|
|
647
|
+
const queue = await this.app.container.make('queue');
|
|
648
|
+
this.queueInspector = new QueueInspector(queue);
|
|
649
|
+
return this.queueInspector;
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
this.queueAvailable = false;
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
844
655
|
}
|
|
845
656
|
}
|
|
846
|
-
/**
|
|
847
|
-
|
|
848
|
-
*/
|
|
849
|
-
async getCacheInspector() {
|
|
850
|
-
if (this.cacheAvailable === false)
|
|
851
|
-
return null;
|
|
852
|
-
if (this.cacheInspector)
|
|
853
|
-
return this.cacheInspector;
|
|
657
|
+
/** Fetch cache overview stats for the overview page. */
|
|
658
|
+
async fetchCacheOverview() {
|
|
854
659
|
try {
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
if (!available)
|
|
660
|
+
const inspector = await this.getInspector('cache');
|
|
661
|
+
if (!inspector)
|
|
858
662
|
return null;
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
663
|
+
const stats = await inspector.getStats();
|
|
664
|
+
return {
|
|
665
|
+
available: true,
|
|
666
|
+
totalKeys: stats.totalKeys,
|
|
667
|
+
hitRate: stats.hitRate,
|
|
668
|
+
memoryUsedHuman: stats.memoryUsedHuman,
|
|
669
|
+
};
|
|
862
670
|
}
|
|
863
671
|
catch {
|
|
864
|
-
this.cacheAvailable = false;
|
|
865
672
|
return null;
|
|
866
673
|
}
|
|
867
674
|
}
|
|
868
|
-
/**
|
|
869
|
-
|
|
870
|
-
*/
|
|
871
|
-
async getQueueInspector() {
|
|
872
|
-
if (this.queueAvailable === false)
|
|
873
|
-
return null;
|
|
874
|
-
if (this.queueInspector)
|
|
875
|
-
return this.queueInspector;
|
|
675
|
+
/** Fetch queue overview stats for the overview page. */
|
|
676
|
+
async fetchQueueOverview() {
|
|
876
677
|
try {
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
if (!available)
|
|
678
|
+
const inspector = await this.getInspector('queue');
|
|
679
|
+
if (!inspector)
|
|
880
680
|
return null;
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
681
|
+
const overview = await inspector.getOverview();
|
|
682
|
+
return {
|
|
683
|
+
available: true,
|
|
684
|
+
active: overview.active,
|
|
685
|
+
waiting: overview.waiting,
|
|
686
|
+
failed: overview.failed,
|
|
687
|
+
completed: overview.completed,
|
|
688
|
+
};
|
|
884
689
|
}
|
|
885
690
|
catch {
|
|
886
|
-
this.queueAvailable = false;
|
|
887
691
|
return null;
|
|
888
692
|
}
|
|
889
693
|
}
|
|
890
694
|
}
|
|
891
695
|
// ---------------------------------------------------------------------------
|
|
892
|
-
// Formatting helpers
|
|
696
|
+
// Formatting helpers (snake_case DB rows → camelCase API response)
|
|
893
697
|
// ---------------------------------------------------------------------------
|
|
894
698
|
function formatRequest(r) {
|
|
895
699
|
return {
|
|
@@ -962,47 +766,12 @@ function emptyOverview() {
|
|
|
962
766
|
slowestEndpoints: [],
|
|
963
767
|
queryStats: { total: 0, avgDuration: 0, perRequest: 0 },
|
|
964
768
|
recentErrors: [],
|
|
769
|
+
topEvents: [],
|
|
770
|
+
emailActivity: { sent: 0, queued: 0, failed: 0 },
|
|
771
|
+
logLevelBreakdown: { error: 0, warn: 0, info: 0, debug: 0 },
|
|
772
|
+
statusDistribution: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
|
|
773
|
+
slowestQueries: [],
|
|
774
|
+
cacheStats: null,
|
|
775
|
+
jobQueueStatus: null,
|
|
965
776
|
};
|
|
966
777
|
}
|
|
967
|
-
// ---------------------------------------------------------------------------
|
|
968
|
-
// Utility functions
|
|
969
|
-
// ---------------------------------------------------------------------------
|
|
970
|
-
function round(n) {
|
|
971
|
-
return Math.round(n * 100) / 100;
|
|
972
|
-
}
|
|
973
|
-
function clamp(value, min, max) {
|
|
974
|
-
return Math.max(min, Math.min(max, value));
|
|
975
|
-
}
|
|
976
|
-
function minutesAgo(minutes) {
|
|
977
|
-
const d = new Date(Date.now() - minutes * 60_000);
|
|
978
|
-
return d.toISOString().replace('T', ' ').slice(0, 19);
|
|
979
|
-
}
|
|
980
|
-
function parseRange(range) {
|
|
981
|
-
const rangeMap = {
|
|
982
|
-
'5m': 5,
|
|
983
|
-
'15m': 15,
|
|
984
|
-
'30m': 30,
|
|
985
|
-
'1h': 60,
|
|
986
|
-
'6h': 360,
|
|
987
|
-
'24h': 1440,
|
|
988
|
-
'7d': 10080,
|
|
989
|
-
};
|
|
990
|
-
const minutes = rangeMap[range] ?? 60;
|
|
991
|
-
return { cutoff: minutesAgo(minutes) };
|
|
992
|
-
}
|
|
993
|
-
function safeParseJson(value) {
|
|
994
|
-
if (value === null || value === undefined)
|
|
995
|
-
return null;
|
|
996
|
-
if (typeof value !== 'string')
|
|
997
|
-
return value;
|
|
998
|
-
try {
|
|
999
|
-
return JSON.parse(value);
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
return value;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
function safeParseJsonArray(value) {
|
|
1006
|
-
const parsed = safeParseJson(value);
|
|
1007
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
1008
|
-
}
|