adonisjs-server-stats 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/field-resolvers.d.ts +64 -0
- package/dist/core/formatters-helpers.d.ts +23 -0
- package/dist/core/formatters.d.ts +15 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +599 -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/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 +10 -442
- package/dist/react/CacheSection-BYN53kYO.js +135 -0
- package/dist/react/CacheStatsBar-CRodCOeP.js +27 -0
- package/dist/react/CacheTab-DOhuK05d.js +106 -0
- package/dist/react/{ConfigSection-DfFd-WRq.js → ConfigSection-B9EHh4Rp.js} +1 -1
- package/dist/react/{ConfigTab-Bdg8YMer.js → ConfigTab-C8kriE2b.js} +1 -1
- package/dist/react/CustomPaneTab-CvzQS_Wh.js +99 -0
- package/dist/react/EmailPreviewOverlay-BmXOAvqG.js +58 -0
- package/dist/react/EmailsSection-BJyFJf7A.js +226 -0
- package/dist/react/EmailsTab-Ch8jp10B.js +110 -0
- package/dist/react/{EventsSection-ByQ-9blq.js → EventsSection-DJPwHeT8.js} +28 -27
- package/dist/react/EventsTab-B-FoehXC.js +58 -0
- package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
- package/dist/react/{InternalsContent-DBzsI0CG.js → InternalsContent-O8ino9oM.js} +133 -109
- package/dist/react/InternalsSection-B6VlVx5f.js +22 -0
- package/dist/react/InternalsTab-CkEKpRMU.js +17 -0
- package/dist/react/JobStatsBar-C7RslAFE.js +30 -0
- package/dist/react/JobsSection-DWF4i1t_.js +167 -0
- package/dist/react/JobsTab-DqnifQXV.js +129 -0
- package/dist/react/LogEntryRow-CMMkqA9M.js +43 -0
- package/dist/react/LogsSection-C1xC5aP4.js +198 -0
- package/dist/react/LogsTab-CS4sLfLw.js +79 -0
- package/dist/react/{OverviewSection-C4T1ur51.js → OverviewSection-CxvfOR0v.js} +70 -80
- package/dist/react/QueriesSection-CrMdU5Ax.js +458 -0
- package/dist/react/{QueriesTab-osLUWd4L.js → QueriesTab-x85PjkyS.js} +38 -40
- package/dist/react/RequestsSection-DETN9oZb.js +321 -0
- package/dist/react/{RoutesSection-BUSkM6PY.js → RoutesSection-CmorkJeC.js} +2 -2
- package/dist/react/RoutesTab-CbzBOzpc.js +68 -0
- package/dist/react/SplitPaneWrapper-BiIgT4ND.js +49 -0
- package/dist/react/TimeAgoCell-o3KigGfM.js +8 -0
- package/dist/react/{TimelineTab-Covg5weo.js → TimelineTab-Ue9tUD_n.js} +76 -102
- package/dist/react/index-DwDK-4oX.js +1121 -0
- package/dist/react/index.js +6 -6
- package/dist/react/react/components/shared/CacheStatsBar.d.ts +13 -0
- package/dist/react/react/components/shared/EmailPreviewOverlay.d.ts +29 -0
- package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
- package/dist/react/react/components/shared/JobStatsBar.d.ts +12 -0
- package/dist/react/react/components/shared/LogEntryRow.d.ts +9 -0
- package/dist/react/react/components/shared/RelatedLogs.d.ts +2 -2
- package/dist/react/react/components/shared/SplitPaneWrapper.d.ts +7 -0
- package/dist/react/react/components/shared/TimeAgoCell.d.ts +17 -0
- package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
- package/dist/react/react/hooks/useDiagnosticsData.d.ts +14 -0
- 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 +51 -544
- 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 -218
- package/dist/src/dashboard/dashboard_store.js +115 -1116
- 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 -159
- 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 +2 -39
- package/dist/src/data/data_access.js +17 -193
- 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 +9 -1
- package/dist/src/debug/trace_collector.js +21 -15
- package/dist/src/debug/types.d.ts +1 -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 +4 -4
- 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 +2 -8
- package/dist/src/middleware/request_tracking_middleware.js +65 -93
- 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 +132 -951
- 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 -352
- 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 +163 -0
- package/dist/src/styles/dashboard.css +13 -105
- package/dist/src/styles/debug-panel.css +2 -53
- package/dist/src/styles/utilities.css +3 -1
- package/dist/src/types.d.ts +305 -14
- package/dist/vue/{CacheSection-oFAJL3mo.js → CacheSection-DT2Mwf_s.js} +1 -1
- package/dist/vue/{ConfigSection-BhfJ4KqL.js → ConfigSection-BwKwS9lh.js} +1 -1
- package/dist/vue/CustomPaneTab-Hr1IBHfz.js +172 -0
- package/dist/vue/{EmailsSection-BcNyhyHs.js → EmailsSection-B65g0FVS.js} +1 -1
- package/dist/vue/{EventsSection-r60Q5Lmu.js → EventsSection-CxqtVF-o.js} +1 -1
- package/dist/vue/{JobsSection-BHL-hkQw.js → JobsSection-rMIyMb-g.js} +1 -1
- package/dist/vue/{LogsSection-DRMGzJmg.js → LogsSection-DmmZVJ7D.js} +9 -3
- package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
- package/dist/vue/{OverviewSection-CXh6Ja1B.js → OverviewSection-BMabyqw-.js} +49 -50
- package/dist/vue/{QueriesSection-IodIsCJ-.js → QueriesSection-BfDFwGqH.js} +44 -45
- package/dist/vue/{QueriesTab-C8_7oprC.js → QueriesTab-DuTG7cpC.js} +30 -31
- package/dist/vue/RelatedLogs.vue_vue_type_script_setup_true_lang-Py1iu9GU.js +77 -0
- package/dist/vue/{RequestsSection-BPuMdmMc.js → RequestsSection-CTu4jPZ_.js} +143 -147
- package/dist/vue/{RoutesSection-NKo3Rbq3.js → RoutesSection-zQZDedL7.js} +1 -1
- package/dist/vue/TimelineTab-DHfXsX7t.js +334 -0
- package/dist/vue/components/shared/RelatedLogs.vue.d.ts +1 -4
- package/dist/vue/composables/useDashboardData.d.ts +12 -23
- package/dist/vue/index-CM3yNVUR.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/CacheSection-UCMptWyn.js +0 -146
- package/dist/react/CacheTab-CA8LB1J5.js +0 -123
- package/dist/react/CustomPaneTab-Bxtv_8Rw.js +0 -104
- package/dist/react/EmailsSection-CM7stSyh.js +0 -262
- package/dist/react/EmailsTab-BDhEiomM.js +0 -153
- package/dist/react/EventsTab-CMfY98Rl.js +0 -63
- package/dist/react/InternalsSection-t7ihcWO-.js +0 -32
- package/dist/react/InternalsTab-Oij0A2fN.js +0 -30
- package/dist/react/JobsSection-DF3qEv9O.js +0 -187
- package/dist/react/JobsTab-BbrBWIOb.js +0 -141
- package/dist/react/LogsSection-DcFTZY7b.js +0 -227
- package/dist/react/LogsTab-CicucmVk.js +0 -103
- package/dist/react/QueriesSection-PswteoF9.js +0 -461
- package/dist/react/RelatedLogs-DFDOyUMr.js +0 -40
- package/dist/react/RequestsSection-Nag30rEA.js +0 -341
- package/dist/react/RoutesTab-DgVzd2PZ.js +0 -74
- package/dist/react/index-Cflz9Ebj.js +0 -1069
- package/dist/vue/CustomPaneTab-BJxT5Dp7.js +0 -172
- package/dist/vue/RelatedLogs.vue_vue_type_script_setup_true_lang-CB2_TzYW.js +0 -84
- package/dist/vue/TimelineTab-zj5Z5OdT.js +0 -338
- package/dist/vue/index-Dtgysd26.js +0 -1229
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { getLogStreamService } from '../collectors/log_collector.js';
|
|
3
|
-
import { DebugStore } from '../debug/debug_store.js';
|
|
4
1
|
import { StatsEngine } from '../engine/stats_engine.js';
|
|
5
|
-
import {
|
|
6
|
-
import { setShouldShow, setTraceCollector, setDashboardPath, setExcludedPrefixes, setOnRequestComplete, } from '../middleware/request_tracking_middleware.js';
|
|
2
|
+
import { setShouldShow, setExcludedPrefixes } from '../middleware/request_tracking_middleware.js';
|
|
7
3
|
import { registerAllRoutes } from '../routes/register_routes.js';
|
|
8
|
-
import { log, dim,
|
|
9
|
-
import {
|
|
4
|
+
import { log, dim, setVerbose } from '../utils/logger.js';
|
|
5
|
+
import { deriveEndpointPaths, computeDashboardPath, collectRegisteredPaths, warnAboutAuthMiddleware, } from './boot_helpers.js';
|
|
6
|
+
import { resolveToolbarConfig, buildExcludedPrefixes } from './dashboard_setup.js';
|
|
7
|
+
import { buildDiagnostics } from './diagnostics.js';
|
|
8
|
+
import { hookPinoToLogStream, setupLogStreamBroadcast, setupStatsIntervalHelper, checkDashboardDepsHelper, registerEdgePluginHelper, setupNonWebBridgeHelper, setupDevToolbarCore, applyToolbarResult, } from './provider_helpers_extra.js';
|
|
9
|
+
import { clearAllTimers, persistDebugData, unsubscribeEmailBridge, cleanupResources, } from './shutdown_helpers.js';
|
|
10
10
|
export default class ServerStatsProvider {
|
|
11
11
|
app;
|
|
12
12
|
intervalId = null;
|
|
@@ -22,14 +22,10 @@ export default class ServerStatsProvider {
|
|
|
22
22
|
statsController = null;
|
|
23
23
|
debugController = null;
|
|
24
24
|
apiController = null;
|
|
25
|
-
// Dashboard dependency check (set in boot, read in ready)
|
|
26
25
|
dashboardDepsAvailable = true;
|
|
27
|
-
// Redis email bridge (cross-process email capture)
|
|
28
26
|
emailBridgeRedis = null;
|
|
29
27
|
emailBridgeChannel = 'adonisjs-server-stats:emails';
|
|
30
|
-
// Log stream (merged from LogStreamProvider)
|
|
31
28
|
logStreamService = null;
|
|
32
|
-
// Diagnostics tracking
|
|
33
29
|
pinoHookActive = false;
|
|
34
30
|
edgePluginActive = false;
|
|
35
31
|
prometheusActive = false;
|
|
@@ -41,998 +37,183 @@ export default class ServerStatsProvider {
|
|
|
41
37
|
this.app = app;
|
|
42
38
|
}
|
|
43
39
|
async boot() {
|
|
44
|
-
// In non-web environments (queue workers, scheduler, REPL), skip all
|
|
45
|
-
// route registration, Edge plugin, and heavy initialization. The
|
|
46
|
-
// email bridge publisher is set up in ready() instead.
|
|
47
40
|
if (this.app.getEnvironment() !== 'web')
|
|
48
41
|
return;
|
|
49
42
|
try {
|
|
50
|
-
await this.
|
|
43
|
+
await this.initBoot();
|
|
51
44
|
}
|
|
52
45
|
catch (err) {
|
|
53
|
-
log.warn(`boot failed: ${err?.message ?? err}\n`
|
|
54
|
-
|
|
55
|
-
if (err?.stack) {
|
|
46
|
+
log.warn(`boot failed: ${err?.message ?? err}\n ${dim('The server will continue without server-stats.')}`);
|
|
47
|
+
if (err?.stack)
|
|
56
48
|
console.error(err.stack);
|
|
57
|
-
}
|
|
58
49
|
}
|
|
59
50
|
}
|
|
60
|
-
async
|
|
51
|
+
async initBoot() {
|
|
61
52
|
const config = this.app.config.get('server_stats');
|
|
62
53
|
if (!config) {
|
|
63
54
|
log.warn('no config found — is config/server_stats.ts set up?');
|
|
64
55
|
return;
|
|
65
56
|
}
|
|
66
|
-
// Re-apply verbose setting from resolved config so the logger
|
|
67
|
-
// respects it even if defineConfig() ran in a separate context.
|
|
68
57
|
setVerbose(config.verbose);
|
|
69
58
|
log.info('booting...');
|
|
70
|
-
|
|
71
|
-
if (config.shouldShow) {
|
|
59
|
+
if (config.shouldShow)
|
|
72
60
|
setShouldShow(config.shouldShow);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
router = await this.app.container.make('router');
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Router not available — skip all route registration
|
|
80
|
-
}
|
|
81
|
-
if (router && !this.app.inProduction) {
|
|
82
|
-
const registeredPaths = [];
|
|
83
|
-
const r = router;
|
|
84
|
-
const toolbarConfig = config.devToolbar;
|
|
85
|
-
// Derive endpoint paths for route registration
|
|
86
|
-
const statsEndpoint = typeof config.endpoint === 'string' ? config.endpoint : false;
|
|
87
|
-
const debugEndpoint = toolbarConfig?.enabled
|
|
88
|
-
? (toolbarConfig.debugEndpoint ?? '/admin/api/debug')
|
|
89
|
-
: undefined;
|
|
90
|
-
// Check dashboard dependencies before registering dashboard routes.
|
|
91
|
-
// Must use appImport — bare import() resolves to this package's
|
|
92
|
-
// devDeps when symlinked, not the app's actual dependencies.
|
|
93
|
-
if (toolbarConfig?.enabled && toolbarConfig.dashboard) {
|
|
94
|
-
const { appImport } = await import('../utils/app_import.js');
|
|
95
|
-
const missing = [];
|
|
96
|
-
try {
|
|
97
|
-
await appImport('knex');
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
missing.push('knex');
|
|
101
|
-
}
|
|
102
|
-
try {
|
|
103
|
-
await appImport('better-sqlite3');
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
missing.push('better-sqlite3');
|
|
107
|
-
}
|
|
108
|
-
if (missing.length > 0) {
|
|
109
|
-
this.dashboardDepsAvailable = false;
|
|
110
|
-
log.block(`Dashboard requires ${missing.join(' and ')}. Install with:`, [
|
|
111
|
-
'',
|
|
112
|
-
bold(`npm install ${missing.join(' ')}`),
|
|
113
|
-
'',
|
|
114
|
-
dim('Dashboard routes have been skipped for now.'),
|
|
115
|
-
dim('Everything else (stats bar, debug panel) works without it.'),
|
|
116
|
-
]);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const dashboardPath = toolbarConfig?.enabled && toolbarConfig.dashboard && this.dashboardDepsAvailable
|
|
120
|
-
? (toolbarConfig.dashboardPath ?? '/__stats')
|
|
121
|
-
: undefined;
|
|
122
|
-
// ── Register all routes via the unified registrar ──────────
|
|
123
|
-
registerAllRoutes({
|
|
124
|
-
router: r,
|
|
125
|
-
getApiController: () => this.apiController,
|
|
126
|
-
getStatsController: () => this.statsController,
|
|
127
|
-
getDebugController: () => this.debugController,
|
|
128
|
-
getDashboardController: () => this.dashboardController,
|
|
129
|
-
statsEndpoint,
|
|
130
|
-
debugEndpoint,
|
|
131
|
-
dashboardPath,
|
|
132
|
-
shouldShow: config.shouldShow,
|
|
133
|
-
});
|
|
134
|
-
// Track which paths were registered for logging
|
|
135
|
-
if (typeof statsEndpoint === 'string') {
|
|
136
|
-
registeredPaths.push(statsEndpoint);
|
|
137
|
-
}
|
|
138
|
-
if (debugEndpoint) {
|
|
139
|
-
registeredPaths.push(debugEndpoint + '/*');
|
|
140
|
-
}
|
|
141
|
-
if (dashboardPath) {
|
|
142
|
-
registeredPaths.push(dashboardPath + '/*');
|
|
143
|
-
}
|
|
144
|
-
// Log registered routes
|
|
145
|
-
if (registeredPaths.length > 0) {
|
|
146
|
-
log.list('routes auto-registered (no manual setup needed):', registeredPaths);
|
|
147
|
-
// Only warn about global auth middleware if:
|
|
148
|
-
// 1. shouldShow is NOT configured (user hasn't set up access control)
|
|
149
|
-
// 2. There IS auth middleware in server.use() or router.use()
|
|
150
|
-
if (!config.shouldShow) {
|
|
151
|
-
const authMiddleware = this.detectGlobalAuthMiddleware();
|
|
152
|
-
if (authMiddleware.length > 0) {
|
|
153
|
-
log.block(bold('found global auth middleware that will run on every poll:'), [
|
|
154
|
-
...authMiddleware.map((m) => `${dim('→')} ${m}`),
|
|
155
|
-
'',
|
|
156
|
-
dim('these routes get polled every ~3s, so auth middleware will'),
|
|
157
|
-
dim('trigger a DB query on each poll. here are two ways to fix it:'),
|
|
158
|
-
'',
|
|
159
|
-
`${bold('option 1:')} add a shouldShow callback to your config:`,
|
|
160
|
-
'',
|
|
161
|
-
dim('// config/server_stats.ts'),
|
|
162
|
-
dim("shouldShow: (ctx) => ctx.auth?.user?.role === 'admin'"),
|
|
163
|
-
'',
|
|
164
|
-
`${bold('option 2:')} move auth middleware from router.use() to a route group:`,
|
|
165
|
-
'',
|
|
166
|
-
dim('// start/kernel.ts — remove from router.use()'),
|
|
167
|
-
dim("// () => import('#middleware/silent_auth_middleware')"),
|
|
168
|
-
'',
|
|
169
|
-
dim('// start/routes.ts — add to your route groups instead'),
|
|
170
|
-
dim('router.group(() => { ... }).use(middleware.silentAuth())'),
|
|
171
|
-
]);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (!this.app.usingEdgeJS)
|
|
177
|
-
return;
|
|
178
|
-
try {
|
|
179
|
-
// Must use appImport for edge.js — when this package is symlinked,
|
|
180
|
-
// bare import('edge.js') resolves to the package's devDep copy,
|
|
181
|
-
// which is a different singleton than the app's Edge instance.
|
|
182
|
-
const { appImport } = await import('../utils/app_import.js');
|
|
183
|
-
const edge = await appImport('edge.js');
|
|
184
|
-
const { edgePluginServerStats } = await import('../edge/plugin.js');
|
|
185
|
-
edge.default.use(edgePluginServerStats(config));
|
|
186
|
-
this.edgePluginActive = true;
|
|
187
|
-
}
|
|
188
|
-
catch (err) {
|
|
189
|
-
log.warn('could not register Edge plugin — @serverStats() tag will not work: ' +
|
|
190
|
-
err?.message);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Read start/kernel.ts and detect auth-related middleware in server.use()
|
|
195
|
-
* or router.use() blocks. Returns import paths of problematic middleware.
|
|
196
|
-
*
|
|
197
|
-
* Ignores initialize_auth_middleware (no DB query — just sets up ctx.auth).
|
|
198
|
-
*/
|
|
199
|
-
detectGlobalAuthMiddleware() {
|
|
200
|
-
const found = [];
|
|
201
|
-
try {
|
|
202
|
-
// Try both .ts and .js extensions
|
|
203
|
-
let source = '';
|
|
204
|
-
for (const ext of ['ts', 'js']) {
|
|
205
|
-
try {
|
|
206
|
-
source = readFileSync(this.app.makePath('start', `kernel.${ext}`), 'utf-8');
|
|
207
|
-
if (source)
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
// Try next extension
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (!source)
|
|
215
|
-
return found;
|
|
216
|
-
// Extract server.use([...]) and router.use([...]) blocks
|
|
217
|
-
const useBlockRegex = /(?:server|router)\.use\(\s*\[([\s\S]*?)\]\s*\)/g;
|
|
218
|
-
let match;
|
|
219
|
-
while ((match = useBlockRegex.exec(source)) !== null) {
|
|
220
|
-
const block = match[1];
|
|
221
|
-
// Find all import paths in this block
|
|
222
|
-
const importRegex = /import\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
223
|
-
let importMatch;
|
|
224
|
-
while ((importMatch = importRegex.exec(block)) !== null) {
|
|
225
|
-
const importPath = importMatch[1];
|
|
226
|
-
// Skip initialize_auth_middleware — it just sets up ctx.auth, no DB query
|
|
227
|
-
if (importPath.includes('initialize_auth'))
|
|
228
|
-
continue;
|
|
229
|
-
// Detect auth-related middleware
|
|
230
|
-
if (importPath.includes('auth') ||
|
|
231
|
-
importPath.includes('silent_auth') ||
|
|
232
|
-
importPath.includes('silentAuth')) {
|
|
233
|
-
found.push(importPath);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
// Can't read kernel file — skip detection
|
|
240
|
-
}
|
|
241
|
-
return found;
|
|
61
|
+
await this.registerRoutes(config);
|
|
62
|
+
this.edgePluginActive = await registerEdgePluginHelper(this.app, config);
|
|
242
63
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
*
|
|
247
|
-
* Uses pino's exported `symbols.streamSym` to access the underlying
|
|
248
|
-
* destination stream, then wraps its `write` method to tee entries
|
|
249
|
-
* into the log collector.
|
|
250
|
-
*/
|
|
251
|
-
async hookPinoLogger() {
|
|
252
|
-
const logStream = getLogStreamService();
|
|
253
|
-
if (!logStream)
|
|
64
|
+
async registerRoutes(config) {
|
|
65
|
+
const router = await this.resolve('router');
|
|
66
|
+
if (!router || this.app.inProduction)
|
|
254
67
|
return;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
catch {
|
|
273
|
-
// pino not directly importable — try finding the symbol on the instance
|
|
274
|
-
}
|
|
275
|
-
if (!streamSym) {
|
|
276
|
-
streamSym = Object.getOwnPropertySymbols(pino).find((s) => s.description === 'pino.stream');
|
|
277
|
-
}
|
|
278
|
-
if (!streamSym)
|
|
279
|
-
return;
|
|
280
|
-
const rawStream = pino[streamSym];
|
|
281
|
-
if (!rawStream || typeof rawStream.write !== 'function')
|
|
68
|
+
this.dashboardDepsAvailable = await checkDashboardDepsHelper(config, this.app);
|
|
69
|
+
const { statsEndpoint, debugEndpoint } = deriveEndpointPaths(config.endpoint, config.devToolbar);
|
|
70
|
+
const dashboardPath = computeDashboardPath(config.devToolbar, this.dashboardDepsAvailable);
|
|
71
|
+
registerAllRoutes({
|
|
72
|
+
router: router,
|
|
73
|
+
getApiController: () => this.apiController,
|
|
74
|
+
getStatsController: () => this.statsController,
|
|
75
|
+
getDebugController: () => this.debugController,
|
|
76
|
+
getDashboardController: () => this.dashboardController,
|
|
77
|
+
statsEndpoint,
|
|
78
|
+
debugEndpoint,
|
|
79
|
+
dashboardPath,
|
|
80
|
+
shouldShow: config.shouldShow,
|
|
81
|
+
});
|
|
82
|
+
const paths = collectRegisteredPaths(statsEndpoint, debugEndpoint, dashboardPath);
|
|
83
|
+
if (paths.length === 0)
|
|
282
84
|
return;
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
stream.write = function (chunk, ...args) {
|
|
286
|
-
try {
|
|
287
|
-
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
288
|
-
const entry = JSON.parse(str);
|
|
289
|
-
if (entry && typeof entry.level === 'number') {
|
|
290
|
-
logStream.ingest(entry);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
// Not valid JSON — ignore (e.g. pino-pretty output)
|
|
295
|
-
}
|
|
296
|
-
return originalWrite(chunk, ...args);
|
|
297
|
-
};
|
|
298
|
-
this.pinoHookActive = true;
|
|
299
|
-
log.info('log collector hooked into AdonisJS logger (zero-config)');
|
|
85
|
+
log.list('routes auto-registered (no manual setup needed):', paths);
|
|
86
|
+
warnAboutAuthMiddleware(config, this.app.makePath.bind(this.app));
|
|
300
87
|
}
|
|
301
88
|
async ready() {
|
|
302
89
|
const config = this.app.config.get('server_stats');
|
|
303
|
-
if (!config)
|
|
304
|
-
return;
|
|
305
|
-
if (this.app.inTest && config.skipInTest !== false)
|
|
90
|
+
if (!config || (this.app.inTest && config.skipInTest !== false))
|
|
306
91
|
return;
|
|
307
|
-
// ── Non-web environments: only start the email bridge publisher ──
|
|
308
92
|
if (this.app.getEnvironment() !== 'web') {
|
|
309
|
-
await this.
|
|
93
|
+
const em = await this.resolve('emitter');
|
|
94
|
+
await setupNonWebBridgeHelper(em, this.emailBridgeChannel);
|
|
310
95
|
return;
|
|
311
96
|
}
|
|
312
|
-
// ── Web environment: full initialization ──
|
|
313
|
-
// Defer to setImmediate so ready() returns immediately. AdonisJS waits
|
|
314
|
-
// for all provider ready() hooks before processing HTTP requests —
|
|
315
|
-
// blocking here would hang the server.
|
|
316
97
|
setImmediate(() => {
|
|
317
|
-
this.
|
|
318
|
-
log.warn(`failed to initialize: ${err?.message ?? err}\n`
|
|
319
|
-
|
|
320
|
-
if (err?.stack) {
|
|
98
|
+
this.initStats(config).catch((err) => {
|
|
99
|
+
log.warn(`failed to initialize: ${err?.message ?? err}\n ${dim('The server will continue without server-stats.')}`);
|
|
100
|
+
if (err?.stack)
|
|
321
101
|
console.error(err.stack);
|
|
322
|
-
}
|
|
323
102
|
});
|
|
324
103
|
});
|
|
325
104
|
}
|
|
326
|
-
async
|
|
105
|
+
async initStats(config) {
|
|
327
106
|
this.resolvedConfig = config;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
collectors = result.collectors;
|
|
333
|
-
log.info(`${bold(String(result.active))} of ${bold(String(result.total))} collectors active`);
|
|
334
|
-
}
|
|
335
|
-
else {
|
|
336
|
-
collectors = config.collectors;
|
|
337
|
-
}
|
|
338
|
-
this.resolvedCollectors = collectors;
|
|
339
|
-
this.engine = new StatsEngine(collectors);
|
|
340
|
-
this.app.container.singleton('server_stats.engine', () => this.engine);
|
|
107
|
+
this.resolvedCollectors = await this.resolveCollectors(config);
|
|
108
|
+
this.engine = new StatsEngine(this.resolvedCollectors);
|
|
109
|
+
const container = this.app.container;
|
|
110
|
+
container.singleton('server_stats.engine', () => this.engine);
|
|
341
111
|
await this.engine.start();
|
|
342
|
-
|
|
343
|
-
await
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
slowQueryThresholdMs: toolbarConfig.slowQueryThresholdMs ?? 100,
|
|
357
|
-
persistDebugData: toolbarConfig.persistDebugData ?? false,
|
|
358
|
-
tracing: toolbarConfig.tracing ?? true,
|
|
359
|
-
maxTraces: toolbarConfig.maxTraces ?? 200,
|
|
360
|
-
dashboard: toolbarConfig.dashboard ?? false,
|
|
361
|
-
dashboardPath: toolbarConfig.dashboardPath ?? '/__stats',
|
|
362
|
-
retentionDays: toolbarConfig.retentionDays ?? 7,
|
|
363
|
-
dbPath: toolbarConfig.dbPath ?? '.adonisjs/server-stats/dashboard.sqlite3',
|
|
364
|
-
debugEndpoint: toolbarConfig.debugEndpoint ?? '/admin/api/debug',
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
catch (err) {
|
|
368
|
-
log.warn(`dev toolbar setup failed: ${err?.message ?? err}\n` +
|
|
369
|
-
` ${dim('Stats bar will still work, but debug panel may be unavailable.')}`);
|
|
370
|
-
}
|
|
371
|
-
// Exclude the stats endpoint and user-specified prefixes from tracing
|
|
372
|
-
// so the debug panel's own polling doesn't flood the timeline
|
|
373
|
-
const debugEndpoint = toolbarConfig.debugEndpoint ?? '/admin/api/debug';
|
|
374
|
-
const defaultExcludes = [debugEndpoint, config.endpoint].filter((p) => typeof p === 'string');
|
|
375
|
-
const prefixes = [...(toolbarConfig.excludeFromTracing ?? defaultExcludes)];
|
|
376
|
-
if (typeof config.endpoint === 'string' && !prefixes.includes(config.endpoint)) {
|
|
377
|
-
prefixes.push(config.endpoint);
|
|
378
|
-
}
|
|
379
|
-
if (prefixes.length > 0) {
|
|
380
|
-
setExcludedPrefixes(prefixes);
|
|
381
|
-
}
|
|
382
|
-
// Create the unified ApiController now that debug store is available.
|
|
383
|
-
// Dashboard store is passed as a getter so it picks up the reference
|
|
384
|
-
// once setupDashboard() completes asynchronously.
|
|
385
|
-
if (this.debugStore) {
|
|
386
|
-
const logPath = this.app.makePath('logs', 'adonisjs.log');
|
|
387
|
-
const { DataAccess: DataAccessClass } = await import('../data/data_access.js');
|
|
388
|
-
const dataAccess = new DataAccessClass(this.debugStore, () => this.dashboardStore, logPath);
|
|
389
|
-
const { ApiController: ApiControllerClass } = await import('../controller/api_controller.js');
|
|
390
|
-
this.apiController = new ApiControllerClass(dataAccess);
|
|
391
|
-
}
|
|
112
|
+
this.pinoHookActive = hookPinoToLogStream(await this.resolve('logger'));
|
|
113
|
+
const SC = (await import('../controller/server_stats_controller.js')).default;
|
|
114
|
+
this.statsController = new SC(this.engine);
|
|
115
|
+
if (config.devToolbar?.enabled && !this.app.inProduction) {
|
|
116
|
+
await this.setupDevToolbar(config);
|
|
117
|
+
}
|
|
118
|
+
const iv = await setupStatsIntervalHelper(this.engine, config, container);
|
|
119
|
+
this.intervalId = iv.intervalId;
|
|
120
|
+
if (iv.transmitAvailable)
|
|
121
|
+
this.transmitAvailable = true;
|
|
122
|
+
if (iv.prometheusActive)
|
|
123
|
+
this.prometheusActive = true;
|
|
124
|
+
if (iv.channelName && !this.transmitChannels.includes(iv.channelName)) {
|
|
125
|
+
this.transmitChannels.push(iv.channelName);
|
|
392
126
|
}
|
|
393
|
-
|
|
394
|
-
this.setupStatsInterval(config);
|
|
395
|
-
// ── Live log streaming via Transmit ──
|
|
396
|
-
this.setupLogStream().catch(() => { });
|
|
127
|
+
await this.setupLogBroadcast();
|
|
397
128
|
log.info('ready');
|
|
398
129
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
// Resolve transmit + prometheus asynchronously but don't block ready()
|
|
408
|
-
const resolveIntegrations = async () => {
|
|
409
|
-
if (config.transport === 'transmit') {
|
|
410
|
-
try {
|
|
411
|
-
transmit = await this.app.container.make('transmit');
|
|
412
|
-
if (transmit) {
|
|
413
|
-
this.transmitAvailable = true;
|
|
414
|
-
if (config.channelName) {
|
|
415
|
-
this.transmitChannels.push(config.channelName);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
catch {
|
|
420
|
-
log.info('transport is "transmit" but @adonisjs/transmit is not installed — falling back to polling');
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
try {
|
|
424
|
-
const mod = await import('../prometheus/prometheus_collector.js');
|
|
425
|
-
prometheusCollector = mod.ServerStatsCollector.instance;
|
|
426
|
-
}
|
|
427
|
-
catch {
|
|
428
|
-
// Prometheus not installed — skip (optional dependency)
|
|
429
|
-
}
|
|
430
|
-
if (prometheusCollector) {
|
|
431
|
-
this.prometheusActive = true;
|
|
432
|
-
log.info('Prometheus integration active');
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
resolveIntegrations().catch(() => { });
|
|
436
|
-
this.intervalId = setInterval(async () => {
|
|
437
|
-
try {
|
|
438
|
-
const stats = await this.engine.collect();
|
|
439
|
-
if (transmit && config.channelName) {
|
|
440
|
-
;
|
|
441
|
-
transmit.broadcast(config.channelName, stats);
|
|
442
|
-
}
|
|
443
|
-
if (prometheusCollector) {
|
|
444
|
-
;
|
|
445
|
-
prometheusCollector.update(stats);
|
|
446
|
-
}
|
|
447
|
-
config.onStats?.(stats);
|
|
448
|
-
}
|
|
449
|
-
catch {
|
|
450
|
-
// Silently ignore collection errors
|
|
451
|
-
}
|
|
452
|
-
}, config.intervalMs);
|
|
453
|
-
}
|
|
454
|
-
async setupDevToolbar(toolbarConfig) {
|
|
455
|
-
this.debugStore = new DebugStore(toolbarConfig);
|
|
456
|
-
this.app.container.singleton('debug.store', () => this.debugStore);
|
|
457
|
-
// Load persisted data before starting collectors
|
|
458
|
-
if (toolbarConfig.persistDebugData) {
|
|
459
|
-
this.persistPath =
|
|
460
|
-
typeof toolbarConfig.persistDebugData === 'string'
|
|
461
|
-
? this.app.makePath(toolbarConfig.persistDebugData)
|
|
462
|
-
: this.app.makePath('.adonisjs', 'server-stats', 'debug-data.json');
|
|
463
|
-
await this.debugStore.loadFromDisk(this.persistPath);
|
|
464
|
-
}
|
|
465
|
-
// Get the emitter
|
|
466
|
-
let emitter = null;
|
|
467
|
-
try {
|
|
468
|
-
emitter = await this.app.container.make('emitter');
|
|
469
|
-
}
|
|
470
|
-
catch {
|
|
471
|
-
log.warn('AdonisJS emitter not available — query and event collection will be disabled');
|
|
472
|
-
}
|
|
473
|
-
// Get the router
|
|
474
|
-
let router = null;
|
|
475
|
-
try {
|
|
476
|
-
router = await this.app.container.make('router');
|
|
477
|
-
}
|
|
478
|
-
catch {
|
|
479
|
-
// Router not available
|
|
480
|
-
}
|
|
481
|
-
await this.debugStore.start(emitter, router);
|
|
482
|
-
// Set up Redis pub/sub bridge for cross-process email capture
|
|
483
|
-
// (e.g., emails sent from Bull queue workers)
|
|
484
|
-
await this.setupEmailBridge(emitter);
|
|
485
|
-
// Create the debug controller (makes the debug routes functional)
|
|
486
|
-
const serverConfig = this.app.config.get('server_stats');
|
|
487
|
-
const DebugControllerClass = (await import('../controller/debug_controller.js')).default;
|
|
488
|
-
this.debugController = new DebugControllerClass(this.debugStore, serverConfig, {
|
|
489
|
-
getEngine: () => this.engine,
|
|
490
|
-
getDashboardStore: () => this.dashboardStore,
|
|
491
|
-
getProviderDiagnostics: () => this.getDiagnostics(),
|
|
492
|
-
getApp: () => this.app,
|
|
493
|
-
});
|
|
494
|
-
// Wire trace collector into the request tracking middleware
|
|
495
|
-
if (this.debugStore.traces) {
|
|
496
|
-
setTraceCollector(this.debugStore.traces);
|
|
497
|
-
}
|
|
498
|
-
// Periodic flush every 30 seconds (handles crashes)
|
|
499
|
-
if (this.persistPath) {
|
|
500
|
-
this.flushTimer = setInterval(async () => {
|
|
501
|
-
try {
|
|
502
|
-
await this.debugStore?.saveToDisk(this.persistPath);
|
|
503
|
-
}
|
|
504
|
-
catch {
|
|
505
|
-
// Silently ignore flush errors
|
|
506
|
-
}
|
|
507
|
-
}, 30_000);
|
|
508
|
-
}
|
|
509
|
-
// ── Transmit broadcasting for debug panel live updates ────────
|
|
510
|
-
let debugTransmit = null;
|
|
511
|
-
try {
|
|
512
|
-
debugTransmit = await this.app.container.make('transmit');
|
|
513
|
-
}
|
|
514
|
-
catch {
|
|
515
|
-
// Transmit not installed — debug panel will use polling
|
|
516
|
-
}
|
|
517
|
-
if (debugTransmit) {
|
|
518
|
-
this.transmitAvailable = true;
|
|
519
|
-
const debugChannel = 'server-stats/debug';
|
|
520
|
-
if (!this.transmitChannels.includes(debugChannel)) {
|
|
521
|
-
this.transmitChannels.push(debugChannel);
|
|
522
|
-
}
|
|
523
|
-
const pendingTypes = new Set();
|
|
524
|
-
this.debugStore.onNewItem((type) => {
|
|
525
|
-
// Debounce: coalesce rapid events into a single broadcast
|
|
526
|
-
pendingTypes.add(type);
|
|
527
|
-
if (this.debugBroadcastTimer)
|
|
528
|
-
return;
|
|
529
|
-
this.debugBroadcastTimer = setTimeout(() => {
|
|
530
|
-
this.debugBroadcastTimer = null;
|
|
531
|
-
const types = Array.from(pendingTypes);
|
|
532
|
-
pendingTypes.clear();
|
|
533
|
-
try {
|
|
534
|
-
;
|
|
535
|
-
debugTransmit.broadcast(debugChannel, { types });
|
|
536
|
-
}
|
|
537
|
-
catch {
|
|
538
|
-
// Silently ignore broadcast errors
|
|
539
|
-
}
|
|
540
|
-
}, 200);
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
// Full-page dashboard setup — deferred with setImmediate so it runs
|
|
544
|
-
// AFTER the current event-loop cycle completes. This guarantees
|
|
545
|
-
// ready() returns and AdonisJS can process HTTP requests while the
|
|
546
|
-
// SQLite store initializes in the background.
|
|
547
|
-
if (toolbarConfig.dashboard && this.dashboardDepsAvailable) {
|
|
548
|
-
setImmediate(() => {
|
|
549
|
-
this.setupDashboard(toolbarConfig, emitter).catch((err) => {
|
|
550
|
-
log.warn(`dashboard setup failed: ${err?.message ?? err}\n` +
|
|
551
|
-
` ${dim('Everything else continues to work.')}`);
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
}
|
|
130
|
+
async resolveCollectors(config) {
|
|
131
|
+
if (config.collectors && config.collectors !== 'auto') {
|
|
132
|
+
return config.collectors;
|
|
133
|
+
}
|
|
134
|
+
const { autoDetectCollectors } = await import('../collectors/auto_detect.js');
|
|
135
|
+
const r = await autoDetectCollectors();
|
|
136
|
+
log.info(`${r.active} of ${r.total} collectors active`);
|
|
137
|
+
return r.collectors;
|
|
555
138
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
// Dynamically import DashboardStore so knex/better-sqlite3 are truly optional
|
|
566
|
-
const { DashboardStore: DashboardStoreClass } = await import('../dashboard/dashboard_store.js');
|
|
567
|
-
this.dashboardStore = new DashboardStoreClass(toolbarConfig);
|
|
568
|
-
const appRoot = this.app.makePath('');
|
|
569
|
-
try {
|
|
570
|
-
// Timeout safety net: if SQLite init hangs (e.g. wrong native binary
|
|
571
|
-
// loaded via symlink), abort after 15s instead of freezing forever.
|
|
572
|
-
const TIMEOUT_MS = 15_000;
|
|
573
|
-
const startPromise = this.dashboardStore.start(null, emitter, appRoot);
|
|
574
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
575
|
-
setTimeout(() => reject(new Error(`Dashboard SQLite initialization timed out after ${TIMEOUT_MS / 1000}s`)), TIMEOUT_MS);
|
|
139
|
+
async setupDevToolbar(config) {
|
|
140
|
+
const tc = resolveToolbarConfig({ enabled: true, ...config.devToolbar });
|
|
141
|
+
try {
|
|
142
|
+
const result = await setupDevToolbarCore({
|
|
143
|
+
tc,
|
|
144
|
+
config,
|
|
145
|
+
app: this.app,
|
|
146
|
+
resolve: (b) => this.resolve(b),
|
|
147
|
+
getDiagnostics: () => this.getDiagnostics(),
|
|
576
148
|
});
|
|
577
|
-
|
|
578
|
-
|
|
149
|
+
if (result)
|
|
150
|
+
applyToolbarResult(result, tc, this);
|
|
579
151
|
}
|
|
580
152
|
catch (err) {
|
|
581
|
-
|
|
582
|
-
const code = err?.code || '';
|
|
583
|
-
const isMissingDep = msg.includes('better-sqlite3') ||
|
|
584
|
-
msg.includes('knex') ||
|
|
585
|
-
msg.includes('Cannot find module') ||
|
|
586
|
-
msg.includes('Cannot find package') ||
|
|
587
|
-
code === 'ERR_MODULE_NOT_FOUND' ||
|
|
588
|
-
code === 'MODULE_NOT_FOUND';
|
|
589
|
-
const isTimeout = msg.includes('timed out');
|
|
590
|
-
if (isMissingDep) {
|
|
591
|
-
log.block('Dashboard could not start — missing dependencies. Install with:', [
|
|
592
|
-
'',
|
|
593
|
-
bold('npm install knex better-sqlite3'),
|
|
594
|
-
'',
|
|
595
|
-
dim('Dashboard has been disabled for this session.'),
|
|
596
|
-
dim('Everything else (stats bar, debug panel) works without it.'),
|
|
597
|
-
]);
|
|
598
|
-
}
|
|
599
|
-
else if (isTimeout) {
|
|
600
|
-
log.block('Dashboard initialization timed out', [
|
|
601
|
-
dim('SQLite setup took too long — this usually means a wrong native'),
|
|
602
|
-
dim('binary was loaded (common with symlinked/file: dependencies).'),
|
|
603
|
-
'',
|
|
604
|
-
dim('Try running:'),
|
|
605
|
-
` ${bold('npm install knex better-sqlite3')}`,
|
|
606
|
-
dim('in your app directory to ensure the correct copies are used.'),
|
|
607
|
-
'',
|
|
608
|
-
dim('Dashboard has been disabled for this session.'),
|
|
609
|
-
dim('Everything else (stats bar, debug panel) works without it.'),
|
|
610
|
-
]);
|
|
611
|
-
}
|
|
612
|
-
else {
|
|
613
|
-
log.warn(`Dashboard could not start: ${msg}\n` +
|
|
614
|
-
` ${dim('Dashboard has been disabled for this session.')}`);
|
|
615
|
-
if (err?.stack) {
|
|
616
|
-
console.error(err.stack);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
this.dashboardStore = null;
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
log.info('dashboard: binding to container...');
|
|
623
|
-
this.app.container.singleton('dashboard.store', () => this.dashboardStore);
|
|
624
|
-
// Set dashboard path in middleware for self-exclusion
|
|
625
|
-
setDashboardPath(toolbarConfig.dashboardPath);
|
|
626
|
-
// Create the controller — this makes the routes registered in boot() functional
|
|
627
|
-
log.info('dashboard: creating controller...');
|
|
628
|
-
const DashboardControllerClass = (await import('../dashboard/dashboard_controller.js')).default;
|
|
629
|
-
this.dashboardController = new DashboardControllerClass(this.dashboardStore, this.app);
|
|
630
|
-
// ── Log piping ────────────────────────────────────────────────
|
|
631
|
-
// If the Pino stream hook is active, piggyback on it for real-time
|
|
632
|
-
// log persistence. Otherwise fall back to polling the log file.
|
|
633
|
-
log.info('dashboard: setting up log piping...');
|
|
634
|
-
const existingLogStream = getLogStreamService();
|
|
635
|
-
if (this.pinoHookActive && existingLogStream && !existingLogStream['logPath']) {
|
|
636
|
-
// Stream mode — add a listener for dashboard persistence
|
|
637
|
-
const origOnEntry = existingLogStream['onEntry'];
|
|
638
|
-
existingLogStream['onEntry'] = (entry) => {
|
|
639
|
-
origOnEntry?.(entry);
|
|
640
|
-
this.dashboardStore?.recordLog(entry);
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
else {
|
|
644
|
-
// File-based fallback
|
|
645
|
-
const logPath = this.app.makePath('logs', 'adonisjs.log');
|
|
646
|
-
this.dashboardLogStream = new LogStreamService(logPath, (entry) => {
|
|
647
|
-
this.dashboardStore?.recordLog(entry);
|
|
648
|
-
});
|
|
649
|
-
await this.dashboardLogStream.start();
|
|
650
|
-
}
|
|
651
|
-
// ── Per-request data piping ────────────────────────────────────
|
|
652
|
-
const debugStore = this.debugStore;
|
|
653
|
-
const dashStore = this.dashboardStore;
|
|
654
|
-
let lastQueryId = 0;
|
|
655
|
-
setOnRequestComplete(({ method, url, statusCode, duration, trace, httpRequestId }) => {
|
|
656
|
-
if (!dashStore.isReady())
|
|
657
|
-
return;
|
|
658
|
-
// O(K) collection of new queries since last seen ID — avoids
|
|
659
|
-
// copying the entire 500-item ring buffer on every request.
|
|
660
|
-
const newQueries = debugStore.queries.getQueriesSince(lastQueryId);
|
|
661
|
-
if (newQueries.length > 0) {
|
|
662
|
-
lastQueryId = newQueries[newQueries.length - 1].id;
|
|
663
|
-
}
|
|
664
|
-
// Queue for batch persistence (flushed every 500ms)
|
|
665
|
-
dashStore.persistRequest({
|
|
666
|
-
method,
|
|
667
|
-
url,
|
|
668
|
-
statusCode,
|
|
669
|
-
duration,
|
|
670
|
-
queries: newQueries,
|
|
671
|
-
trace: trace ?? null,
|
|
672
|
-
httpRequestId: httpRequestId ?? null,
|
|
673
|
-
});
|
|
674
|
-
});
|
|
675
|
-
// ── Transmit streaming for real-time dashboard updates ────────
|
|
676
|
-
let transmit = null;
|
|
677
|
-
try {
|
|
678
|
-
transmit = await this.app.container.make('transmit');
|
|
679
|
-
}
|
|
680
|
-
catch {
|
|
681
|
-
// Transmit not installed — skip real-time updates
|
|
153
|
+
log.warn(`dev toolbar setup failed: ${err?.message ?? err}\n ${dim('Stats bar will still work.')}`);
|
|
682
154
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
this.transmitChannels.push(dashChannel);
|
|
688
|
-
}
|
|
689
|
-
// Broadcast overview metrics every 30s (not 5s) to reduce SQLite
|
|
690
|
-
// pool pressure. Each broadcast runs 5+ sequential queries on the
|
|
691
|
-
// single-connection pool, blocking all dashboard API reads.
|
|
692
|
-
this.dashboardBroadcastTimer = setInterval(async () => {
|
|
693
|
-
try {
|
|
694
|
-
if (!dashStore.isReady())
|
|
695
|
-
return;
|
|
696
|
-
const overview = await dashStore.getOverviewMetrics('1h');
|
|
697
|
-
const diagnostics = {
|
|
698
|
-
collectors: this.engine.getCollectorHealth(),
|
|
699
|
-
buffers: this.debugStore.getBufferStats(),
|
|
700
|
-
};
|
|
701
|
-
transmit.broadcast(dashChannel, {
|
|
702
|
-
...overview,
|
|
703
|
-
diagnostics,
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
|
-
catch {
|
|
707
|
-
// Silently ignore
|
|
708
|
-
}
|
|
709
|
-
}, 30_000);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Lightweight email bridge publisher for non-web environments
|
|
714
|
-
* (queue workers, scheduler). Listens to local AdonisJS mail events
|
|
715
|
-
* and publishes them to Redis so the web server can ingest them.
|
|
716
|
-
*/
|
|
717
|
-
async setupEmailBridgePublisher() {
|
|
718
|
-
let emitter;
|
|
719
|
-
try {
|
|
720
|
-
emitter = (await this.app.container.make('emitter'));
|
|
721
|
-
}
|
|
722
|
-
catch {
|
|
155
|
+
const pfx = buildExcludedPrefixes(tc, config.endpoint);
|
|
156
|
+
if (pfx.length > 0)
|
|
157
|
+
setExcludedPrefixes(pfx);
|
|
158
|
+
if (!this.debugStore)
|
|
723
159
|
return;
|
|
724
|
-
}
|
|
725
|
-
const {
|
|
726
|
-
|
|
727
|
-
try {
|
|
728
|
-
const mod = await appImport('@adonisjs/redis/services/main');
|
|
729
|
-
redis = mod.default;
|
|
730
|
-
}
|
|
731
|
-
catch {
|
|
732
|
-
return; // @adonisjs/redis not installed
|
|
733
|
-
}
|
|
734
|
-
const tag = `${process.pid}-${Date.now()}`;
|
|
735
|
-
const ch = this.emailBridgeChannel;
|
|
736
|
-
const statusMap = [
|
|
737
|
-
['mail:sending', 'sending'],
|
|
738
|
-
['mail:sent', 'sent'],
|
|
739
|
-
['mail:queueing', 'queueing'],
|
|
740
|
-
['mail:queued', 'queued'],
|
|
741
|
-
['queued:mail:error', 'failed'],
|
|
742
|
-
];
|
|
743
|
-
const MAX_HTML = 50_000; // 50 KB cap per email body, same as EmailCollector
|
|
744
|
-
const capSize = (v) => {
|
|
745
|
-
if (!v || typeof v !== 'string')
|
|
746
|
-
return null;
|
|
747
|
-
return v.length <= MAX_HTML ? v : v.slice(0, MAX_HTML) + '\n<!-- truncated -->';
|
|
748
|
-
};
|
|
749
|
-
for (const [event, status] of statusMap) {
|
|
750
|
-
emitter.on(event, (data) => {
|
|
751
|
-
try {
|
|
752
|
-
const d = data;
|
|
753
|
-
const msg = (d?.message || d);
|
|
754
|
-
const payload = JSON.stringify({
|
|
755
|
-
_t: tag,
|
|
756
|
-
from: extractAddresses(msg?.from) || 'unknown',
|
|
757
|
-
to: extractAddresses(msg?.to) || 'unknown',
|
|
758
|
-
cc: extractAddresses(msg?.cc) || null,
|
|
759
|
-
bcc: extractAddresses(msg?.bcc) || null,
|
|
760
|
-
subject: msg?.subject || '(no subject)',
|
|
761
|
-
html: capSize(msg?.html),
|
|
762
|
-
text: capSize(msg?.text),
|
|
763
|
-
mailer: d?.mailerName || d?.mailer || 'unknown',
|
|
764
|
-
status,
|
|
765
|
-
messageId: d?.response?.messageId ||
|
|
766
|
-
d?.messageId ||
|
|
767
|
-
null,
|
|
768
|
-
attachmentCount: Array.isArray(msg?.attachments) ? msg.attachments.length : 0,
|
|
769
|
-
timestamp: Date.now(),
|
|
770
|
-
});
|
|
771
|
-
redis.publish(ch, payload).catch(() => { });
|
|
772
|
-
}
|
|
773
|
-
catch {
|
|
774
|
-
// Silently ignore serialization errors
|
|
775
|
-
}
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
log.info('email bridge publisher active (queue worker → Redis)');
|
|
160
|
+
const { DataAccess: DA } = await import('../data/data_access.js');
|
|
161
|
+
const { ApiController: AC } = await import('../controller/api_controller.js');
|
|
162
|
+
this.apiController = new AC(new DA(this.debugStore, () => this.dashboardStore, this.app.makePath('logs', 'adonisjs.log')));
|
|
779
163
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
*/
|
|
784
|
-
async setupLogStream() {
|
|
785
|
-
let transmit;
|
|
786
|
-
try {
|
|
787
|
-
transmit = (await this.app.container.make('transmit'));
|
|
788
|
-
}
|
|
789
|
-
catch {
|
|
790
|
-
return; // @adonisjs/transmit not available
|
|
791
|
-
}
|
|
792
|
-
const channelName = this.app.config.get('server_stats.logChannelName', 'admin/logs');
|
|
793
|
-
const broadcast = (entry) => {
|
|
794
|
-
try {
|
|
795
|
-
transmit.broadcast(channelName, entry);
|
|
796
|
-
}
|
|
797
|
-
catch {
|
|
798
|
-
// Silently ignore broadcast errors
|
|
799
|
-
}
|
|
800
|
-
};
|
|
801
|
-
// If the Pino stream hook is active, piggyback on its onEntry chain.
|
|
802
|
-
const existing = getLogStreamService();
|
|
803
|
-
if (this.pinoHookActive && existing) {
|
|
804
|
-
const internal = existing;
|
|
805
|
-
const origOnEntry = internal.onEntry;
|
|
806
|
-
internal.onEntry = (entry) => {
|
|
807
|
-
origOnEntry?.(entry);
|
|
808
|
-
broadcast(entry);
|
|
809
|
-
};
|
|
164
|
+
async setupLogBroadcast() {
|
|
165
|
+
const t = (await this.resolve('transmit'));
|
|
166
|
+
if (!t)
|
|
810
167
|
return;
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const logPath = this.app.makePath('logs', 'adonisjs.log');
|
|
814
|
-
this.logStreamService = new LogStreamService(logPath, broadcast);
|
|
815
|
-
await this.logStreamService.start();
|
|
168
|
+
const ch = this.app.config.get('server_stats.logChannelName', 'admin/logs');
|
|
169
|
+
this.logStreamService = setupLogStreamBroadcast(t, ch, this.pinoHookActive, this.app.makePath.bind(this.app));
|
|
816
170
|
}
|
|
817
|
-
|
|
818
|
-
* Set up a Redis pub/sub bridge for cross-process email capture.
|
|
819
|
-
*
|
|
820
|
-
* Mail events (`mail:sending`, `mail:sent`, etc.) are process-local.
|
|
821
|
-
* When emails are sent from a Bull queue worker, the web server's
|
|
822
|
-
* {@link EmailCollector} never sees them. This bridge solves that:
|
|
823
|
-
*
|
|
824
|
-
* 1. **Every process** publishes mail events to a Redis channel.
|
|
825
|
-
* 2. **Every process** subscribes and ingests events from *other*
|
|
826
|
-
* processes (identified by a unique process tag).
|
|
827
|
-
*
|
|
828
|
-
* Requires `@adonisjs/redis`. Silently skipped if not installed.
|
|
829
|
-
*/
|
|
830
|
-
async setupEmailBridge(emitter) {
|
|
831
|
-
if (!emitter)
|
|
832
|
-
return;
|
|
833
|
-
const { appImport } = await import('../utils/app_import.js');
|
|
834
|
-
let redis;
|
|
171
|
+
async resolve(binding) {
|
|
835
172
|
try {
|
|
836
|
-
|
|
837
|
-
redis = mod.default;
|
|
173
|
+
return await this.app.container.make(binding);
|
|
838
174
|
}
|
|
839
175
|
catch {
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
const CHANNEL = this.emailBridgeChannel;
|
|
843
|
-
const processTag = `${process.pid}-${Date.now()}`;
|
|
844
|
-
const em = emitter;
|
|
845
|
-
// ── Publish local mail events to Redis ──────────────────────
|
|
846
|
-
const statusMap = [
|
|
847
|
-
['mail:sending', 'sending'],
|
|
848
|
-
['mail:sent', 'sent'],
|
|
849
|
-
['mail:queueing', 'queueing'],
|
|
850
|
-
['mail:queued', 'queued'],
|
|
851
|
-
['queued:mail:error', 'failed'],
|
|
852
|
-
];
|
|
853
|
-
const MAX_HTML = 50_000;
|
|
854
|
-
const capSize = (v) => {
|
|
855
|
-
if (!v || typeof v !== 'string')
|
|
856
|
-
return null;
|
|
857
|
-
return v.length <= MAX_HTML ? v : v.slice(0, MAX_HTML) + '\n<!-- truncated -->';
|
|
858
|
-
};
|
|
859
|
-
for (const [event, status] of statusMap) {
|
|
860
|
-
em.on(event, (data) => {
|
|
861
|
-
try {
|
|
862
|
-
const d = data;
|
|
863
|
-
const msg = d?.message || d;
|
|
864
|
-
const payload = JSON.stringify({
|
|
865
|
-
_t: processTag,
|
|
866
|
-
from: extractAddresses(msg?.from) || 'unknown',
|
|
867
|
-
to: extractAddresses(msg?.to) || 'unknown',
|
|
868
|
-
cc: extractAddresses(msg?.cc) || null,
|
|
869
|
-
bcc: extractAddresses(msg?.bcc) || null,
|
|
870
|
-
subject: msg?.subject || '(no subject)',
|
|
871
|
-
html: capSize(msg?.html),
|
|
872
|
-
text: capSize(msg?.text),
|
|
873
|
-
mailer: d?.mailerName || d?.mailer || 'unknown',
|
|
874
|
-
status,
|
|
875
|
-
messageId: d?.response?.messageId || d?.messageId || null,
|
|
876
|
-
attachmentCount: Array.isArray(msg?.attachments) ? msg.attachments.length : 0,
|
|
877
|
-
timestamp: Date.now(),
|
|
878
|
-
});
|
|
879
|
-
redis.publish(CHANNEL, payload).catch(() => { });
|
|
880
|
-
}
|
|
881
|
-
catch {
|
|
882
|
-
// Silently ignore serialization errors
|
|
883
|
-
}
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
// ── Subscribe to receive emails from other processes ─────────
|
|
887
|
-
try {
|
|
888
|
-
await redis.subscribe(CHANNEL, (message) => {
|
|
889
|
-
try {
|
|
890
|
-
const parsed = JSON.parse(message);
|
|
891
|
-
if (parsed._t === processTag)
|
|
892
|
-
return; // Skip own messages
|
|
893
|
-
const { _t: _, ...fields } = parsed;
|
|
894
|
-
const record = {
|
|
895
|
-
...fields,
|
|
896
|
-
html: fields.html || null,
|
|
897
|
-
text: fields.text || null,
|
|
898
|
-
};
|
|
899
|
-
// Ingest into in-memory ring buffer (debug panel)
|
|
900
|
-
this.debugStore?.emails.ingest(record);
|
|
901
|
-
// Also persist to SQLite dashboard (the sending process has
|
|
902
|
-
// no DashboardStore, so we must record it here)
|
|
903
|
-
this.dashboardStore?.recordEmail({ id: 0, ...record });
|
|
904
|
-
}
|
|
905
|
-
catch {
|
|
906
|
-
// Ignore malformed messages
|
|
907
|
-
}
|
|
908
|
-
});
|
|
909
|
-
this.emailBridgeRedis = redis;
|
|
910
|
-
log.info('email bridge active (cross-process capture via Redis)');
|
|
911
|
-
}
|
|
912
|
-
catch {
|
|
913
|
-
// Subscribe failed — bridge unavailable, local capture still works
|
|
176
|
+
return null;
|
|
914
177
|
}
|
|
915
178
|
}
|
|
916
|
-
/** Return diagnostics state for the Internals endpoint. */
|
|
917
179
|
getDiagnostics() {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
persistFlush: {
|
|
935
|
-
active: this.flushTimer !== null,
|
|
936
|
-
intervalMs: 30_000,
|
|
937
|
-
},
|
|
938
|
-
retentionCleanup: {
|
|
939
|
-
active: this.dashboardStore?.isReady() ?? false,
|
|
940
|
-
intervalMs: 60 * 60 * 1000,
|
|
941
|
-
},
|
|
942
|
-
},
|
|
943
|
-
transmit: {
|
|
944
|
-
available: this.transmitAvailable,
|
|
945
|
-
channels: this.transmitChannels,
|
|
946
|
-
},
|
|
947
|
-
integrations: {
|
|
948
|
-
prometheus: { active: this.prometheusActive },
|
|
949
|
-
pinoHook: {
|
|
950
|
-
active: this.pinoHookActive,
|
|
951
|
-
mode: this.pinoHookActive ? 'stream' : toolbarConfig?.enabled ? 'none' : 'none',
|
|
952
|
-
},
|
|
953
|
-
edgePlugin: { active: this.edgePluginActive },
|
|
954
|
-
emailBridge: { active: this.emailBridgeRedis !== null },
|
|
955
|
-
cacheInspector: {
|
|
956
|
-
available: this.resolvedCollectors.some((c) => c.name === 'redis'),
|
|
957
|
-
},
|
|
958
|
-
queueInspector: {
|
|
959
|
-
available: this.resolvedCollectors.some((c) => c.name === 'queue'),
|
|
960
|
-
},
|
|
961
|
-
},
|
|
962
|
-
config: {
|
|
963
|
-
intervalMs: config?.intervalMs ?? 0,
|
|
964
|
-
transport: config?.transport ?? 'none',
|
|
965
|
-
channelName: config?.channelName ?? '',
|
|
966
|
-
endpoint: config?.endpoint ?? false,
|
|
967
|
-
skipInTest: config?.skipInTest !== false,
|
|
968
|
-
hasOnStatsCallback: typeof config?.onStats === 'function',
|
|
969
|
-
hasShouldShowCallback: typeof config?.shouldShow === 'function',
|
|
970
|
-
},
|
|
971
|
-
devToolbar: {
|
|
972
|
-
enabled: !!toolbarConfig?.enabled,
|
|
973
|
-
maxQueries: toolbarConfig?.maxQueries ?? 500,
|
|
974
|
-
maxEvents: toolbarConfig?.maxEvents ?? 200,
|
|
975
|
-
maxEmails: toolbarConfig?.maxEmails ?? 100,
|
|
976
|
-
maxTraces: toolbarConfig?.maxTraces ?? 200,
|
|
977
|
-
slowQueryThresholdMs: toolbarConfig?.slowQueryThresholdMs ?? 100,
|
|
978
|
-
tracing: toolbarConfig?.tracing ?? true,
|
|
979
|
-
dashboard: toolbarConfig?.dashboard ?? false,
|
|
980
|
-
dashboardPath: toolbarConfig?.dashboardPath ?? '/__stats',
|
|
981
|
-
debugEndpoint: toolbarConfig?.debugEndpoint ?? '/admin/api/debug',
|
|
982
|
-
retentionDays: toolbarConfig?.retentionDays ?? 7,
|
|
983
|
-
dbPath: toolbarConfig?.dbPath ?? '.adonisjs/server-stats/dashboard.sqlite3',
|
|
984
|
-
persistDebugData: toolbarConfig?.persistDebugData ?? false,
|
|
985
|
-
renderer: toolbarConfig?.renderer ?? 'preact',
|
|
986
|
-
excludeFromTracing: toolbarConfig?.excludeFromTracing ?? [],
|
|
987
|
-
customPaneCount: toolbarConfig?.panes?.length ?? 0,
|
|
988
|
-
},
|
|
989
|
-
};
|
|
180
|
+
return buildDiagnostics({
|
|
181
|
+
intervalId: this.intervalId,
|
|
182
|
+
dashboardBroadcastTimer: this.dashboardBroadcastTimer,
|
|
183
|
+
debugBroadcastTimer: this.debugBroadcastTimer,
|
|
184
|
+
flushTimer: this.flushTimer,
|
|
185
|
+
dashboardStoreReady: this.dashboardStore?.isReady() ?? false,
|
|
186
|
+
transmitAvailable: this.transmitAvailable,
|
|
187
|
+
transmitChannels: this.transmitChannels,
|
|
188
|
+
prometheusActive: this.prometheusActive,
|
|
189
|
+
pinoHookActive: this.pinoHookActive,
|
|
190
|
+
edgePluginActive: this.edgePluginActive,
|
|
191
|
+
emailBridgeActive: this.emailBridgeRedis !== null,
|
|
192
|
+
hasCacheCollector: this.resolvedCollectors.some((c) => c.name === 'redis'),
|
|
193
|
+
hasQueueCollector: this.resolvedCollectors.some((c) => c.name === 'queue'),
|
|
194
|
+
config: this.resolvedConfig,
|
|
195
|
+
});
|
|
990
196
|
}
|
|
991
197
|
async shutdown() {
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
this.
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
this.
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
}
|
|
1013
|
-
catch (err) {
|
|
1014
|
-
log.warn('could not save debug data on shutdown — ' + err?.message);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
// Unsubscribe Redis email bridge
|
|
1018
|
-
if (this.emailBridgeRedis) {
|
|
1019
|
-
try {
|
|
1020
|
-
;
|
|
1021
|
-
this.emailBridgeRedis.unsubscribe(this.emailBridgeChannel);
|
|
1022
|
-
}
|
|
1023
|
-
catch {
|
|
1024
|
-
// Ignore cleanup errors
|
|
1025
|
-
}
|
|
1026
|
-
this.emailBridgeRedis = null;
|
|
1027
|
-
}
|
|
1028
|
-
// Clean up log stream and dashboard resources
|
|
1029
|
-
this.logStreamService?.stop();
|
|
1030
|
-
this.dashboardLogStream?.stop();
|
|
1031
|
-
setOnRequestComplete(null);
|
|
1032
|
-
setDashboardPath(null);
|
|
1033
|
-
setExcludedPrefixes([]);
|
|
1034
|
-
await this.dashboardStore?.stop();
|
|
1035
|
-
this.debugStore?.stop();
|
|
1036
|
-
await this.engine?.stop();
|
|
198
|
+
clearAllTimers({
|
|
199
|
+
intervalId: this.intervalId,
|
|
200
|
+
flushTimer: this.flushTimer,
|
|
201
|
+
dashboardBroadcastTimer: this.dashboardBroadcastTimer,
|
|
202
|
+
debugBroadcastTimer: this.debugBroadcastTimer,
|
|
203
|
+
});
|
|
204
|
+
this.intervalId = null;
|
|
205
|
+
this.flushTimer = null;
|
|
206
|
+
this.dashboardBroadcastTimer = null;
|
|
207
|
+
this.debugBroadcastTimer = null;
|
|
208
|
+
await persistDebugData(this.debugStore, this.persistPath);
|
|
209
|
+
unsubscribeEmailBridge(this.emailBridgeRedis, this.emailBridgeChannel);
|
|
210
|
+
this.emailBridgeRedis = null;
|
|
211
|
+
await cleanupResources({
|
|
212
|
+
logStreamService: this.logStreamService,
|
|
213
|
+
dashboardLogStream: this.dashboardLogStream,
|
|
214
|
+
dashboardStore: this.dashboardStore,
|
|
215
|
+
debugStore: this.debugStore,
|
|
216
|
+
engine: this.engine,
|
|
217
|
+
});
|
|
1037
218
|
}
|
|
1038
219
|
}
|