adonisjs-server-stats 1.9.0 → 1.10.3
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 +23 -14
- package/dist/core/config-utils.d.ts +8 -0
- package/dist/core/constants.d.ts +4 -0
- package/dist/core/dashboard-data-controller.d.ts +16 -0
- package/dist/core/dashboard-data-helpers.d.ts +12 -0
- package/dist/core/debug-data-controller.d.ts +4 -0
- package/dist/core/define-config-helpers.d.ts +25 -0
- package/dist/core/feature-detect-helpers.d.ts +36 -0
- package/dist/core/formatters-helpers.d.ts +23 -0
- package/dist/core/index.js +596 -509
- package/dist/core/log-utils-helpers.d.ts +13 -0
- package/dist/core/metrics.d.ts +3 -28
- package/dist/core/pagination.d.ts +0 -9
- package/dist/core/server-stats-controller.d.ts +6 -0
- package/dist/core/split-pane.d.ts +18 -0
- package/dist/core/trace-utils.d.ts +5 -0
- package/dist/core/transmit-helpers.d.ts +7 -0
- package/dist/core/types-dashboard.d.ts +178 -0
- package/dist/core/types-diagnostics.d.ts +85 -0
- package/dist/core/types.d.ts +11 -443
- package/dist/react/{CacheSection-xH75hwXu.js → CacheSection-baMZotSn.js} +2 -2
- package/dist/react/CacheTab-2cw_rMzj.js +117 -0
- package/dist/react/{ConfigSection-D8BO1Ry9.js → ConfigSection-DGgqjAal.js} +1 -1
- package/dist/react/{ConfigTab-CcN-tfjv.js → ConfigTab-H3OnYqmK.js} +1 -1
- package/dist/react/CustomPaneTab-B6r7ha0u.js +98 -0
- package/dist/react/{EmailsSection-BzlsTdPs.js → EmailsSection-C-UZISG-.js} +2 -2
- package/dist/react/EmailsTab-DbK4Eobn.js +139 -0
- package/dist/react/{EventsSection-CGQWiIdV.js → EventsSection-C7RQW_LY.js} +2 -2
- package/dist/react/EventsTab-CfVr7AiM.js +57 -0
- package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
- package/dist/react/{JobsSection-D7AHQmZi.js → JobsSection-CQHNK_Ls.js} +2 -2
- package/dist/react/{JobsTab-B3Lfdqed.js → JobsTab-znzf6jzk.js} +54 -42
- package/dist/react/{LogsSection-Cly1dpvS.js → LogsSection-Dmm3rE2B.js} +9 -3
- package/dist/react/LogsTab-D8unMV5P.js +108 -0
- package/dist/react/{OverviewSection-CkBGFEWq.js → OverviewSection-ABP9ueBo.js} +1 -1
- package/dist/react/{QueriesSection-CfCpnNUD.js → QueriesSection-CnmSkznA.js} +2 -2
- package/dist/react/{QueriesTab-DbBmAqzO.js → QueriesTab-BQzcxEiW.js} +37 -40
- package/dist/react/RelatedLogs-3A8RuGKH.js +52 -0
- package/dist/react/RequestsSection-kW79_M7k.js +341 -0
- package/dist/react/{RoutesSection-CRqF-cNM.js → RoutesSection-BRhxrtjZ.js} +2 -2
- package/dist/react/RoutesTab-CpYH5lUw.js +68 -0
- package/dist/react/TimelineTab-DjLR35Ce.js +214 -0
- package/dist/react/index-CsImORX6.js +1121 -0
- package/dist/react/index.js +1 -1
- package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
- package/dist/react/react/components/shared/RelatedLogs.d.ts +7 -0
- package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
- package/dist/react/style.css +1 -1
- package/dist/src/collectors/app_collector.d.ts +0 -8
- package/dist/src/collectors/app_collector.js +45 -52
- package/dist/src/collectors/auto_detect.d.ts +0 -23
- package/dist/src/collectors/auto_detect.js +33 -55
- package/dist/src/collectors/db_pool_collector.d.ts +14 -16
- package/dist/src/collectors/db_pool_collector.js +72 -57
- package/dist/src/collectors/log_collector.d.ts +0 -47
- package/dist/src/collectors/log_collector.js +36 -65
- package/dist/src/collectors/queue_collector.d.ts +0 -20
- package/dist/src/collectors/queue_collector.js +60 -76
- package/dist/src/collectors/redis_collector.d.ts +10 -10
- package/dist/src/collectors/redis_collector.js +69 -66
- package/dist/src/config/deprecation_migration.d.ts +7 -0
- package/dist/src/config/deprecation_migration.js +201 -0
- package/dist/src/controller/debug_controller.d.ts +1 -1
- package/dist/src/controller/debug_controller.js +87 -81
- package/dist/src/dashboard/cache_handlers.d.ts +14 -0
- package/dist/src/dashboard/cache_handlers.js +52 -0
- package/dist/src/dashboard/chart_aggregator.d.ts +0 -7
- package/dist/src/dashboard/chart_aggregator.js +68 -50
- package/dist/src/dashboard/coalesce_cache.d.ts +25 -0
- package/dist/src/dashboard/coalesce_cache.js +47 -0
- package/dist/src/dashboard/dashboard_controller.d.ts +11 -37
- package/dist/src/dashboard/dashboard_controller.js +52 -532
- package/dist/src/dashboard/dashboard_page_assets.d.ts +17 -0
- package/dist/src/dashboard/dashboard_page_assets.js +51 -0
- package/dist/src/dashboard/dashboard_store.d.ts +19 -217
- package/dist/src/dashboard/dashboard_store.js +115 -1069
- package/dist/src/dashboard/dashboard_types.d.ts +83 -0
- package/dist/src/dashboard/dashboard_types.js +4 -0
- package/dist/src/dashboard/detail_queries.d.ts +19 -0
- package/dist/src/dashboard/detail_queries.js +98 -0
- package/dist/src/dashboard/email_event_builder.d.ts +8 -0
- package/dist/src/dashboard/email_event_builder.js +65 -0
- package/dist/src/dashboard/explain_query.d.ts +8 -0
- package/dist/src/dashboard/explain_query.js +22 -0
- package/dist/src/dashboard/filter_handlers.d.ts +23 -0
- package/dist/src/dashboard/filter_handlers.js +56 -0
- package/dist/src/dashboard/filtered_queries.d.ts +15 -0
- package/dist/src/dashboard/filtered_queries.js +155 -0
- package/dist/src/dashboard/flush_manager.d.ts +25 -0
- package/dist/src/dashboard/flush_manager.js +107 -0
- package/dist/src/dashboard/format_helpers.d.ts +126 -0
- package/dist/src/dashboard/format_helpers.js +140 -0
- package/dist/src/dashboard/inspector_manager.d.ts +36 -0
- package/dist/src/dashboard/inspector_manager.js +102 -0
- package/dist/src/dashboard/integrations/config_inspector.js +11 -13
- package/dist/src/dashboard/integrations/queue_inspector.d.ts +3 -3
- package/dist/src/dashboard/integrations/queue_inspector.js +13 -10
- package/dist/src/dashboard/jobs_handlers.d.ts +14 -0
- package/dist/src/dashboard/jobs_handlers.js +61 -0
- package/dist/src/dashboard/knex_factory.d.ts +18 -0
- package/dist/src/dashboard/knex_factory.js +91 -0
- package/dist/src/dashboard/migrator.js +30 -153
- package/dist/src/dashboard/migrator_tables.d.ts +19 -0
- package/dist/src/dashboard/migrator_tables.js +153 -0
- package/dist/src/dashboard/overview_queries.d.ts +66 -0
- package/dist/src/dashboard/overview_queries.js +155 -0
- package/dist/src/dashboard/overview_query_runners.d.ts +25 -0
- package/dist/src/dashboard/overview_query_runners.js +84 -0
- package/dist/src/dashboard/overview_store_queries.d.ts +40 -0
- package/dist/src/dashboard/overview_store_queries.js +69 -0
- package/dist/src/dashboard/paginate_helper.d.ts +12 -0
- package/dist/src/dashboard/paginate_helper.js +33 -0
- package/dist/src/dashboard/query_explain_handler.d.ts +10 -0
- package/dist/src/dashboard/query_explain_handler.js +80 -0
- package/dist/src/dashboard/read_queries.d.ts +32 -0
- package/dist/src/dashboard/read_queries.js +107 -0
- package/dist/src/dashboard/saved_filter_queries.d.ts +10 -0
- package/dist/src/dashboard/saved_filter_queries.js +24 -0
- package/dist/src/dashboard/storage_stats.d.ts +41 -0
- package/dist/src/dashboard/storage_stats.js +81 -0
- package/dist/src/dashboard/write_queue.d.ts +106 -0
- package/dist/src/dashboard/write_queue.js +225 -0
- package/dist/src/data/data_access.d.ts +6 -36
- package/dist/src/data/data_access.js +43 -188
- package/dist/src/data/data_access_helpers.d.ts +130 -0
- package/dist/src/data/data_access_helpers.js +212 -0
- package/dist/src/debug/debug_store.js +37 -32
- package/dist/src/debug/email_collector.d.ts +1 -10
- package/dist/src/debug/email_collector.js +78 -81
- package/dist/src/debug/event_collector.d.ts +0 -9
- package/dist/src/debug/event_collector.js +79 -62
- package/dist/src/debug/query_collector.js +23 -19
- package/dist/src/debug/route_inspector.d.ts +1 -5
- package/dist/src/debug/route_inspector.js +50 -51
- package/dist/src/debug/trace_collector.d.ts +10 -2
- package/dist/src/debug/trace_collector.js +23 -16
- package/dist/src/debug/types.d.ts +5 -1
- package/dist/src/define_config.d.ts +0 -65
- package/dist/src/define_config.js +93 -333
- package/dist/src/edge/client/dashboard.js +2 -2
- package/dist/src/edge/client/debug-panel-deferred.js +1 -1
- package/dist/src/edge/client/stats-bar.js +1 -1
- package/dist/src/edge/client-vue/dashboard.js +5 -5
- package/dist/src/edge/client-vue/debug-panel-deferred.js +3 -3
- package/dist/src/edge/client-vue/stats-bar.js +3 -3
- package/dist/src/edge/plugin.d.ts +0 -16
- package/dist/src/edge/plugin.js +57 -64
- package/dist/src/engine/request_metrics.d.ts +1 -0
- package/dist/src/engine/request_metrics.js +32 -42
- package/dist/src/middleware/request_tracking_middleware.d.ts +3 -8
- package/dist/src/middleware/request_tracking_middleware.js +65 -91
- package/dist/src/provider/auth_middleware_detector.d.ts +16 -0
- package/dist/src/provider/auth_middleware_detector.js +97 -0
- package/dist/src/provider/boot_helpers.d.ts +20 -0
- package/dist/src/provider/boot_helpers.js +91 -0
- package/dist/src/provider/boot_initializer.d.ts +28 -0
- package/dist/src/provider/boot_initializer.js +35 -0
- package/dist/src/provider/dashboard_init.d.ts +30 -0
- package/dist/src/provider/dashboard_init.js +138 -0
- package/dist/src/provider/dashboard_setup.d.ts +25 -0
- package/dist/src/provider/dashboard_setup.js +78 -0
- package/dist/src/provider/diagnostics.d.ts +134 -0
- package/dist/src/provider/diagnostics.js +127 -0
- package/dist/src/provider/email_bridge.d.ts +43 -0
- package/dist/src/provider/email_bridge.js +80 -0
- package/dist/src/provider/email_helpers.d.ts +13 -0
- package/dist/src/provider/email_helpers.js +68 -0
- package/dist/src/provider/pino_hook.d.ts +17 -0
- package/dist/src/provider/pino_hook.js +35 -0
- package/dist/src/provider/provider_helpers_extra.d.ts +47 -0
- package/dist/src/provider/provider_helpers_extra.js +177 -0
- package/dist/src/provider/server_stats_provider.d.ts +39 -85
- package/dist/src/provider/server_stats_provider.js +131 -936
- package/dist/src/provider/shutdown_helpers.d.ts +43 -0
- package/dist/src/provider/shutdown_helpers.js +70 -0
- package/dist/src/provider/toolbar_setup.d.ts +57 -0
- package/dist/src/provider/toolbar_setup.js +141 -0
- package/dist/src/routes/dashboard_routes.d.ts +14 -0
- package/dist/src/routes/dashboard_routes.js +197 -0
- package/dist/src/routes/debug_routes.d.ts +14 -0
- package/dist/src/routes/debug_routes.js +101 -0
- package/dist/src/routes/register_routes.d.ts +0 -78
- package/dist/src/routes/register_routes.js +22 -347
- package/dist/src/routes/stats_routes.d.ts +5 -0
- package/dist/src/routes/stats_routes.js +14 -0
- package/dist/src/styles/components.css +177 -0
- package/dist/src/styles/dashboard.css +8 -90
- package/dist/src/styles/debug-panel.css +10 -31
- package/dist/src/types.d.ts +306 -15
- package/dist/vue/{CacheSection-Cx-hj09X.js → CacheSection-ITqvpfH5.js} +1 -1
- package/dist/vue/{ConfigSection-CMXyryf6.js → ConfigSection-DTn3GslE.js} +1 -1
- package/dist/vue/{EmailsSection-DgKl9xGT.js → EmailsSection-DtLJ4XoS.js} +1 -1
- package/dist/vue/{EventsSection-BNMCAim1.js → EventsSection-BOYYz0Ty.js} +1 -1
- package/dist/vue/{JobsSection-CCMgMlxd.js → JobsSection-BazTxcJL.js} +1 -1
- package/dist/vue/{LogsSection-CvOnTxUu.js → LogsSection-D55PjTKX.js} +9 -3
- package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
- package/dist/vue/{OverviewSection-CHgaKtUR.js → OverviewSection-1uBKo-Tu.js} +1 -1
- package/dist/vue/{QueriesSection-BnHRD98z.js → QueriesSection-rpoZ4ogd.js} +1 -1
- package/dist/vue/RelatedLogs.vue_vue_type_script_setup_true_lang-CB2_TzYW.js +84 -0
- package/dist/vue/RequestsSection-x7LvT0MC.js +401 -0
- package/dist/vue/{RoutesSection-BrceOcKQ.js → RoutesSection-CCD0zZqQ.js} +1 -1
- package/dist/vue/TimelineTab-zj5Z5OdT.js +338 -0
- package/dist/vue/components/Dashboard/sections/RequestsSection.vue.d.ts +4 -0
- package/dist/vue/components/DebugPanel/tabs/TimelineTab.vue.d.ts +4 -0
- package/dist/vue/components/{Dashboard/sections/TimelineSection.vue.d.ts → shared/RelatedLogs.vue.d.ts} +5 -6
- package/dist/vue/composables/useDashboardData.d.ts +12 -23
- package/dist/vue/index-C8MxnS7Q.js +1232 -0
- package/dist/vue/index.js +1 -1
- package/dist/vue/style.css +1 -1
- package/package.json +1 -1
- package/dist/react/CacheTab-DYmsZJJ1.js +0 -123
- package/dist/react/CustomPaneTab-D7_o3Ec6.js +0 -104
- package/dist/react/EmailsTab-Uh2CQY3o.js +0 -153
- package/dist/react/EventsTab-CC6DQzEm.js +0 -63
- package/dist/react/LogsTab-BbYK-iyh.js +0 -103
- package/dist/react/RequestsSection-Cb5a6MlT.js +0 -209
- package/dist/react/RoutesTab-Bwreij3e.js +0 -74
- package/dist/react/TimelineSection-B2y06kRE.js +0 -158
- package/dist/react/TimelineTab-6hthfdBB.js +0 -193
- package/dist/react/WaterfallChart-Cj73WdfM.js +0 -100
- package/dist/react/index-CecA4IdQ.js +0 -1075
- package/dist/react/react/components/Dashboard/sections/TimelineSection.d.ts +0 -8
- package/dist/vue/RequestsSection-B-uSlM0f.js +0 -243
- package/dist/vue/TimelineSection-CfvnA2Oo.js +0 -186
- package/dist/vue/TimelineTab-Db6lKKsD.js +0 -250
- package/dist/vue/WaterfallChart.vue_vue_type_script_setup_true_lang-tZ13cNj1.js +0 -118
- package/dist/vue/index-oLxS08vN.js +0 -1235
|
@@ -1,126 +1,62 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { safeParseJson, safeParseJsonArray } from '../utils/json_helpers.js';
|
|
5
1
|
import { log } from '../utils/logger.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { clamp } from '../utils/math_helpers.js';
|
|
3
|
+
import { handleCacheStats, handleCacheKey, handleCacheKeyDelete } from './cache_handlers.js';
|
|
4
|
+
import { DashboardPageAssets } from './dashboard_page_assets.js';
|
|
5
|
+
import { handleSavedFilters, handleCreateSavedFilter, handleDeleteSavedFilter, } from './filter_handlers.js';
|
|
6
|
+
import { paginatedResponse, emptyPage, emptyOverview, formatRequest, formatQuery, formatTrace, formatLog, mapChartBucket, buildSparklines, formatGroupedQuery, } from './format_helpers.js';
|
|
7
|
+
import { InspectorManager } from './inspector_manager.js';
|
|
9
8
|
import { ConfigInspector } from './integrations/config_inspector.js';
|
|
10
|
-
import {
|
|
9
|
+
import { handleJobs, handleJobDetail, handleJobRetry } from './jobs_handlers.js';
|
|
10
|
+
import { handleQueryExplain } from './query_explain_handler.js';
|
|
11
11
|
const warnedDbReads = new Set();
|
|
12
|
-
const SRC_DIR = dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const EDGE_DIR = join(SRC_DIR, '..', 'edge');
|
|
14
|
-
const STYLES_DIR = join(SRC_DIR, '..', 'styles');
|
|
15
|
-
function paginatedResponse(data, total, page, perPage) {
|
|
16
|
-
return {
|
|
17
|
-
data,
|
|
18
|
-
meta: { total, page, perPage, lastPage: Math.max(1, Math.ceil(total / perPage)) },
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
function emptyPage(page, perPage) {
|
|
22
|
-
return { data: [], meta: { total: 0, page, perPage, lastPage: 1 } };
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Controller for the full-page dashboard.
|
|
26
|
-
*
|
|
27
|
-
* Serves the dashboard HTML page and dashboard-specific JSON API
|
|
28
|
-
* endpoints (overview, requests, grouped queries, query explain,
|
|
29
|
-
* cache, jobs, config, saved filters).
|
|
30
|
-
*
|
|
31
|
-
* Data resource endpoints (queries, events, emails, traces, routes,
|
|
32
|
-
* logs) are handled by the unified ApiController — see
|
|
33
|
-
* `src/controller/api_controller.ts`.
|
|
34
|
-
*/
|
|
35
12
|
export default class DashboardController {
|
|
36
13
|
dashboardStore;
|
|
37
14
|
app;
|
|
38
|
-
cacheInspector = null;
|
|
39
|
-
queueInspector = null;
|
|
40
15
|
configInspector;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
cachedCss = null;
|
|
44
|
-
cachedJs = null;
|
|
45
|
-
cachedTransmitClient = null;
|
|
16
|
+
inspectors;
|
|
17
|
+
pageAssets;
|
|
46
18
|
constructor(dashboardStore, app) {
|
|
47
19
|
this.dashboardStore = dashboardStore;
|
|
48
20
|
this.app = app;
|
|
49
21
|
this.configInspector = new ConfigInspector(app);
|
|
22
|
+
this.inspectors = new InspectorManager(app);
|
|
23
|
+
this.pageAssets = new DashboardPageAssets();
|
|
50
24
|
}
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Page
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
25
|
async page(ctx) {
|
|
55
|
-
if (!this.checkAccess(ctx))
|
|
26
|
+
if (!this.checkAccess(ctx))
|
|
56
27
|
return ctx.response.forbidden({ error: 'Access denied' });
|
|
57
|
-
}
|
|
58
28
|
const config = this.app.config.get('server_stats');
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const components = readFileSync(join(STYLES_DIR, 'components.css'), 'utf-8');
|
|
63
|
-
const utilities = readFileSync(join(STYLES_DIR, 'utilities.css'), 'utf-8');
|
|
64
|
-
const dashboard = readFileSync(join(STYLES_DIR, 'dashboard.css'), 'utf-8');
|
|
65
|
-
this.cachedCss = tokens + '\n' + components + '\n' + utilities + '\n' + dashboard;
|
|
66
|
-
}
|
|
67
|
-
if (!this.cachedJs) {
|
|
68
|
-
const renderer = toolbarConfig.renderer || 'preact';
|
|
69
|
-
const clientDir = renderer === 'vue' ? 'client-vue' : 'client';
|
|
70
|
-
this.cachedJs = readFileSync(join(EDGE_DIR, clientDir, 'dashboard.js'), 'utf-8');
|
|
71
|
-
}
|
|
72
|
-
if (this.cachedTransmitClient === null) {
|
|
73
|
-
this.cachedTransmitClient = loadTransmitClient(this.app.makePath('package.json'));
|
|
74
|
-
if (this.cachedTransmitClient) {
|
|
75
|
-
log.info('Transmit client loaded for dashboard');
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
log.info('Dashboard will use polling. Install @adonisjs/transmit-client for real-time updates.');
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const dashPath = this.getDashboardPath();
|
|
29
|
+
const tc = config?.devToolbar ?? {};
|
|
30
|
+
const renderer = tc.renderer || 'preact';
|
|
31
|
+
const dp = this.getDashboardPath();
|
|
82
32
|
return ctx.view.render('ss::dashboard', {
|
|
83
|
-
css: this.
|
|
84
|
-
js: this.
|
|
85
|
-
transmitClient: this.
|
|
33
|
+
css: this.pageAssets.getCss(),
|
|
34
|
+
js: this.pageAssets.getJs(renderer),
|
|
35
|
+
transmitClient: this.pageAssets.getTransmitClient(this.app.makePath('package.json')),
|
|
86
36
|
dashConfig: {
|
|
87
37
|
baseUrl: '',
|
|
88
|
-
dashboardEndpoint:
|
|
89
|
-
debugEndpoint:
|
|
38
|
+
dashboardEndpoint: dp + '/api',
|
|
39
|
+
debugEndpoint: tc.debugEndpoint || '/admin/api/debug',
|
|
90
40
|
channelName: 'server-stats/dashboard',
|
|
91
41
|
backUrl: '/',
|
|
92
42
|
},
|
|
93
43
|
});
|
|
94
44
|
}
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Overview
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
45
|
async overview({ request, response }) {
|
|
99
46
|
return this.withDb(response, 'overview', emptyOverview(), async () => {
|
|
100
47
|
const range = request.qs().range || '1h';
|
|
101
|
-
// Sequential awaits — with a single-connection SQLite pool, Promise.all
|
|
102
|
-
// creates concurrent pendingAcquires that thrash tarn's scheduler.
|
|
103
48
|
const overview = await this.dashboardStore.getOverviewMetrics(range);
|
|
104
49
|
if (!overview)
|
|
105
50
|
return emptyOverview();
|
|
106
51
|
const widgets = await this.dashboardStore.getOverviewWidgets(range);
|
|
107
52
|
const sparklineData = await this.dashboardStore.getSparklineData(range);
|
|
108
|
-
// Cache and queue inspectors use Redis/BullMQ (not SQLite), so
|
|
109
|
-
// Promise.all is fine here — different connection pools.
|
|
110
53
|
const [cacheStats, jobQueueStatus] = await Promise.all([
|
|
111
|
-
this.fetchCacheOverview(),
|
|
112
|
-
this.fetchQueueOverview(),
|
|
54
|
+
this.inspectors.fetchCacheOverview(),
|
|
55
|
+
this.inspectors.fetchQueueOverview(),
|
|
113
56
|
]);
|
|
114
57
|
return {
|
|
115
58
|
...overview,
|
|
116
|
-
sparklines:
|
|
117
|
-
avgResponseTime: sparklineData.map((m) => m.avg_duration),
|
|
118
|
-
p95ResponseTime: sparklineData.map((m) => m.p95_duration),
|
|
119
|
-
requestsPerMinute: sparklineData.map((m) => m.request_count),
|
|
120
|
-
errorRate: sparklineData.map((m) => m.request_count > 0
|
|
121
|
-
? round((m.error_count / m.request_count) * 100)
|
|
122
|
-
: 0),
|
|
123
|
-
},
|
|
59
|
+
sparklines: buildSparklines(sparklineData),
|
|
124
60
|
...widgets,
|
|
125
61
|
cacheStats,
|
|
126
62
|
jobQueueStatus,
|
|
@@ -131,22 +67,9 @@ export default class DashboardController {
|
|
|
131
67
|
const range = request.qs().range || '1h';
|
|
132
68
|
return this.withDb(response, 'overviewChart', { range, buckets: [] }, async () => {
|
|
133
69
|
const buckets = await this.dashboardStore.getChartData(range);
|
|
134
|
-
return {
|
|
135
|
-
range,
|
|
136
|
-
buckets: buckets.map((b) => ({
|
|
137
|
-
bucket: b.bucket,
|
|
138
|
-
requestCount: b.request_count,
|
|
139
|
-
avgDuration: b.avg_duration,
|
|
140
|
-
p95Duration: b.p95_duration,
|
|
141
|
-
errorCount: b.error_count,
|
|
142
|
-
queryCount: b.query_count,
|
|
143
|
-
})),
|
|
144
|
-
};
|
|
70
|
+
return { range, buckets: buckets.map(mapChartBucket) };
|
|
145
71
|
});
|
|
146
72
|
}
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
// Requests
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
73
|
async requests({ request, response }) {
|
|
151
74
|
const qs = request.qs();
|
|
152
75
|
const page = Math.max(1, Number(qs.page) || 1);
|
|
@@ -162,9 +85,8 @@ export default class DashboardController {
|
|
|
162
85
|
});
|
|
163
86
|
}
|
|
164
87
|
async requestDetail({ params, response }) {
|
|
165
|
-
if (!this.dashboardStore.isReady())
|
|
88
|
+
if (!this.dashboardStore.isReady())
|
|
166
89
|
return response.notFound({ error: 'Not found' });
|
|
167
|
-
}
|
|
168
90
|
try {
|
|
169
91
|
const detail = await this.dashboardStore.getRequestDetail(Number(params.id));
|
|
170
92
|
if (!detail)
|
|
@@ -173,6 +95,7 @@ export default class DashboardController {
|
|
|
173
95
|
...formatRequest(detail),
|
|
174
96
|
queries: (detail.queries || []).map(formatQuery),
|
|
175
97
|
trace: detail.trace ? formatTrace(detail.trace) : null,
|
|
98
|
+
logs: (detail.logs || []).map(formatLog),
|
|
176
99
|
});
|
|
177
100
|
}
|
|
178
101
|
catch {
|
|
@@ -183,301 +106,49 @@ export default class DashboardController {
|
|
|
183
106
|
return this.withDb(response, 'queriesGrouped', { groups: [] }, async () => {
|
|
184
107
|
const qs = request.qs();
|
|
185
108
|
const limit = clamp(Number(qs.limit) || 50, 1, 200);
|
|
186
|
-
const
|
|
187
|
-
const search = qs.search || undefined;
|
|
188
|
-
const groups = await this.dashboardStore.getQueriesGrouped(limit, sort, search);
|
|
109
|
+
const groups = await this.dashboardStore.getQueriesGrouped(limit, qs.sort || 'total_duration', qs.search || undefined);
|
|
189
110
|
const totalTime = groups.reduce((sum, g) => sum + (g.total_duration || 0), 0);
|
|
190
111
|
return {
|
|
191
|
-
groups: groups.map((g) => (
|
|
192
|
-
sqlNormalized: g.sql_normalized,
|
|
193
|
-
count: g.count,
|
|
194
|
-
avgDuration: round(g.avg_duration),
|
|
195
|
-
minDuration: round(g.min_duration),
|
|
196
|
-
maxDuration: round(g.max_duration),
|
|
197
|
-
totalDuration: round(g.total_duration),
|
|
198
|
-
percentOfTotal: totalTime > 0 ? round((g.total_duration / totalTime) * 100) : 0,
|
|
199
|
-
})),
|
|
112
|
+
groups: groups.map((g) => formatGroupedQuery(g, totalTime)),
|
|
200
113
|
};
|
|
201
114
|
});
|
|
202
115
|
}
|
|
203
|
-
async queryExplain(
|
|
204
|
-
|
|
205
|
-
return response.notFound({ error: 'Not found' });
|
|
206
|
-
}
|
|
207
|
-
try {
|
|
208
|
-
const db = this.dashboardStore.getDb();
|
|
209
|
-
if (!db)
|
|
210
|
-
return response.notFound({ error: 'Not found' });
|
|
211
|
-
const id = Number(params.id);
|
|
212
|
-
const query = await db('server_stats_queries')
|
|
213
|
-
.where('id', id)
|
|
214
|
-
.first();
|
|
215
|
-
if (!query)
|
|
216
|
-
return response.notFound({ error: 'Query not found' });
|
|
217
|
-
const sqlTrimmed = query.sql_text.trim().toUpperCase();
|
|
218
|
-
if (!sqlTrimmed.startsWith('SELECT')) {
|
|
219
|
-
return response.badRequest({
|
|
220
|
-
error: 'EXPLAIN is only supported for SELECT queries',
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
let appDb;
|
|
224
|
-
try {
|
|
225
|
-
const lucid = await this.app.container.make('lucid.db');
|
|
226
|
-
appDb = lucid
|
|
227
|
-
.connection()
|
|
228
|
-
.getWriteClient();
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return response.serviceUnavailable({
|
|
232
|
-
error: 'App database connection not available',
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
let bindings = [];
|
|
236
|
-
if (query.bindings) {
|
|
237
|
-
try {
|
|
238
|
-
bindings = JSON.parse(query.bindings);
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
// If bindings can't be parsed, run without them
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
const explainResult = await appDb.raw(`EXPLAIN (FORMAT JSON) ${query.sql_text}`, bindings);
|
|
245
|
-
let plan = [];
|
|
246
|
-
const rawRows = explainResult?.rows ??
|
|
247
|
-
(Array.isArray(explainResult) ? explainResult : []);
|
|
248
|
-
if (rawRows.length > 0 && rawRows[0]['QUERY PLAN']) {
|
|
249
|
-
plan = rawRows[0]['QUERY PLAN'];
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
plan = rawRows;
|
|
253
|
-
}
|
|
254
|
-
return response.json({ queryId: id, sql: query.sql_text, plan });
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
return response.internalServerError({
|
|
258
|
-
error: 'EXPLAIN failed',
|
|
259
|
-
message: error?.message ?? 'Unknown error',
|
|
260
|
-
});
|
|
261
|
-
}
|
|
116
|
+
async queryExplain(ctx) {
|
|
117
|
+
return handleQueryExplain(this.dashboardStore, this.app, ctx);
|
|
262
118
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
async cacheStats({ request, response }) {
|
|
267
|
-
const inspector = await this.getInspector('cache');
|
|
268
|
-
if (!inspector) {
|
|
269
|
-
return response.json({ available: false, stats: null, keys: [] });
|
|
270
|
-
}
|
|
271
|
-
const qs = request.qs();
|
|
272
|
-
const searchTerm = qs.search || qs.pattern || '';
|
|
273
|
-
const pattern = searchTerm ? `*${searchTerm}*` : '*';
|
|
274
|
-
const cursor = qs.cursor || '0';
|
|
275
|
-
const count = clamp(Number(qs.count) || 100, 1, 500);
|
|
276
|
-
try {
|
|
277
|
-
const [stats, keyList] = await Promise.all([
|
|
278
|
-
inspector.getStats(),
|
|
279
|
-
inspector.listKeys(pattern, cursor, count),
|
|
280
|
-
]);
|
|
281
|
-
return response.json({
|
|
282
|
-
available: true,
|
|
283
|
-
stats,
|
|
284
|
-
keys: keyList.keys,
|
|
285
|
-
cursor: keyList.cursor,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
return response.json({ available: false, stats: null, keys: [] });
|
|
290
|
-
}
|
|
119
|
+
async cacheStats(ctx) {
|
|
120
|
+
return handleCacheStats(this.inspectors, ctx);
|
|
291
121
|
}
|
|
292
|
-
async cacheKey(
|
|
293
|
-
|
|
294
|
-
if (!inspector) {
|
|
295
|
-
return response.notFound({ error: 'Cache not available' });
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const key = decodeURIComponent(params.key);
|
|
299
|
-
const detail = await inspector.getKey(key);
|
|
300
|
-
if (!detail)
|
|
301
|
-
return response.notFound({ error: 'Key not found' });
|
|
302
|
-
return response.json(detail);
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
return response.notFound({ error: 'Key not found' });
|
|
306
|
-
}
|
|
122
|
+
async cacheKey(ctx) {
|
|
123
|
+
return handleCacheKey(this.inspectors, ctx);
|
|
307
124
|
}
|
|
308
|
-
async cacheKeyDelete(
|
|
309
|
-
|
|
310
|
-
if (!inspector) {
|
|
311
|
-
return response.notFound({ error: 'Cache not available' });
|
|
312
|
-
}
|
|
313
|
-
try {
|
|
314
|
-
const key = decodeURIComponent(params.key);
|
|
315
|
-
const deleted = await inspector.deleteKey(key);
|
|
316
|
-
if (!deleted)
|
|
317
|
-
return response.notFound({ error: 'Key not found' });
|
|
318
|
-
return response.json({ deleted: true });
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
return response.internalServerError({ error: 'Failed to delete cache key' });
|
|
322
|
-
}
|
|
125
|
+
async cacheKeyDelete(ctx) {
|
|
126
|
+
return handleCacheKeyDelete(this.inspectors, ctx);
|
|
323
127
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
327
|
-
async jobs({ request, response }) {
|
|
328
|
-
const inspector = await this.getInspector('queue');
|
|
329
|
-
if (!inspector) {
|
|
330
|
-
return response.json({
|
|
331
|
-
available: false,
|
|
332
|
-
overview: null,
|
|
333
|
-
stats: null,
|
|
334
|
-
jobs: [],
|
|
335
|
-
total: 0,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
const qs = request.qs();
|
|
339
|
-
const status = qs.status || 'all';
|
|
340
|
-
const searchTerm = qs.search || '';
|
|
341
|
-
const page = Math.max(1, Number(qs.page) || 1);
|
|
342
|
-
const perPage = clamp(Number(qs.perPage) || Number(qs.limit) || 25, 1, 100);
|
|
343
|
-
try {
|
|
344
|
-
const [overview, jobList] = await Promise.all([
|
|
345
|
-
inspector.getOverview(),
|
|
346
|
-
inspector.listJobs(status, page, perPage),
|
|
347
|
-
]);
|
|
348
|
-
// Filter jobs by search term (name match) if provided
|
|
349
|
-
let filteredJobs = jobList.jobs;
|
|
350
|
-
let filteredTotal = jobList.total;
|
|
351
|
-
if (searchTerm) {
|
|
352
|
-
const term = searchTerm.toLowerCase();
|
|
353
|
-
filteredJobs = jobList.jobs.filter((j) => j.name?.toLowerCase().includes(term) || j.id?.toString().toLowerCase().includes(term));
|
|
354
|
-
filteredTotal = filteredJobs.length;
|
|
355
|
-
}
|
|
356
|
-
return response.json({
|
|
357
|
-
available: true,
|
|
358
|
-
overview,
|
|
359
|
-
stats: overview,
|
|
360
|
-
jobs: filteredJobs,
|
|
361
|
-
total: filteredTotal,
|
|
362
|
-
page,
|
|
363
|
-
perPage,
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
catch {
|
|
367
|
-
return response.json({
|
|
368
|
-
available: false,
|
|
369
|
-
overview: null,
|
|
370
|
-
stats: null,
|
|
371
|
-
jobs: [],
|
|
372
|
-
total: 0,
|
|
373
|
-
});
|
|
374
|
-
}
|
|
128
|
+
async jobs(ctx) {
|
|
129
|
+
return handleJobs(this.inspectors, ctx);
|
|
375
130
|
}
|
|
376
|
-
async jobDetail(
|
|
377
|
-
|
|
378
|
-
if (!inspector) {
|
|
379
|
-
return response.notFound({ error: 'Queue not available' });
|
|
380
|
-
}
|
|
381
|
-
try {
|
|
382
|
-
const detail = await inspector.getJob(String(params.id));
|
|
383
|
-
if (!detail)
|
|
384
|
-
return response.notFound({ error: 'Job not found' });
|
|
385
|
-
return response.json(detail);
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
return response.notFound({ error: 'Job not found' });
|
|
389
|
-
}
|
|
131
|
+
async jobDetail(ctx) {
|
|
132
|
+
return handleJobDetail(this.inspectors, ctx);
|
|
390
133
|
}
|
|
391
|
-
async jobRetry(
|
|
392
|
-
|
|
393
|
-
if (!inspector) {
|
|
394
|
-
return response.notFound({ error: 'Queue not available' });
|
|
395
|
-
}
|
|
396
|
-
try {
|
|
397
|
-
const success = await inspector.retryJob(String(params.id));
|
|
398
|
-
if (!success) {
|
|
399
|
-
return response.badRequest({
|
|
400
|
-
error: 'Job could not be retried (not in failed state)',
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
return response.json({ success: true });
|
|
404
|
-
}
|
|
405
|
-
catch {
|
|
406
|
-
return response.internalServerError({ error: 'Retry failed' });
|
|
407
|
-
}
|
|
134
|
+
async jobRetry(ctx) {
|
|
135
|
+
return handleJobRetry(this.inspectors, ctx);
|
|
408
136
|
}
|
|
409
|
-
// ---------------------------------------------------------------------------
|
|
410
|
-
// Config
|
|
411
|
-
// ---------------------------------------------------------------------------
|
|
412
137
|
async config({ response }) {
|
|
413
|
-
const configData = this.configInspector.getConfig();
|
|
414
|
-
const envData = this.configInspector.getEnvVars();
|
|
415
138
|
return response.json({
|
|
416
|
-
app:
|
|
417
|
-
env:
|
|
139
|
+
app: this.configInspector.getConfig().config,
|
|
140
|
+
env: this.configInspector.getEnvVars().env,
|
|
418
141
|
});
|
|
419
142
|
}
|
|
420
|
-
// ---------------------------------------------------------------------------
|
|
421
|
-
// Saved Filters
|
|
422
|
-
// ---------------------------------------------------------------------------
|
|
423
143
|
async savedFilters({ response }) {
|
|
424
|
-
return this.withDb(response, 'savedFilters', { filters: [] },
|
|
425
|
-
const filters = await this.dashboardStore.getSavedFilters();
|
|
426
|
-
return {
|
|
427
|
-
filters: filters.map((f) => ({
|
|
428
|
-
id: f.id,
|
|
429
|
-
name: f.name,
|
|
430
|
-
section: f.section,
|
|
431
|
-
filterConfig: safeParseJson(f.filter_config),
|
|
432
|
-
createdAt: f.created_at,
|
|
433
|
-
})),
|
|
434
|
-
};
|
|
435
|
-
});
|
|
144
|
+
return this.withDb(response, 'savedFilters', { filters: [] }, () => handleSavedFilters(this.dashboardStore));
|
|
436
145
|
}
|
|
437
|
-
async createSavedFilter(
|
|
438
|
-
|
|
439
|
-
return response.serviceUnavailable({ error: 'Database not available' });
|
|
440
|
-
}
|
|
441
|
-
try {
|
|
442
|
-
const body = request.body();
|
|
443
|
-
const { name, section, filterConfig } = body;
|
|
444
|
-
if (!name || !section || !filterConfig) {
|
|
445
|
-
return response.badRequest({
|
|
446
|
-
error: 'Missing required fields: name, section, filterConfig',
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
const configObj = typeof filterConfig === 'string' ? safeParseJson(filterConfig) : filterConfig;
|
|
450
|
-
const result = await this.dashboardStore.createSavedFilter(name, section, configObj);
|
|
451
|
-
if (!result) {
|
|
452
|
-
return response.serviceUnavailable({ error: 'Database not available' });
|
|
453
|
-
}
|
|
454
|
-
return response.json(result);
|
|
455
|
-
}
|
|
456
|
-
catch {
|
|
457
|
-
return response.internalServerError({ error: 'Failed to create filter' });
|
|
458
|
-
}
|
|
146
|
+
async createSavedFilter(ctx) {
|
|
147
|
+
return handleCreateSavedFilter(this.dashboardStore, ctx);
|
|
459
148
|
}
|
|
460
|
-
async deleteSavedFilter(
|
|
461
|
-
|
|
462
|
-
return response.serviceUnavailable({ error: 'Database not available' });
|
|
463
|
-
}
|
|
464
|
-
try {
|
|
465
|
-
const deleted = await this.dashboardStore.deleteSavedFilter(Number(params.id));
|
|
466
|
-
if (!deleted)
|
|
467
|
-
return response.notFound({ error: 'Filter not found' });
|
|
468
|
-
return response.json({ success: true });
|
|
469
|
-
}
|
|
470
|
-
catch {
|
|
471
|
-
return response.internalServerError({ error: 'Failed to delete filter' });
|
|
472
|
-
}
|
|
149
|
+
async deleteSavedFilter(ctx) {
|
|
150
|
+
return handleDeleteSavedFilter(this.dashboardStore, ctx);
|
|
473
151
|
}
|
|
474
|
-
// ---------------------------------------------------------------------------
|
|
475
|
-
// Private helpers
|
|
476
|
-
// ---------------------------------------------------------------------------
|
|
477
|
-
/**
|
|
478
|
-
* Wraps a store call with null-guard + try/catch boilerplate.
|
|
479
|
-
* Returns emptyValue if the store is not ready or the fn throws.
|
|
480
|
-
*/
|
|
481
152
|
async withDb(response, label, emptyValue, fn) {
|
|
482
153
|
if (!this.dashboardStore.isReady())
|
|
483
154
|
return response.json(emptyValue);
|
|
@@ -504,158 +175,7 @@ export default class DashboardController {
|
|
|
504
175
|
}
|
|
505
176
|
}
|
|
506
177
|
getDashboardPath() {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
async getInspector(type) {
|
|
511
|
-
if (type === 'cache') {
|
|
512
|
-
if (this.cacheAvailable === false)
|
|
513
|
-
return null;
|
|
514
|
-
if (this.cacheInspector)
|
|
515
|
-
return this.cacheInspector;
|
|
516
|
-
try {
|
|
517
|
-
const available = await CacheInspector.isAvailable(this.app);
|
|
518
|
-
this.cacheAvailable = available;
|
|
519
|
-
if (!available) {
|
|
520
|
-
log.info('dashboard: Redis not detected — Cache panel disabled');
|
|
521
|
-
return null;
|
|
522
|
-
}
|
|
523
|
-
const redis = await this.app.container.make('redis');
|
|
524
|
-
this.cacheInspector = new CacheInspector(redis);
|
|
525
|
-
return this.cacheInspector;
|
|
526
|
-
}
|
|
527
|
-
catch (err) {
|
|
528
|
-
this.cacheAvailable = false;
|
|
529
|
-
log.warn('dashboard: CacheInspector init failed — ' + err?.message);
|
|
530
|
-
return null;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
else {
|
|
534
|
-
if (this.queueAvailable === false)
|
|
535
|
-
return null;
|
|
536
|
-
if (this.queueInspector)
|
|
537
|
-
return this.queueInspector;
|
|
538
|
-
try {
|
|
539
|
-
const available = await QueueInspector.isAvailable(this.app);
|
|
540
|
-
this.queueAvailable = available;
|
|
541
|
-
if (!available) {
|
|
542
|
-
log.info('dashboard: Queue not detected — Jobs panel disabled');
|
|
543
|
-
return null;
|
|
544
|
-
}
|
|
545
|
-
const queue = await this.app.container.make('rlanz/queue');
|
|
546
|
-
this.queueInspector = new QueueInspector(queue);
|
|
547
|
-
return this.queueInspector;
|
|
548
|
-
}
|
|
549
|
-
catch (err) {
|
|
550
|
-
this.queueAvailable = false;
|
|
551
|
-
log.warn('dashboard: QueueInspector init failed — ' + err?.message);
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
/** Fetch cache overview stats for the overview page. */
|
|
557
|
-
async fetchCacheOverview() {
|
|
558
|
-
try {
|
|
559
|
-
const inspector = await this.getInspector('cache');
|
|
560
|
-
if (!inspector)
|
|
561
|
-
return null;
|
|
562
|
-
const stats = await inspector.getStats();
|
|
563
|
-
return {
|
|
564
|
-
available: true,
|
|
565
|
-
totalKeys: stats.totalKeys,
|
|
566
|
-
hitRate: stats.hitRate,
|
|
567
|
-
memoryUsedHuman: stats.memoryUsedHuman,
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
catch {
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
/** Fetch queue overview stats for the overview page. */
|
|
575
|
-
async fetchQueueOverview() {
|
|
576
|
-
try {
|
|
577
|
-
const inspector = await this.getInspector('queue');
|
|
578
|
-
if (!inspector)
|
|
579
|
-
return null;
|
|
580
|
-
const overview = await inspector.getOverview();
|
|
581
|
-
return {
|
|
582
|
-
available: true,
|
|
583
|
-
active: overview.active,
|
|
584
|
-
waiting: overview.waiting,
|
|
585
|
-
failed: overview.failed,
|
|
586
|
-
completed: overview.completed,
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
catch {
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
178
|
+
return (this.app.config.get('server_stats')?.devToolbar?.dashboardPath ??
|
|
179
|
+
'/__stats');
|
|
592
180
|
}
|
|
593
181
|
}
|
|
594
|
-
// ---------------------------------------------------------------------------
|
|
595
|
-
// Formatting helpers (snake_case DB rows → camelCase API response)
|
|
596
|
-
// ---------------------------------------------------------------------------
|
|
597
|
-
function formatRequest(r) {
|
|
598
|
-
return {
|
|
599
|
-
id: r.id,
|
|
600
|
-
method: r.method,
|
|
601
|
-
url: r.url,
|
|
602
|
-
statusCode: r.status_code,
|
|
603
|
-
duration: r.duration,
|
|
604
|
-
spanCount: r.span_count,
|
|
605
|
-
warningCount: r.warning_count,
|
|
606
|
-
createdAt: r.created_at,
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
function formatQuery(q) {
|
|
610
|
-
return {
|
|
611
|
-
id: q.id,
|
|
612
|
-
requestId: q.request_id,
|
|
613
|
-
sql: q.sql_text,
|
|
614
|
-
sqlNormalized: q.sql_normalized,
|
|
615
|
-
bindings: safeParseJson(q.bindings),
|
|
616
|
-
duration: q.duration,
|
|
617
|
-
method: q.method,
|
|
618
|
-
model: q.model,
|
|
619
|
-
connection: q.connection,
|
|
620
|
-
inTransaction: !!q.in_transaction,
|
|
621
|
-
createdAt: q.created_at,
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
function formatTrace(t) {
|
|
625
|
-
return {
|
|
626
|
-
id: t.id,
|
|
627
|
-
requestId: t.request_id,
|
|
628
|
-
method: t.method,
|
|
629
|
-
url: t.url,
|
|
630
|
-
statusCode: t.status_code,
|
|
631
|
-
totalDuration: t.total_duration,
|
|
632
|
-
spanCount: t.span_count,
|
|
633
|
-
spans: safeParseJson(t.spans) ?? [],
|
|
634
|
-
warnings: safeParseJsonArray(t.warnings),
|
|
635
|
-
createdAt: t.created_at,
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
function emptyOverview() {
|
|
639
|
-
return {
|
|
640
|
-
avgResponseTime: 0,
|
|
641
|
-
p95ResponseTime: 0,
|
|
642
|
-
requestsPerMinute: 0,
|
|
643
|
-
errorRate: 0,
|
|
644
|
-
sparklines: {
|
|
645
|
-
avgResponseTime: [],
|
|
646
|
-
p95ResponseTime: [],
|
|
647
|
-
requestsPerMinute: [],
|
|
648
|
-
errorRate: [],
|
|
649
|
-
},
|
|
650
|
-
slowestEndpoints: [],
|
|
651
|
-
queryStats: { total: 0, avgDuration: 0, perRequest: 0 },
|
|
652
|
-
recentErrors: [],
|
|
653
|
-
topEvents: [],
|
|
654
|
-
emailActivity: { sent: 0, queued: 0, failed: 0 },
|
|
655
|
-
logLevelBreakdown: { error: 0, warn: 0, info: 0, debug: 0 },
|
|
656
|
-
statusDistribution: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
|
|
657
|
-
slowestQueries: [],
|
|
658
|
-
cacheStats: null,
|
|
659
|
-
jobQueueStatus: null,
|
|
660
|
-
};
|
|
661
|
-
}
|