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.
Files changed (227) hide show
  1. package/README.md +23 -14
  2. package/dist/core/config-utils.d.ts +8 -0
  3. package/dist/core/constants.d.ts +4 -0
  4. package/dist/core/dashboard-data-controller.d.ts +16 -0
  5. package/dist/core/dashboard-data-helpers.d.ts +12 -0
  6. package/dist/core/debug-data-controller.d.ts +4 -0
  7. package/dist/core/define-config-helpers.d.ts +25 -0
  8. package/dist/core/feature-detect-helpers.d.ts +36 -0
  9. package/dist/core/formatters-helpers.d.ts +23 -0
  10. package/dist/core/index.js +596 -509
  11. package/dist/core/log-utils-helpers.d.ts +13 -0
  12. package/dist/core/metrics.d.ts +3 -28
  13. package/dist/core/pagination.d.ts +0 -9
  14. package/dist/core/server-stats-controller.d.ts +6 -0
  15. package/dist/core/split-pane.d.ts +18 -0
  16. package/dist/core/trace-utils.d.ts +5 -0
  17. package/dist/core/transmit-helpers.d.ts +7 -0
  18. package/dist/core/types-dashboard.d.ts +178 -0
  19. package/dist/core/types-diagnostics.d.ts +85 -0
  20. package/dist/core/types.d.ts +11 -443
  21. package/dist/react/{CacheSection-xH75hwXu.js → CacheSection-baMZotSn.js} +2 -2
  22. package/dist/react/CacheTab-2cw_rMzj.js +117 -0
  23. package/dist/react/{ConfigSection-D8BO1Ry9.js → ConfigSection-DGgqjAal.js} +1 -1
  24. package/dist/react/{ConfigTab-CcN-tfjv.js → ConfigTab-H3OnYqmK.js} +1 -1
  25. package/dist/react/CustomPaneTab-B6r7ha0u.js +98 -0
  26. package/dist/react/{EmailsSection-BzlsTdPs.js → EmailsSection-C-UZISG-.js} +2 -2
  27. package/dist/react/EmailsTab-DbK4Eobn.js +139 -0
  28. package/dist/react/{EventsSection-CGQWiIdV.js → EventsSection-C7RQW_LY.js} +2 -2
  29. package/dist/react/EventsTab-CfVr7AiM.js +57 -0
  30. package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
  31. package/dist/react/{JobsSection-D7AHQmZi.js → JobsSection-CQHNK_Ls.js} +2 -2
  32. package/dist/react/{JobsTab-B3Lfdqed.js → JobsTab-znzf6jzk.js} +54 -42
  33. package/dist/react/{LogsSection-Cly1dpvS.js → LogsSection-Dmm3rE2B.js} +9 -3
  34. package/dist/react/LogsTab-D8unMV5P.js +108 -0
  35. package/dist/react/{OverviewSection-CkBGFEWq.js → OverviewSection-ABP9ueBo.js} +1 -1
  36. package/dist/react/{QueriesSection-CfCpnNUD.js → QueriesSection-CnmSkznA.js} +2 -2
  37. package/dist/react/{QueriesTab-DbBmAqzO.js → QueriesTab-BQzcxEiW.js} +37 -40
  38. package/dist/react/RelatedLogs-3A8RuGKH.js +52 -0
  39. package/dist/react/RequestsSection-kW79_M7k.js +341 -0
  40. package/dist/react/{RoutesSection-CRqF-cNM.js → RoutesSection-BRhxrtjZ.js} +2 -2
  41. package/dist/react/RoutesTab-CpYH5lUw.js +68 -0
  42. package/dist/react/TimelineTab-DjLR35Ce.js +214 -0
  43. package/dist/react/index-CsImORX6.js +1121 -0
  44. package/dist/react/index.js +1 -1
  45. package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
  46. package/dist/react/react/components/shared/RelatedLogs.d.ts +7 -0
  47. package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
  48. package/dist/react/style.css +1 -1
  49. package/dist/src/collectors/app_collector.d.ts +0 -8
  50. package/dist/src/collectors/app_collector.js +45 -52
  51. package/dist/src/collectors/auto_detect.d.ts +0 -23
  52. package/dist/src/collectors/auto_detect.js +33 -55
  53. package/dist/src/collectors/db_pool_collector.d.ts +14 -16
  54. package/dist/src/collectors/db_pool_collector.js +72 -57
  55. package/dist/src/collectors/log_collector.d.ts +0 -47
  56. package/dist/src/collectors/log_collector.js +36 -65
  57. package/dist/src/collectors/queue_collector.d.ts +0 -20
  58. package/dist/src/collectors/queue_collector.js +60 -76
  59. package/dist/src/collectors/redis_collector.d.ts +10 -10
  60. package/dist/src/collectors/redis_collector.js +69 -66
  61. package/dist/src/config/deprecation_migration.d.ts +7 -0
  62. package/dist/src/config/deprecation_migration.js +201 -0
  63. package/dist/src/controller/debug_controller.d.ts +1 -1
  64. package/dist/src/controller/debug_controller.js +87 -81
  65. package/dist/src/dashboard/cache_handlers.d.ts +14 -0
  66. package/dist/src/dashboard/cache_handlers.js +52 -0
  67. package/dist/src/dashboard/chart_aggregator.d.ts +0 -7
  68. package/dist/src/dashboard/chart_aggregator.js +68 -50
  69. package/dist/src/dashboard/coalesce_cache.d.ts +25 -0
  70. package/dist/src/dashboard/coalesce_cache.js +47 -0
  71. package/dist/src/dashboard/dashboard_controller.d.ts +11 -37
  72. package/dist/src/dashboard/dashboard_controller.js +52 -532
  73. package/dist/src/dashboard/dashboard_page_assets.d.ts +17 -0
  74. package/dist/src/dashboard/dashboard_page_assets.js +51 -0
  75. package/dist/src/dashboard/dashboard_store.d.ts +19 -217
  76. package/dist/src/dashboard/dashboard_store.js +115 -1069
  77. package/dist/src/dashboard/dashboard_types.d.ts +83 -0
  78. package/dist/src/dashboard/dashboard_types.js +4 -0
  79. package/dist/src/dashboard/detail_queries.d.ts +19 -0
  80. package/dist/src/dashboard/detail_queries.js +98 -0
  81. package/dist/src/dashboard/email_event_builder.d.ts +8 -0
  82. package/dist/src/dashboard/email_event_builder.js +65 -0
  83. package/dist/src/dashboard/explain_query.d.ts +8 -0
  84. package/dist/src/dashboard/explain_query.js +22 -0
  85. package/dist/src/dashboard/filter_handlers.d.ts +23 -0
  86. package/dist/src/dashboard/filter_handlers.js +56 -0
  87. package/dist/src/dashboard/filtered_queries.d.ts +15 -0
  88. package/dist/src/dashboard/filtered_queries.js +155 -0
  89. package/dist/src/dashboard/flush_manager.d.ts +25 -0
  90. package/dist/src/dashboard/flush_manager.js +107 -0
  91. package/dist/src/dashboard/format_helpers.d.ts +126 -0
  92. package/dist/src/dashboard/format_helpers.js +140 -0
  93. package/dist/src/dashboard/inspector_manager.d.ts +36 -0
  94. package/dist/src/dashboard/inspector_manager.js +102 -0
  95. package/dist/src/dashboard/integrations/config_inspector.js +11 -13
  96. package/dist/src/dashboard/integrations/queue_inspector.d.ts +3 -3
  97. package/dist/src/dashboard/integrations/queue_inspector.js +13 -10
  98. package/dist/src/dashboard/jobs_handlers.d.ts +14 -0
  99. package/dist/src/dashboard/jobs_handlers.js +61 -0
  100. package/dist/src/dashboard/knex_factory.d.ts +18 -0
  101. package/dist/src/dashboard/knex_factory.js +91 -0
  102. package/dist/src/dashboard/migrator.js +30 -153
  103. package/dist/src/dashboard/migrator_tables.d.ts +19 -0
  104. package/dist/src/dashboard/migrator_tables.js +153 -0
  105. package/dist/src/dashboard/overview_queries.d.ts +66 -0
  106. package/dist/src/dashboard/overview_queries.js +155 -0
  107. package/dist/src/dashboard/overview_query_runners.d.ts +25 -0
  108. package/dist/src/dashboard/overview_query_runners.js +84 -0
  109. package/dist/src/dashboard/overview_store_queries.d.ts +40 -0
  110. package/dist/src/dashboard/overview_store_queries.js +69 -0
  111. package/dist/src/dashboard/paginate_helper.d.ts +12 -0
  112. package/dist/src/dashboard/paginate_helper.js +33 -0
  113. package/dist/src/dashboard/query_explain_handler.d.ts +10 -0
  114. package/dist/src/dashboard/query_explain_handler.js +80 -0
  115. package/dist/src/dashboard/read_queries.d.ts +32 -0
  116. package/dist/src/dashboard/read_queries.js +107 -0
  117. package/dist/src/dashboard/saved_filter_queries.d.ts +10 -0
  118. package/dist/src/dashboard/saved_filter_queries.js +24 -0
  119. package/dist/src/dashboard/storage_stats.d.ts +41 -0
  120. package/dist/src/dashboard/storage_stats.js +81 -0
  121. package/dist/src/dashboard/write_queue.d.ts +106 -0
  122. package/dist/src/dashboard/write_queue.js +225 -0
  123. package/dist/src/data/data_access.d.ts +6 -36
  124. package/dist/src/data/data_access.js +43 -188
  125. package/dist/src/data/data_access_helpers.d.ts +130 -0
  126. package/dist/src/data/data_access_helpers.js +212 -0
  127. package/dist/src/debug/debug_store.js +37 -32
  128. package/dist/src/debug/email_collector.d.ts +1 -10
  129. package/dist/src/debug/email_collector.js +78 -81
  130. package/dist/src/debug/event_collector.d.ts +0 -9
  131. package/dist/src/debug/event_collector.js +79 -62
  132. package/dist/src/debug/query_collector.js +23 -19
  133. package/dist/src/debug/route_inspector.d.ts +1 -5
  134. package/dist/src/debug/route_inspector.js +50 -51
  135. package/dist/src/debug/trace_collector.d.ts +10 -2
  136. package/dist/src/debug/trace_collector.js +23 -16
  137. package/dist/src/debug/types.d.ts +5 -1
  138. package/dist/src/define_config.d.ts +0 -65
  139. package/dist/src/define_config.js +93 -333
  140. package/dist/src/edge/client/dashboard.js +2 -2
  141. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  142. package/dist/src/edge/client/stats-bar.js +1 -1
  143. package/dist/src/edge/client-vue/dashboard.js +5 -5
  144. package/dist/src/edge/client-vue/debug-panel-deferred.js +3 -3
  145. package/dist/src/edge/client-vue/stats-bar.js +3 -3
  146. package/dist/src/edge/plugin.d.ts +0 -16
  147. package/dist/src/edge/plugin.js +57 -64
  148. package/dist/src/engine/request_metrics.d.ts +1 -0
  149. package/dist/src/engine/request_metrics.js +32 -42
  150. package/dist/src/middleware/request_tracking_middleware.d.ts +3 -8
  151. package/dist/src/middleware/request_tracking_middleware.js +65 -91
  152. package/dist/src/provider/auth_middleware_detector.d.ts +16 -0
  153. package/dist/src/provider/auth_middleware_detector.js +97 -0
  154. package/dist/src/provider/boot_helpers.d.ts +20 -0
  155. package/dist/src/provider/boot_helpers.js +91 -0
  156. package/dist/src/provider/boot_initializer.d.ts +28 -0
  157. package/dist/src/provider/boot_initializer.js +35 -0
  158. package/dist/src/provider/dashboard_init.d.ts +30 -0
  159. package/dist/src/provider/dashboard_init.js +138 -0
  160. package/dist/src/provider/dashboard_setup.d.ts +25 -0
  161. package/dist/src/provider/dashboard_setup.js +78 -0
  162. package/dist/src/provider/diagnostics.d.ts +134 -0
  163. package/dist/src/provider/diagnostics.js +127 -0
  164. package/dist/src/provider/email_bridge.d.ts +43 -0
  165. package/dist/src/provider/email_bridge.js +80 -0
  166. package/dist/src/provider/email_helpers.d.ts +13 -0
  167. package/dist/src/provider/email_helpers.js +68 -0
  168. package/dist/src/provider/pino_hook.d.ts +17 -0
  169. package/dist/src/provider/pino_hook.js +35 -0
  170. package/dist/src/provider/provider_helpers_extra.d.ts +47 -0
  171. package/dist/src/provider/provider_helpers_extra.js +177 -0
  172. package/dist/src/provider/server_stats_provider.d.ts +39 -85
  173. package/dist/src/provider/server_stats_provider.js +131 -936
  174. package/dist/src/provider/shutdown_helpers.d.ts +43 -0
  175. package/dist/src/provider/shutdown_helpers.js +70 -0
  176. package/dist/src/provider/toolbar_setup.d.ts +57 -0
  177. package/dist/src/provider/toolbar_setup.js +141 -0
  178. package/dist/src/routes/dashboard_routes.d.ts +14 -0
  179. package/dist/src/routes/dashboard_routes.js +197 -0
  180. package/dist/src/routes/debug_routes.d.ts +14 -0
  181. package/dist/src/routes/debug_routes.js +101 -0
  182. package/dist/src/routes/register_routes.d.ts +0 -78
  183. package/dist/src/routes/register_routes.js +22 -347
  184. package/dist/src/routes/stats_routes.d.ts +5 -0
  185. package/dist/src/routes/stats_routes.js +14 -0
  186. package/dist/src/styles/components.css +177 -0
  187. package/dist/src/styles/dashboard.css +8 -90
  188. package/dist/src/styles/debug-panel.css +10 -31
  189. package/dist/src/types.d.ts +306 -15
  190. package/dist/vue/{CacheSection-Cx-hj09X.js → CacheSection-ITqvpfH5.js} +1 -1
  191. package/dist/vue/{ConfigSection-CMXyryf6.js → ConfigSection-DTn3GslE.js} +1 -1
  192. package/dist/vue/{EmailsSection-DgKl9xGT.js → EmailsSection-DtLJ4XoS.js} +1 -1
  193. package/dist/vue/{EventsSection-BNMCAim1.js → EventsSection-BOYYz0Ty.js} +1 -1
  194. package/dist/vue/{JobsSection-CCMgMlxd.js → JobsSection-BazTxcJL.js} +1 -1
  195. package/dist/vue/{LogsSection-CvOnTxUu.js → LogsSection-D55PjTKX.js} +9 -3
  196. package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
  197. package/dist/vue/{OverviewSection-CHgaKtUR.js → OverviewSection-1uBKo-Tu.js} +1 -1
  198. package/dist/vue/{QueriesSection-BnHRD98z.js → QueriesSection-rpoZ4ogd.js} +1 -1
  199. package/dist/vue/RelatedLogs.vue_vue_type_script_setup_true_lang-CB2_TzYW.js +84 -0
  200. package/dist/vue/RequestsSection-x7LvT0MC.js +401 -0
  201. package/dist/vue/{RoutesSection-BrceOcKQ.js → RoutesSection-CCD0zZqQ.js} +1 -1
  202. package/dist/vue/TimelineTab-zj5Z5OdT.js +338 -0
  203. package/dist/vue/components/Dashboard/sections/RequestsSection.vue.d.ts +4 -0
  204. package/dist/vue/components/DebugPanel/tabs/TimelineTab.vue.d.ts +4 -0
  205. package/dist/vue/components/{Dashboard/sections/TimelineSection.vue.d.ts → shared/RelatedLogs.vue.d.ts} +5 -6
  206. package/dist/vue/composables/useDashboardData.d.ts +12 -23
  207. package/dist/vue/index-C8MxnS7Q.js +1232 -0
  208. package/dist/vue/index.js +1 -1
  209. package/dist/vue/style.css +1 -1
  210. package/package.json +1 -1
  211. package/dist/react/CacheTab-DYmsZJJ1.js +0 -123
  212. package/dist/react/CustomPaneTab-D7_o3Ec6.js +0 -104
  213. package/dist/react/EmailsTab-Uh2CQY3o.js +0 -153
  214. package/dist/react/EventsTab-CC6DQzEm.js +0 -63
  215. package/dist/react/LogsTab-BbYK-iyh.js +0 -103
  216. package/dist/react/RequestsSection-Cb5a6MlT.js +0 -209
  217. package/dist/react/RoutesTab-Bwreij3e.js +0 -74
  218. package/dist/react/TimelineSection-B2y06kRE.js +0 -158
  219. package/dist/react/TimelineTab-6hthfdBB.js +0 -193
  220. package/dist/react/WaterfallChart-Cj73WdfM.js +0 -100
  221. package/dist/react/index-CecA4IdQ.js +0 -1075
  222. package/dist/react/react/components/Dashboard/sections/TimelineSection.d.ts +0 -8
  223. package/dist/vue/RequestsSection-B-uSlM0f.js +0 -243
  224. package/dist/vue/TimelineSection-CfvnA2Oo.js +0 -186
  225. package/dist/vue/TimelineTab-Db6lKKsD.js +0 -250
  226. package/dist/vue/WaterfallChart.vue_vue_type_script_setup_true_lang-tZ13cNj1.js +0 -118
  227. package/dist/vue/index-oLxS08vN.js +0 -1235
@@ -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 { LogStreamService } from '../log_stream/log_stream_service.js';
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, bold, setVerbose } from '../utils/logger.js';
9
- import { extractAddresses } from '../utils/mail_helpers.js';
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,984 +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.initializeBoot();
43
+ await this.initBoot();
51
44
  }
52
45
  catch (err) {
53
- log.warn(`boot failed: ${err?.message ?? err}\n` +
54
- ` ${dim('The server will continue without server-stats.')}`);
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 initializeBoot() {
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
- // Wire up the per-request shouldShow callback
71
- if (config.shouldShow) {
59
+ if (config.shouldShow)
72
60
  setShouldShow(config.shouldShow);
73
- }
74
- let router = null;
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
- }
61
+ await this.registerRoutes(config);
62
+ this.edgePluginActive = await registerEdgePluginHelper(this.app, config);
192
63
  }
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;
242
- }
243
- /**
244
- * Hook into the AdonisJS logger's Pino stream to feed log entries
245
- * directly into the LogStreamService — no file path needed.
246
- *
247
- * Uses `Symbol.for('pino.stream')` (documented Pino API) to access
248
- * the underlying destination stream, then wraps its `write` method
249
- * to tee entries into the log collector.
250
- */
251
- async hookPinoLogger() {
252
- const logStream = getLogStreamService();
253
- if (!logStream)
254
- return; // logCollector() not in the config
255
- let logger;
256
- try {
257
- logger = await this.app.container.make('logger');
258
- }
259
- catch {
260
- // Logger not available
261
- }
262
- const pino = logger?.pino;
263
- if (!pino)
64
+ async registerRoutes(config) {
65
+ const router = await this.resolve('router');
66
+ if (!router || this.app.inProduction)
264
67
  return;
265
- const streamSym = Symbol.for('pino.stream');
266
- const rawStream = pino[streamSym];
267
- 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)
268
84
  return;
269
- const stream = rawStream;
270
- const originalWrite = stream.write.bind(stream);
271
- stream.write = function (chunk, ...args) {
272
- try {
273
- const str = typeof chunk === 'string' ? chunk : chunk.toString();
274
- const entry = JSON.parse(str);
275
- if (entry && typeof entry.level === 'number') {
276
- logStream.ingest(entry);
277
- }
278
- }
279
- catch {
280
- // Not valid JSON — ignore (e.g. pino-pretty output)
281
- }
282
- return originalWrite(chunk, ...args);
283
- };
284
- this.pinoHookActive = true;
285
- 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));
286
87
  }
287
88
  async ready() {
288
89
  const config = this.app.config.get('server_stats');
289
- if (!config)
290
- return;
291
- if (this.app.inTest && config.skipInTest !== false)
90
+ if (!config || (this.app.inTest && config.skipInTest !== false))
292
91
  return;
293
- // ── Non-web environments: only start the email bridge publisher ──
294
92
  if (this.app.getEnvironment() !== 'web') {
295
- await this.setupEmailBridgePublisher();
93
+ const em = await this.resolve('emitter');
94
+ await setupNonWebBridgeHelper(em, this.emailBridgeChannel);
296
95
  return;
297
96
  }
298
- // ── Web environment: full initialization ──
299
- // Defer to setImmediate so ready() returns immediately. AdonisJS waits
300
- // for all provider ready() hooks before processing HTTP requests —
301
- // blocking here would hang the server.
302
97
  setImmediate(() => {
303
- this.initializeServerStats(config).catch((err) => {
304
- log.warn(`failed to initialize: ${err?.message ?? err}\n` +
305
- ` ${dim('The server will continue without server-stats.')}`);
306
- 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)
307
101
  console.error(err.stack);
308
- }
309
102
  });
310
103
  });
311
104
  }
312
- async initializeServerStats(config) {
105
+ async initStats(config) {
313
106
  this.resolvedConfig = config;
314
- let collectors;
315
- if (!config.collectors || config.collectors === 'auto') {
316
- const { autoDetectCollectors } = await import('../collectors/auto_detect.js');
317
- const result = await autoDetectCollectors();
318
- collectors = result.collectors;
319
- log.info(`${bold(String(result.active))} of ${bold(String(result.total))} collectors active`);
320
- }
321
- else {
322
- collectors = config.collectors;
323
- }
324
- this.resolvedCollectors = collectors;
325
- this.engine = new StatsEngine(collectors);
326
- 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);
327
111
  await this.engine.start();
328
- // Auto-hook log collector into the AdonisJS Pino logger (zero-config)
329
- await this.hookPinoLogger();
330
- // Create the stats controller (makes the stats route functional)
331
- const StatsControllerClass = (await import('../controller/server_stats_controller.js')).default;
332
- this.statsController = new StatsControllerClass(this.engine);
333
- // Dev toolbar setup
334
- const toolbarConfig = config.devToolbar;
335
- if (toolbarConfig?.enabled && !this.app.inProduction) {
336
- try {
337
- await this.setupDevToolbar({
338
- enabled: true,
339
- maxQueries: toolbarConfig.maxQueries ?? 500,
340
- maxEvents: toolbarConfig.maxEvents ?? 200,
341
- maxEmails: toolbarConfig.maxEmails ?? 100,
342
- slowQueryThresholdMs: toolbarConfig.slowQueryThresholdMs ?? 100,
343
- persistDebugData: toolbarConfig.persistDebugData ?? false,
344
- tracing: toolbarConfig.tracing ?? false,
345
- maxTraces: toolbarConfig.maxTraces ?? 200,
346
- dashboard: toolbarConfig.dashboard ?? false,
347
- dashboardPath: toolbarConfig.dashboardPath ?? '/__stats',
348
- retentionDays: toolbarConfig.retentionDays ?? 7,
349
- dbPath: toolbarConfig.dbPath ?? '.adonisjs/server-stats/dashboard.sqlite3',
350
- debugEndpoint: toolbarConfig.debugEndpoint ?? '/admin/api/debug',
351
- });
352
- }
353
- catch (err) {
354
- log.warn(`dev toolbar setup failed: ${err?.message ?? err}\n` +
355
- ` ${dim('Stats bar will still work, but debug panel may be unavailable.')}`);
356
- }
357
- // Exclude the stats endpoint and user-specified prefixes from tracing
358
- // so the debug panel's own polling doesn't flood the timeline
359
- const debugEndpoint = toolbarConfig.debugEndpoint ?? '/admin/api/debug';
360
- const defaultExcludes = [debugEndpoint, config.endpoint].filter((p) => typeof p === 'string');
361
- const prefixes = [...(toolbarConfig.excludeFromTracing ?? defaultExcludes)];
362
- if (typeof config.endpoint === 'string' && !prefixes.includes(config.endpoint)) {
363
- prefixes.push(config.endpoint);
364
- }
365
- if (prefixes.length > 0) {
366
- setExcludedPrefixes(prefixes);
367
- }
368
- // Create the unified ApiController now that debug store is available.
369
- // Dashboard store is passed as a getter so it picks up the reference
370
- // once setupDashboard() completes asynchronously.
371
- if (this.debugStore) {
372
- const logPath = this.app.makePath('logs', 'adonisjs.log');
373
- const { DataAccess: DataAccessClass } = await import('../data/data_access.js');
374
- const dataAccess = new DataAccessClass(this.debugStore, () => this.dashboardStore, logPath);
375
- const { ApiController: ApiControllerClass } = await import('../controller/api_controller.js');
376
- this.apiController = new ApiControllerClass(dataAccess);
377
- }
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);
378
126
  }
379
- // ── Stats collection interval + transmit (lightweight, set up inline) ──
380
- this.setupStatsInterval(config);
381
- // ── Live log streaming via Transmit ──
382
- this.setupLogStream().catch(() => { });
127
+ await this.setupLogBroadcast();
383
128
  log.info('ready');
384
129
  }
385
- /**
386
- * Set up the stats collection interval, transmit broadcasting,
387
- * and Prometheus integration. Extracted from initializeServerStats
388
- * so the ready log fires promptly.
389
- */
390
- setupStatsInterval(config) {
391
- let transmit = null;
392
- let prometheusCollector = null;
393
- // Resolve transmit + prometheus asynchronously but don't block ready()
394
- const resolveIntegrations = async () => {
395
- if (config.transport === 'transmit') {
396
- try {
397
- transmit = await this.app.container.make('transmit');
398
- if (transmit) {
399
- this.transmitAvailable = true;
400
- if (config.channelName) {
401
- this.transmitChannels.push(config.channelName);
402
- }
403
- }
404
- }
405
- catch {
406
- log.info('transport is "transmit" but @adonisjs/transmit is not installed — falling back to polling');
407
- }
408
- }
409
- try {
410
- const mod = await import('../prometheus/prometheus_collector.js');
411
- prometheusCollector = mod.ServerStatsCollector.instance;
412
- }
413
- catch {
414
- // Prometheus not installed — skip (optional dependency)
415
- }
416
- if (prometheusCollector) {
417
- this.prometheusActive = true;
418
- log.info('Prometheus integration active');
419
- }
420
- };
421
- resolveIntegrations().catch(() => { });
422
- this.intervalId = setInterval(async () => {
423
- try {
424
- const stats = await this.engine.collect();
425
- if (transmit && config.channelName) {
426
- ;
427
- transmit.broadcast(config.channelName, stats);
428
- }
429
- if (prometheusCollector) {
430
- ;
431
- prometheusCollector.update(stats);
432
- }
433
- config.onStats?.(stats);
434
- }
435
- catch {
436
- // Silently ignore collection errors
437
- }
438
- }, config.intervalMs);
439
- }
440
- async setupDevToolbar(toolbarConfig) {
441
- this.debugStore = new DebugStore(toolbarConfig);
442
- this.app.container.singleton('debug.store', () => this.debugStore);
443
- // Load persisted data before starting collectors
444
- if (toolbarConfig.persistDebugData) {
445
- this.persistPath =
446
- typeof toolbarConfig.persistDebugData === 'string'
447
- ? this.app.makePath(toolbarConfig.persistDebugData)
448
- : this.app.makePath('.adonisjs', 'server-stats', 'debug-data.json');
449
- await this.debugStore.loadFromDisk(this.persistPath);
450
- }
451
- // Get the emitter
452
- let emitter = null;
453
- try {
454
- emitter = await this.app.container.make('emitter');
455
- }
456
- catch {
457
- log.warn('AdonisJS emitter not available — query and event collection will be disabled');
458
- }
459
- // Get the router
460
- let router = null;
461
- try {
462
- router = await this.app.container.make('router');
463
- }
464
- catch {
465
- // Router not available
466
- }
467
- await this.debugStore.start(emitter, router);
468
- // Set up Redis pub/sub bridge for cross-process email capture
469
- // (e.g., emails sent from Bull queue workers)
470
- await this.setupEmailBridge(emitter);
471
- // Create the debug controller (makes the debug routes functional)
472
- const serverConfig = this.app.config.get('server_stats');
473
- const DebugControllerClass = (await import('../controller/debug_controller.js')).default;
474
- this.debugController = new DebugControllerClass(this.debugStore, serverConfig, {
475
- getEngine: () => this.engine,
476
- getDashboardStore: () => this.dashboardStore,
477
- getProviderDiagnostics: () => this.getDiagnostics(),
478
- getApp: () => this.app,
479
- });
480
- // Wire trace collector into the request tracking middleware
481
- if (this.debugStore.traces) {
482
- setTraceCollector(this.debugStore.traces);
483
- }
484
- // Periodic flush every 30 seconds (handles crashes)
485
- if (this.persistPath) {
486
- this.flushTimer = setInterval(async () => {
487
- try {
488
- await this.debugStore?.saveToDisk(this.persistPath);
489
- }
490
- catch {
491
- // Silently ignore flush errors
492
- }
493
- }, 30_000);
494
- }
495
- // ── Transmit broadcasting for debug panel live updates ────────
496
- let debugTransmit = null;
497
- try {
498
- debugTransmit = await this.app.container.make('transmit');
499
- }
500
- catch {
501
- // Transmit not installed — debug panel will use polling
502
- }
503
- if (debugTransmit) {
504
- this.transmitAvailable = true;
505
- const debugChannel = 'server-stats/debug';
506
- if (!this.transmitChannels.includes(debugChannel)) {
507
- this.transmitChannels.push(debugChannel);
508
- }
509
- const pendingTypes = new Set();
510
- this.debugStore.onNewItem((type) => {
511
- // Debounce: coalesce rapid events into a single broadcast
512
- pendingTypes.add(type);
513
- if (this.debugBroadcastTimer)
514
- return;
515
- this.debugBroadcastTimer = setTimeout(() => {
516
- this.debugBroadcastTimer = null;
517
- const types = Array.from(pendingTypes);
518
- pendingTypes.clear();
519
- try {
520
- ;
521
- debugTransmit.broadcast(debugChannel, { types });
522
- }
523
- catch {
524
- // Silently ignore broadcast errors
525
- }
526
- }, 200);
527
- });
528
- }
529
- // Full-page dashboard setup — deferred with setImmediate so it runs
530
- // AFTER the current event-loop cycle completes. This guarantees
531
- // ready() returns and AdonisJS can process HTTP requests while the
532
- // SQLite store initializes in the background.
533
- if (toolbarConfig.dashboard && this.dashboardDepsAvailable) {
534
- setImmediate(() => {
535
- this.setupDashboard(toolbarConfig, emitter).catch((err) => {
536
- log.warn(`dashboard setup failed: ${err?.message ?? err}\n` +
537
- ` ${dim('Everything else continues to work.')}`);
538
- });
539
- });
540
- }
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;
541
138
  }
542
- /**
543
- * Initialize the full-page dashboard: SQLite store, controller,
544
- * log piping, and per-request data persistence.
545
- *
546
- * Routes are already registered in boot() with a lazy controller getter.
547
- * This method creates the controller so those routes become functional.
548
- */
549
- async setupDashboard(toolbarConfig, emitter) {
550
- log.info('dashboard: initializing SQLite store...');
551
- // Dynamically import DashboardStore so knex/better-sqlite3 are truly optional
552
- const { DashboardStore: DashboardStoreClass } = await import('../dashboard/dashboard_store.js');
553
- this.dashboardStore = new DashboardStoreClass(toolbarConfig);
554
- const appRoot = this.app.makePath('');
139
+ async setupDevToolbar(config) {
140
+ const tc = resolveToolbarConfig({ enabled: true, ...config.devToolbar });
555
141
  try {
556
- // Timeout safety net: if SQLite init hangs (e.g. wrong native binary
557
- // loaded via symlink), abort after 15s instead of freezing forever.
558
- const TIMEOUT_MS = 15_000;
559
- const startPromise = this.dashboardStore.start(null, emitter, appRoot);
560
- const timeoutPromise = new Promise((_, reject) => {
561
- setTimeout(() => reject(new Error(`Dashboard SQLite initialization timed out after ${TIMEOUT_MS / 1000}s`)), TIMEOUT_MS);
142
+ const result = await setupDevToolbarCore({
143
+ tc,
144
+ config,
145
+ app: this.app,
146
+ resolve: (b) => this.resolve(b),
147
+ getDiagnostics: () => this.getDiagnostics(),
562
148
  });
563
- await Promise.race([startPromise, timeoutPromise]);
564
- log.info('dashboard: SQLite store ready');
149
+ if (result)
150
+ applyToolbarResult(result, tc, this);
565
151
  }
566
152
  catch (err) {
567
- const msg = err?.message || '';
568
- const code = err?.code || '';
569
- const isMissingDep = msg.includes('better-sqlite3') ||
570
- msg.includes('knex') ||
571
- msg.includes('Cannot find module') ||
572
- msg.includes('Cannot find package') ||
573
- code === 'ERR_MODULE_NOT_FOUND' ||
574
- code === 'MODULE_NOT_FOUND';
575
- const isTimeout = msg.includes('timed out');
576
- if (isMissingDep) {
577
- log.block('Dashboard could not start — missing dependencies. Install with:', [
578
- '',
579
- bold('npm install knex better-sqlite3'),
580
- '',
581
- dim('Dashboard has been disabled for this session.'),
582
- dim('Everything else (stats bar, debug panel) works without it.'),
583
- ]);
584
- }
585
- else if (isTimeout) {
586
- log.block('Dashboard initialization timed out', [
587
- dim('SQLite setup took too long — this usually means a wrong native'),
588
- dim('binary was loaded (common with symlinked/file: dependencies).'),
589
- '',
590
- dim('Try running:'),
591
- ` ${bold('npm install knex better-sqlite3')}`,
592
- dim('in your app directory to ensure the correct copies are used.'),
593
- '',
594
- dim('Dashboard has been disabled for this session.'),
595
- dim('Everything else (stats bar, debug panel) works without it.'),
596
- ]);
597
- }
598
- else {
599
- log.warn(`Dashboard could not start: ${msg}\n` +
600
- ` ${dim('Dashboard has been disabled for this session.')}`);
601
- if (err?.stack) {
602
- console.error(err.stack);
603
- }
604
- }
605
- this.dashboardStore = null;
606
- return;
607
- }
608
- log.info('dashboard: binding to container...');
609
- this.app.container.singleton('dashboard.store', () => this.dashboardStore);
610
- // Set dashboard path in middleware for self-exclusion
611
- setDashboardPath(toolbarConfig.dashboardPath);
612
- // Create the controller — this makes the routes registered in boot() functional
613
- log.info('dashboard: creating controller...');
614
- const DashboardControllerClass = (await import('../dashboard/dashboard_controller.js')).default;
615
- this.dashboardController = new DashboardControllerClass(this.dashboardStore, this.app);
616
- // ── Log piping ────────────────────────────────────────────────
617
- // If the log collector is already hooked into Pino (zero-config mode),
618
- // piggyback on it instead of creating a separate file poller.
619
- log.info('dashboard: setting up log piping...');
620
- const existingLogStream = getLogStreamService();
621
- if (existingLogStream && !existingLogStream['logPath']) {
622
- // Stream mode — add a listener for dashboard persistence
623
- const origOnEntry = existingLogStream['onEntry'];
624
- existingLogStream['onEntry'] = (entry) => {
625
- origOnEntry?.(entry);
626
- this.dashboardStore?.recordLog(entry);
627
- };
628
- }
629
- else {
630
- // File-based fallback
631
- const logPath = this.app.makePath('logs', 'adonisjs.log');
632
- this.dashboardLogStream = new LogStreamService(logPath, (entry) => {
633
- this.dashboardStore?.recordLog(entry);
634
- });
635
- await this.dashboardLogStream.start();
636
- }
637
- // ── Per-request data piping ────────────────────────────────────
638
- const debugStore = this.debugStore;
639
- const dashStore = this.dashboardStore;
640
- let lastQueryId = 0;
641
- setOnRequestComplete(({ method, url, statusCode, duration, trace }) => {
642
- if (!dashStore.isReady())
643
- return;
644
- // O(K) collection of new queries since last seen ID — avoids
645
- // copying the entire 500-item ring buffer on every request.
646
- const newQueries = debugStore.queries.getQueriesSince(lastQueryId);
647
- if (newQueries.length > 0) {
648
- lastQueryId = newQueries[newQueries.length - 1].id;
649
- }
650
- // Queue for batch persistence (flushed every 500ms)
651
- dashStore.persistRequest({
652
- method,
653
- url,
654
- statusCode,
655
- duration,
656
- queries: newQueries,
657
- trace: trace ?? null,
658
- });
659
- });
660
- // ── Transmit streaming for real-time dashboard updates ────────
661
- let transmit = null;
662
- try {
663
- transmit = await this.app.container.make('transmit');
664
- }
665
- catch {
666
- // Transmit not installed — skip real-time updates
153
+ log.warn(`dev toolbar setup failed: ${err?.message ?? err}\n ${dim('Stats bar will still work.')}`);
667
154
  }
668
- if (transmit) {
669
- this.transmitAvailable = true;
670
- const dashChannel = 'server-stats/dashboard';
671
- if (!this.transmitChannels.includes(dashChannel)) {
672
- this.transmitChannels.push(dashChannel);
673
- }
674
- // Broadcast overview metrics every 30s (not 5s) to reduce SQLite
675
- // pool pressure. Each broadcast runs 5+ sequential queries on the
676
- // single-connection pool, blocking all dashboard API reads.
677
- this.dashboardBroadcastTimer = setInterval(async () => {
678
- try {
679
- if (!dashStore.isReady())
680
- return;
681
- const overview = await dashStore.getOverviewMetrics('1h');
682
- const diagnostics = {
683
- collectors: this.engine.getCollectorHealth(),
684
- buffers: this.debugStore.getBufferStats(),
685
- };
686
- transmit.broadcast(dashChannel, {
687
- ...overview,
688
- diagnostics,
689
- });
690
- }
691
- catch {
692
- // Silently ignore
693
- }
694
- }, 30_000);
695
- }
696
- }
697
- /**
698
- * Lightweight email bridge publisher for non-web environments
699
- * (queue workers, scheduler). Listens to local AdonisJS mail events
700
- * and publishes them to Redis so the web server can ingest them.
701
- */
702
- async setupEmailBridgePublisher() {
703
- let emitter;
704
- try {
705
- emitter = (await this.app.container.make('emitter'));
706
- }
707
- catch {
155
+ const pfx = buildExcludedPrefixes(tc, config.endpoint);
156
+ if (pfx.length > 0)
157
+ setExcludedPrefixes(pfx);
158
+ if (!this.debugStore)
708
159
  return;
709
- }
710
- const { appImport } = await import('../utils/app_import.js');
711
- let redis;
712
- try {
713
- const mod = await appImport('@adonisjs/redis/services/main');
714
- redis = mod.default;
715
- }
716
- catch {
717
- return; // @adonisjs/redis not installed
718
- }
719
- const tag = `${process.pid}-${Date.now()}`;
720
- const ch = this.emailBridgeChannel;
721
- const statusMap = [
722
- ['mail:sending', 'sending'],
723
- ['mail:sent', 'sent'],
724
- ['mail:queueing', 'queueing'],
725
- ['mail:queued', 'queued'],
726
- ['queued:mail:error', 'failed'],
727
- ];
728
- const MAX_HTML = 50_000; // 50 KB cap per email body, same as EmailCollector
729
- const capSize = (v) => {
730
- if (!v || typeof v !== 'string')
731
- return null;
732
- return v.length <= MAX_HTML ? v : v.slice(0, MAX_HTML) + '\n<!-- truncated -->';
733
- };
734
- for (const [event, status] of statusMap) {
735
- emitter.on(event, (data) => {
736
- try {
737
- const d = data;
738
- const msg = (d?.message || d);
739
- const payload = JSON.stringify({
740
- _t: tag,
741
- from: extractAddresses(msg?.from) || 'unknown',
742
- to: extractAddresses(msg?.to) || 'unknown',
743
- cc: extractAddresses(msg?.cc) || null,
744
- bcc: extractAddresses(msg?.bcc) || null,
745
- subject: msg?.subject || '(no subject)',
746
- html: capSize(msg?.html),
747
- text: capSize(msg?.text),
748
- mailer: d?.mailerName || d?.mailer || 'unknown',
749
- status,
750
- messageId: d?.response?.messageId ||
751
- d?.messageId ||
752
- null,
753
- attachmentCount: Array.isArray(msg?.attachments) ? msg.attachments.length : 0,
754
- timestamp: Date.now(),
755
- });
756
- redis.publish(ch, payload).catch(() => { });
757
- }
758
- catch {
759
- // Silently ignore serialization errors
760
- }
761
- });
762
- }
763
- 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')));
764
163
  }
765
- /**
766
- * Set up live log streaming via Transmit (web environment only).
767
- * Merged from the former standalone LogStreamProvider.
768
- */
769
- async setupLogStream() {
770
- let transmit;
771
- try {
772
- transmit = (await this.app.container.make('transmit'));
773
- }
774
- catch {
775
- return; // @adonisjs/transmit not available
776
- }
777
- const channelName = this.app.config.get('server_stats.logChannelName', 'admin/logs');
778
- const broadcast = (entry) => {
779
- try {
780
- transmit.broadcast(channelName, entry);
781
- }
782
- catch {
783
- // Silently ignore broadcast errors
784
- }
785
- };
786
- // If logCollector() is already hooked into Pino (zero-config),
787
- // piggyback on its stream instead of creating a file poller.
788
- const existing = getLogStreamService();
789
- if (existing) {
790
- const internal = existing;
791
- const origOnEntry = internal.onEntry;
792
- internal.onEntry = (entry) => {
793
- origOnEntry?.(entry);
794
- broadcast(entry);
795
- };
164
+ async setupLogBroadcast() {
165
+ const t = (await this.resolve('transmit'));
166
+ if (!t)
796
167
  return;
797
- }
798
- // Fallback: poll the log file directly
799
- const logPath = this.app.makePath('logs', 'adonisjs.log');
800
- this.logStreamService = new LogStreamService(logPath, broadcast);
801
- 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));
802
170
  }
803
- /**
804
- * Set up a Redis pub/sub bridge for cross-process email capture.
805
- *
806
- * Mail events (`mail:sending`, `mail:sent`, etc.) are process-local.
807
- * When emails are sent from a Bull queue worker, the web server's
808
- * {@link EmailCollector} never sees them. This bridge solves that:
809
- *
810
- * 1. **Every process** publishes mail events to a Redis channel.
811
- * 2. **Every process** subscribes and ingests events from *other*
812
- * processes (identified by a unique process tag).
813
- *
814
- * Requires `@adonisjs/redis`. Silently skipped if not installed.
815
- */
816
- async setupEmailBridge(emitter) {
817
- if (!emitter)
818
- return;
819
- const { appImport } = await import('../utils/app_import.js');
820
- let redis;
171
+ async resolve(binding) {
821
172
  try {
822
- const mod = await appImport('@adonisjs/redis/services/main');
823
- redis = mod.default;
173
+ return await this.app.container.make(binding);
824
174
  }
825
175
  catch {
826
- return; // @adonisjs/redis not installed — skip
827
- }
828
- const CHANNEL = this.emailBridgeChannel;
829
- const processTag = `${process.pid}-${Date.now()}`;
830
- const em = emitter;
831
- // ── Publish local mail events to Redis ──────────────────────
832
- const statusMap = [
833
- ['mail:sending', 'sending'],
834
- ['mail:sent', 'sent'],
835
- ['mail:queueing', 'queueing'],
836
- ['mail:queued', 'queued'],
837
- ['queued:mail:error', 'failed'],
838
- ];
839
- const MAX_HTML = 50_000;
840
- const capSize = (v) => {
841
- if (!v || typeof v !== 'string')
842
- return null;
843
- return v.length <= MAX_HTML ? v : v.slice(0, MAX_HTML) + '\n<!-- truncated -->';
844
- };
845
- for (const [event, status] of statusMap) {
846
- em.on(event, (data) => {
847
- try {
848
- const d = data;
849
- const msg = d?.message || d;
850
- const payload = JSON.stringify({
851
- _t: processTag,
852
- from: extractAddresses(msg?.from) || 'unknown',
853
- to: extractAddresses(msg?.to) || 'unknown',
854
- cc: extractAddresses(msg?.cc) || null,
855
- bcc: extractAddresses(msg?.bcc) || null,
856
- subject: msg?.subject || '(no subject)',
857
- html: capSize(msg?.html),
858
- text: capSize(msg?.text),
859
- mailer: d?.mailerName || d?.mailer || 'unknown',
860
- status,
861
- messageId: d?.response?.messageId || d?.messageId || null,
862
- attachmentCount: Array.isArray(msg?.attachments) ? msg.attachments.length : 0,
863
- timestamp: Date.now(),
864
- });
865
- redis.publish(CHANNEL, payload).catch(() => { });
866
- }
867
- catch {
868
- // Silently ignore serialization errors
869
- }
870
- });
871
- }
872
- // ── Subscribe to receive emails from other processes ─────────
873
- try {
874
- await redis.subscribe(CHANNEL, (message) => {
875
- try {
876
- const parsed = JSON.parse(message);
877
- if (parsed._t === processTag)
878
- return; // Skip own messages
879
- const { _t: _, ...fields } = parsed;
880
- const record = {
881
- ...fields,
882
- html: fields.html || null,
883
- text: fields.text || null,
884
- };
885
- // Ingest into in-memory ring buffer (debug panel)
886
- this.debugStore?.emails.ingest(record);
887
- // Also persist to SQLite dashboard (the sending process has
888
- // no DashboardStore, so we must record it here)
889
- this.dashboardStore?.recordEmail({ id: 0, ...record });
890
- }
891
- catch {
892
- // Ignore malformed messages
893
- }
894
- });
895
- this.emailBridgeRedis = redis;
896
- log.info('email bridge active (cross-process capture via Redis)');
897
- }
898
- catch {
899
- // Subscribe failed — bridge unavailable, local capture still works
176
+ return null;
900
177
  }
901
178
  }
902
- /** Return diagnostics state for the Internals endpoint. */
903
179
  getDiagnostics() {
904
- const config = this.resolvedConfig;
905
- const toolbarConfig = config?.devToolbar;
906
- return {
907
- timers: {
908
- collectionInterval: {
909
- active: this.intervalId !== null,
910
- intervalMs: config?.intervalMs ?? 0,
911
- },
912
- dashboardBroadcast: {
913
- active: this.dashboardBroadcastTimer !== null,
914
- intervalMs: 30_000,
915
- },
916
- debugBroadcast: {
917
- active: this.debugBroadcastTimer !== null,
918
- debounceMs: 200,
919
- },
920
- persistFlush: {
921
- active: this.flushTimer !== null,
922
- intervalMs: 30_000,
923
- },
924
- retentionCleanup: {
925
- active: this.dashboardStore?.isReady() ?? false,
926
- intervalMs: 60 * 60 * 1000,
927
- },
928
- },
929
- transmit: {
930
- available: this.transmitAvailable,
931
- channels: this.transmitChannels,
932
- },
933
- integrations: {
934
- prometheus: { active: this.prometheusActive },
935
- pinoHook: {
936
- active: this.pinoHookActive,
937
- mode: this.pinoHookActive ? 'stream' : toolbarConfig?.enabled ? 'none' : 'none',
938
- },
939
- edgePlugin: { active: this.edgePluginActive },
940
- emailBridge: { active: this.emailBridgeRedis !== null },
941
- cacheInspector: {
942
- available: this.resolvedCollectors.some((c) => c.name === 'redis'),
943
- },
944
- queueInspector: {
945
- available: this.resolvedCollectors.some((c) => c.name === 'queue'),
946
- },
947
- },
948
- config: {
949
- intervalMs: config?.intervalMs ?? 0,
950
- transport: config?.transport ?? 'none',
951
- channelName: config?.channelName ?? '',
952
- endpoint: config?.endpoint ?? false,
953
- skipInTest: config?.skipInTest !== false,
954
- hasOnStatsCallback: typeof config?.onStats === 'function',
955
- hasShouldShowCallback: typeof config?.shouldShow === 'function',
956
- },
957
- devToolbar: {
958
- enabled: !!toolbarConfig?.enabled,
959
- maxQueries: toolbarConfig?.maxQueries ?? 500,
960
- maxEvents: toolbarConfig?.maxEvents ?? 200,
961
- maxEmails: toolbarConfig?.maxEmails ?? 100,
962
- maxTraces: toolbarConfig?.maxTraces ?? 200,
963
- slowQueryThresholdMs: toolbarConfig?.slowQueryThresholdMs ?? 100,
964
- tracing: toolbarConfig?.tracing ?? false,
965
- dashboard: toolbarConfig?.dashboard ?? false,
966
- dashboardPath: toolbarConfig?.dashboardPath ?? '/__stats',
967
- debugEndpoint: toolbarConfig?.debugEndpoint ?? '/admin/api/debug',
968
- retentionDays: toolbarConfig?.retentionDays ?? 7,
969
- dbPath: toolbarConfig?.dbPath ?? '.adonisjs/server-stats/dashboard.sqlite3',
970
- persistDebugData: toolbarConfig?.persistDebugData ?? false,
971
- renderer: toolbarConfig?.renderer ?? 'preact',
972
- excludeFromTracing: toolbarConfig?.excludeFromTracing ?? [],
973
- customPaneCount: toolbarConfig?.panes?.length ?? 0,
974
- },
975
- };
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
+ });
976
196
  }
977
197
  async shutdown() {
978
- if (this.intervalId) {
979
- clearInterval(this.intervalId);
980
- this.intervalId = null;
981
- }
982
- if (this.flushTimer) {
983
- clearInterval(this.flushTimer);
984
- this.flushTimer = null;
985
- }
986
- if (this.dashboardBroadcastTimer) {
987
- clearInterval(this.dashboardBroadcastTimer);
988
- this.dashboardBroadcastTimer = null;
989
- }
990
- if (this.debugBroadcastTimer) {
991
- clearTimeout(this.debugBroadcastTimer);
992
- this.debugBroadcastTimer = null;
993
- }
994
- // Save debug data before stopping collectors
995
- if (this.persistPath && this.debugStore) {
996
- try {
997
- await this.debugStore.saveToDisk(this.persistPath);
998
- }
999
- catch (err) {
1000
- log.warn('could not save debug data on shutdown — ' + err?.message);
1001
- }
1002
- }
1003
- // Unsubscribe Redis email bridge
1004
- if (this.emailBridgeRedis) {
1005
- try {
1006
- ;
1007
- this.emailBridgeRedis.unsubscribe(this.emailBridgeChannel);
1008
- }
1009
- catch {
1010
- // Ignore cleanup errors
1011
- }
1012
- this.emailBridgeRedis = null;
1013
- }
1014
- // Clean up log stream and dashboard resources
1015
- this.logStreamService?.stop();
1016
- this.dashboardLogStream?.stop();
1017
- setOnRequestComplete(null);
1018
- setDashboardPath(null);
1019
- setExcludedPrefixes([]);
1020
- await this.dashboardStore?.stop();
1021
- this.debugStore?.stop();
1022
- 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
+ });
1023
218
  }
1024
219
  }