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.
Files changed (248) 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/field-resolvers.d.ts +64 -0
  10. package/dist/core/formatters-helpers.d.ts +23 -0
  11. package/dist/core/formatters.d.ts +15 -0
  12. package/dist/core/index.d.ts +1 -1
  13. package/dist/core/index.js +599 -509
  14. package/dist/core/log-utils-helpers.d.ts +13 -0
  15. package/dist/core/metrics.d.ts +3 -28
  16. package/dist/core/pagination.d.ts +0 -9
  17. package/dist/core/server-stats-controller.d.ts +6 -0
  18. package/dist/core/transmit-helpers.d.ts +7 -0
  19. package/dist/core/types-dashboard.d.ts +178 -0
  20. package/dist/core/types-diagnostics.d.ts +85 -0
  21. package/dist/core/types.d.ts +10 -442
  22. package/dist/react/CacheSection-BYN53kYO.js +135 -0
  23. package/dist/react/CacheStatsBar-CRodCOeP.js +27 -0
  24. package/dist/react/CacheTab-DOhuK05d.js +106 -0
  25. package/dist/react/{ConfigSection-DfFd-WRq.js → ConfigSection-B9EHh4Rp.js} +1 -1
  26. package/dist/react/{ConfigTab-Bdg8YMer.js → ConfigTab-C8kriE2b.js} +1 -1
  27. package/dist/react/CustomPaneTab-CvzQS_Wh.js +99 -0
  28. package/dist/react/EmailPreviewOverlay-BmXOAvqG.js +58 -0
  29. package/dist/react/EmailsSection-BJyFJf7A.js +226 -0
  30. package/dist/react/EmailsTab-Ch8jp10B.js +110 -0
  31. package/dist/react/{EventsSection-ByQ-9blq.js → EventsSection-DJPwHeT8.js} +28 -27
  32. package/dist/react/EventsTab-B-FoehXC.js +58 -0
  33. package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
  34. package/dist/react/{InternalsContent-DBzsI0CG.js → InternalsContent-O8ino9oM.js} +133 -109
  35. package/dist/react/InternalsSection-B6VlVx5f.js +22 -0
  36. package/dist/react/InternalsTab-CkEKpRMU.js +17 -0
  37. package/dist/react/JobStatsBar-C7RslAFE.js +30 -0
  38. package/dist/react/JobsSection-DWF4i1t_.js +167 -0
  39. package/dist/react/JobsTab-DqnifQXV.js +129 -0
  40. package/dist/react/LogEntryRow-CMMkqA9M.js +43 -0
  41. package/dist/react/LogsSection-C1xC5aP4.js +198 -0
  42. package/dist/react/LogsTab-CS4sLfLw.js +79 -0
  43. package/dist/react/{OverviewSection-C4T1ur51.js → OverviewSection-CxvfOR0v.js} +70 -80
  44. package/dist/react/QueriesSection-CrMdU5Ax.js +458 -0
  45. package/dist/react/{QueriesTab-osLUWd4L.js → QueriesTab-x85PjkyS.js} +38 -40
  46. package/dist/react/RequestsSection-DETN9oZb.js +321 -0
  47. package/dist/react/{RoutesSection-BUSkM6PY.js → RoutesSection-CmorkJeC.js} +2 -2
  48. package/dist/react/RoutesTab-CbzBOzpc.js +68 -0
  49. package/dist/react/SplitPaneWrapper-BiIgT4ND.js +49 -0
  50. package/dist/react/TimeAgoCell-o3KigGfM.js +8 -0
  51. package/dist/react/{TimelineTab-Covg5weo.js → TimelineTab-Ue9tUD_n.js} +76 -102
  52. package/dist/react/index-DwDK-4oX.js +1121 -0
  53. package/dist/react/index.js +6 -6
  54. package/dist/react/react/components/shared/CacheStatsBar.d.ts +13 -0
  55. package/dist/react/react/components/shared/EmailPreviewOverlay.d.ts +29 -0
  56. package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
  57. package/dist/react/react/components/shared/JobStatsBar.d.ts +12 -0
  58. package/dist/react/react/components/shared/LogEntryRow.d.ts +9 -0
  59. package/dist/react/react/components/shared/RelatedLogs.d.ts +2 -2
  60. package/dist/react/react/components/shared/SplitPaneWrapper.d.ts +7 -0
  61. package/dist/react/react/components/shared/TimeAgoCell.d.ts +17 -0
  62. package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
  63. package/dist/react/react/hooks/useDiagnosticsData.d.ts +14 -0
  64. package/dist/react/style.css +1 -1
  65. package/dist/src/collectors/app_collector.d.ts +0 -8
  66. package/dist/src/collectors/app_collector.js +45 -52
  67. package/dist/src/collectors/auto_detect.d.ts +0 -23
  68. package/dist/src/collectors/auto_detect.js +33 -55
  69. package/dist/src/collectors/db_pool_collector.d.ts +14 -16
  70. package/dist/src/collectors/db_pool_collector.js +72 -57
  71. package/dist/src/collectors/log_collector.d.ts +0 -47
  72. package/dist/src/collectors/log_collector.js +36 -65
  73. package/dist/src/collectors/queue_collector.d.ts +0 -20
  74. package/dist/src/collectors/queue_collector.js +60 -76
  75. package/dist/src/collectors/redis_collector.d.ts +10 -10
  76. package/dist/src/collectors/redis_collector.js +69 -66
  77. package/dist/src/config/deprecation_migration.d.ts +7 -0
  78. package/dist/src/config/deprecation_migration.js +201 -0
  79. package/dist/src/controller/debug_controller.d.ts +1 -1
  80. package/dist/src/controller/debug_controller.js +87 -81
  81. package/dist/src/dashboard/cache_handlers.d.ts +14 -0
  82. package/dist/src/dashboard/cache_handlers.js +52 -0
  83. package/dist/src/dashboard/chart_aggregator.d.ts +0 -7
  84. package/dist/src/dashboard/chart_aggregator.js +68 -50
  85. package/dist/src/dashboard/coalesce_cache.d.ts +25 -0
  86. package/dist/src/dashboard/coalesce_cache.js +47 -0
  87. package/dist/src/dashboard/dashboard_controller.d.ts +11 -37
  88. package/dist/src/dashboard/dashboard_controller.js +51 -544
  89. package/dist/src/dashboard/dashboard_page_assets.d.ts +17 -0
  90. package/dist/src/dashboard/dashboard_page_assets.js +51 -0
  91. package/dist/src/dashboard/dashboard_store.d.ts +19 -218
  92. package/dist/src/dashboard/dashboard_store.js +115 -1116
  93. package/dist/src/dashboard/dashboard_types.d.ts +83 -0
  94. package/dist/src/dashboard/dashboard_types.js +4 -0
  95. package/dist/src/dashboard/detail_queries.d.ts +19 -0
  96. package/dist/src/dashboard/detail_queries.js +98 -0
  97. package/dist/src/dashboard/email_event_builder.d.ts +8 -0
  98. package/dist/src/dashboard/email_event_builder.js +65 -0
  99. package/dist/src/dashboard/explain_query.d.ts +8 -0
  100. package/dist/src/dashboard/explain_query.js +22 -0
  101. package/dist/src/dashboard/filter_handlers.d.ts +23 -0
  102. package/dist/src/dashboard/filter_handlers.js +56 -0
  103. package/dist/src/dashboard/filtered_queries.d.ts +15 -0
  104. package/dist/src/dashboard/filtered_queries.js +155 -0
  105. package/dist/src/dashboard/flush_manager.d.ts +25 -0
  106. package/dist/src/dashboard/flush_manager.js +107 -0
  107. package/dist/src/dashboard/format_helpers.d.ts +126 -0
  108. package/dist/src/dashboard/format_helpers.js +140 -0
  109. package/dist/src/dashboard/inspector_manager.d.ts +36 -0
  110. package/dist/src/dashboard/inspector_manager.js +102 -0
  111. package/dist/src/dashboard/integrations/config_inspector.js +11 -13
  112. package/dist/src/dashboard/integrations/queue_inspector.d.ts +3 -3
  113. package/dist/src/dashboard/integrations/queue_inspector.js +13 -10
  114. package/dist/src/dashboard/jobs_handlers.d.ts +14 -0
  115. package/dist/src/dashboard/jobs_handlers.js +61 -0
  116. package/dist/src/dashboard/knex_factory.d.ts +18 -0
  117. package/dist/src/dashboard/knex_factory.js +91 -0
  118. package/dist/src/dashboard/migrator.js +30 -159
  119. package/dist/src/dashboard/migrator_tables.d.ts +19 -0
  120. package/dist/src/dashboard/migrator_tables.js +153 -0
  121. package/dist/src/dashboard/overview_queries.d.ts +66 -0
  122. package/dist/src/dashboard/overview_queries.js +155 -0
  123. package/dist/src/dashboard/overview_query_runners.d.ts +25 -0
  124. package/dist/src/dashboard/overview_query_runners.js +84 -0
  125. package/dist/src/dashboard/overview_store_queries.d.ts +40 -0
  126. package/dist/src/dashboard/overview_store_queries.js +69 -0
  127. package/dist/src/dashboard/paginate_helper.d.ts +12 -0
  128. package/dist/src/dashboard/paginate_helper.js +33 -0
  129. package/dist/src/dashboard/query_explain_handler.d.ts +10 -0
  130. package/dist/src/dashboard/query_explain_handler.js +80 -0
  131. package/dist/src/dashboard/read_queries.d.ts +32 -0
  132. package/dist/src/dashboard/read_queries.js +107 -0
  133. package/dist/src/dashboard/saved_filter_queries.d.ts +10 -0
  134. package/dist/src/dashboard/saved_filter_queries.js +24 -0
  135. package/dist/src/dashboard/storage_stats.d.ts +41 -0
  136. package/dist/src/dashboard/storage_stats.js +81 -0
  137. package/dist/src/dashboard/write_queue.d.ts +106 -0
  138. package/dist/src/dashboard/write_queue.js +225 -0
  139. package/dist/src/data/data_access.d.ts +2 -39
  140. package/dist/src/data/data_access.js +17 -193
  141. package/dist/src/data/data_access_helpers.d.ts +130 -0
  142. package/dist/src/data/data_access_helpers.js +212 -0
  143. package/dist/src/debug/debug_store.js +37 -32
  144. package/dist/src/debug/email_collector.d.ts +1 -10
  145. package/dist/src/debug/email_collector.js +78 -81
  146. package/dist/src/debug/event_collector.d.ts +0 -9
  147. package/dist/src/debug/event_collector.js +79 -62
  148. package/dist/src/debug/query_collector.js +23 -19
  149. package/dist/src/debug/route_inspector.d.ts +1 -5
  150. package/dist/src/debug/route_inspector.js +50 -51
  151. package/dist/src/debug/trace_collector.d.ts +9 -1
  152. package/dist/src/debug/trace_collector.js +21 -15
  153. package/dist/src/debug/types.d.ts +1 -1
  154. package/dist/src/define_config.d.ts +0 -65
  155. package/dist/src/define_config.js +93 -333
  156. package/dist/src/edge/client/dashboard.js +2 -2
  157. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  158. package/dist/src/edge/client/stats-bar.js +1 -1
  159. package/dist/src/edge/client-vue/dashboard.js +5 -5
  160. package/dist/src/edge/client-vue/debug-panel-deferred.js +4 -4
  161. package/dist/src/edge/client-vue/stats-bar.js +3 -3
  162. package/dist/src/edge/plugin.d.ts +0 -16
  163. package/dist/src/edge/plugin.js +57 -64
  164. package/dist/src/engine/request_metrics.d.ts +1 -0
  165. package/dist/src/engine/request_metrics.js +32 -42
  166. package/dist/src/middleware/request_tracking_middleware.d.ts +2 -8
  167. package/dist/src/middleware/request_tracking_middleware.js +65 -93
  168. package/dist/src/provider/auth_middleware_detector.d.ts +16 -0
  169. package/dist/src/provider/auth_middleware_detector.js +97 -0
  170. package/dist/src/provider/boot_helpers.d.ts +20 -0
  171. package/dist/src/provider/boot_helpers.js +91 -0
  172. package/dist/src/provider/boot_initializer.d.ts +28 -0
  173. package/dist/src/provider/boot_initializer.js +35 -0
  174. package/dist/src/provider/dashboard_init.d.ts +30 -0
  175. package/dist/src/provider/dashboard_init.js +138 -0
  176. package/dist/src/provider/dashboard_setup.d.ts +25 -0
  177. package/dist/src/provider/dashboard_setup.js +78 -0
  178. package/dist/src/provider/diagnostics.d.ts +134 -0
  179. package/dist/src/provider/diagnostics.js +127 -0
  180. package/dist/src/provider/email_bridge.d.ts +43 -0
  181. package/dist/src/provider/email_bridge.js +80 -0
  182. package/dist/src/provider/email_helpers.d.ts +13 -0
  183. package/dist/src/provider/email_helpers.js +68 -0
  184. package/dist/src/provider/pino_hook.d.ts +17 -0
  185. package/dist/src/provider/pino_hook.js +35 -0
  186. package/dist/src/provider/provider_helpers_extra.d.ts +47 -0
  187. package/dist/src/provider/provider_helpers_extra.js +177 -0
  188. package/dist/src/provider/server_stats_provider.d.ts +39 -85
  189. package/dist/src/provider/server_stats_provider.js +132 -951
  190. package/dist/src/provider/shutdown_helpers.d.ts +43 -0
  191. package/dist/src/provider/shutdown_helpers.js +70 -0
  192. package/dist/src/provider/toolbar_setup.d.ts +57 -0
  193. package/dist/src/provider/toolbar_setup.js +141 -0
  194. package/dist/src/routes/dashboard_routes.d.ts +14 -0
  195. package/dist/src/routes/dashboard_routes.js +197 -0
  196. package/dist/src/routes/debug_routes.d.ts +14 -0
  197. package/dist/src/routes/debug_routes.js +101 -0
  198. package/dist/src/routes/register_routes.d.ts +0 -78
  199. package/dist/src/routes/register_routes.js +22 -352
  200. package/dist/src/routes/stats_routes.d.ts +5 -0
  201. package/dist/src/routes/stats_routes.js +14 -0
  202. package/dist/src/styles/components.css +163 -0
  203. package/dist/src/styles/dashboard.css +13 -105
  204. package/dist/src/styles/debug-panel.css +2 -53
  205. package/dist/src/styles/utilities.css +3 -1
  206. package/dist/src/types.d.ts +305 -14
  207. package/dist/vue/{CacheSection-oFAJL3mo.js → CacheSection-DT2Mwf_s.js} +1 -1
  208. package/dist/vue/{ConfigSection-BhfJ4KqL.js → ConfigSection-BwKwS9lh.js} +1 -1
  209. package/dist/vue/CustomPaneTab-Hr1IBHfz.js +172 -0
  210. package/dist/vue/{EmailsSection-BcNyhyHs.js → EmailsSection-B65g0FVS.js} +1 -1
  211. package/dist/vue/{EventsSection-r60Q5Lmu.js → EventsSection-CxqtVF-o.js} +1 -1
  212. package/dist/vue/{JobsSection-BHL-hkQw.js → JobsSection-rMIyMb-g.js} +1 -1
  213. package/dist/vue/{LogsSection-DRMGzJmg.js → LogsSection-DmmZVJ7D.js} +9 -3
  214. package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
  215. package/dist/vue/{OverviewSection-CXh6Ja1B.js → OverviewSection-BMabyqw-.js} +49 -50
  216. package/dist/vue/{QueriesSection-IodIsCJ-.js → QueriesSection-BfDFwGqH.js} +44 -45
  217. package/dist/vue/{QueriesTab-C8_7oprC.js → QueriesTab-DuTG7cpC.js} +30 -31
  218. package/dist/vue/RelatedLogs.vue_vue_type_script_setup_true_lang-Py1iu9GU.js +77 -0
  219. package/dist/vue/{RequestsSection-BPuMdmMc.js → RequestsSection-CTu4jPZ_.js} +143 -147
  220. package/dist/vue/{RoutesSection-NKo3Rbq3.js → RoutesSection-zQZDedL7.js} +1 -1
  221. package/dist/vue/TimelineTab-DHfXsX7t.js +334 -0
  222. package/dist/vue/components/shared/RelatedLogs.vue.d.ts +1 -4
  223. package/dist/vue/composables/useDashboardData.d.ts +12 -23
  224. package/dist/vue/index-CM3yNVUR.js +1232 -0
  225. package/dist/vue/index.js +1 -1
  226. package/dist/vue/style.css +1 -1
  227. package/package.json +1 -1
  228. package/dist/react/CacheSection-UCMptWyn.js +0 -146
  229. package/dist/react/CacheTab-CA8LB1J5.js +0 -123
  230. package/dist/react/CustomPaneTab-Bxtv_8Rw.js +0 -104
  231. package/dist/react/EmailsSection-CM7stSyh.js +0 -262
  232. package/dist/react/EmailsTab-BDhEiomM.js +0 -153
  233. package/dist/react/EventsTab-CMfY98Rl.js +0 -63
  234. package/dist/react/InternalsSection-t7ihcWO-.js +0 -32
  235. package/dist/react/InternalsTab-Oij0A2fN.js +0 -30
  236. package/dist/react/JobsSection-DF3qEv9O.js +0 -187
  237. package/dist/react/JobsTab-BbrBWIOb.js +0 -141
  238. package/dist/react/LogsSection-DcFTZY7b.js +0 -227
  239. package/dist/react/LogsTab-CicucmVk.js +0 -103
  240. package/dist/react/QueriesSection-PswteoF9.js +0 -461
  241. package/dist/react/RelatedLogs-DFDOyUMr.js +0 -40
  242. package/dist/react/RequestsSection-Nag30rEA.js +0 -341
  243. package/dist/react/RoutesTab-DgVzd2PZ.js +0 -74
  244. package/dist/react/index-Cflz9Ebj.js +0 -1069
  245. package/dist/vue/CustomPaneTab-BJxT5Dp7.js +0 -172
  246. package/dist/vue/RelatedLogs.vue_vue_type_script_setup_true_lang-CB2_TzYW.js +0 -84
  247. package/dist/vue/TimelineTab-zj5Z5OdT.js +0 -338
  248. 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 { 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,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.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
- }
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
- * Hook into the AdonisJS logger's Pino stream to feed log entries
245
- * directly into the LogStreamService — no file path needed.
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
- let logger;
256
- try {
257
- logger = await this.app.container.make('logger');
258
- }
259
- catch {
260
- // Logger not available yet
261
- }
262
- const pino = logger?.pino;
263
- if (!pino)
264
- return;
265
- // Use pino's exported streamSym — NOT Symbol.for('pino.stream').
266
- // Pino uses a local Symbol('pino.stream'), not a global registry symbol.
267
- let streamSym;
268
- try {
269
- const pinoMod = await import('pino');
270
- streamSym = pinoMod.default?.symbols?.streamSym;
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
- const stream = rawStream;
284
- const originalWrite = stream.write.bind(stream);
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.setupEmailBridgePublisher();
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.initializeServerStats(config).catch((err) => {
318
- log.warn(`failed to initialize: ${err?.message ?? err}\n` +
319
- ` ${dim('The server will continue without server-stats.')}`);
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 initializeServerStats(config) {
105
+ async initStats(config) {
327
106
  this.resolvedConfig = config;
328
- let collectors;
329
- if (!config.collectors || config.collectors === 'auto') {
330
- const { autoDetectCollectors } = await import('../collectors/auto_detect.js');
331
- const result = await autoDetectCollectors();
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
- // Auto-hook log collector into the AdonisJS Pino logger (zero-config)
343
- await this.hookPinoLogger();
344
- // Create the stats controller (makes the stats route functional)
345
- const StatsControllerClass = (await import('../controller/server_stats_controller.js')).default;
346
- this.statsController = new StatsControllerClass(this.engine);
347
- // Dev toolbar setup
348
- const toolbarConfig = config.devToolbar;
349
- if (toolbarConfig?.enabled && !this.app.inProduction) {
350
- try {
351
- await this.setupDevToolbar({
352
- enabled: true,
353
- maxQueries: toolbarConfig.maxQueries ?? 500,
354
- maxEvents: toolbarConfig.maxEvents ?? 200,
355
- maxEmails: toolbarConfig.maxEmails ?? 100,
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
- // ── Stats collection interval + transmit (lightweight, set up inline) ──
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
- * Set up the stats collection interval, transmit broadcasting,
401
- * and Prometheus integration. Extracted from initializeServerStats
402
- * so the ready log fires promptly.
403
- */
404
- setupStatsInterval(config) {
405
- let transmit = null;
406
- let prometheusCollector = null;
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
- * Initialize the full-page dashboard: SQLite store, controller,
558
- * log piping, and per-request data persistence.
559
- *
560
- * Routes are already registered in boot() with a lazy controller getter.
561
- * This method creates the controller so those routes become functional.
562
- */
563
- async setupDashboard(toolbarConfig, emitter) {
564
- log.info('dashboard: initializing SQLite store...');
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
- await Promise.race([startPromise, timeoutPromise]);
578
- log.info('dashboard: SQLite store ready');
149
+ if (result)
150
+ applyToolbarResult(result, tc, this);
579
151
  }
580
152
  catch (err) {
581
- const msg = err?.message || '';
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
- if (transmit) {
684
- this.transmitAvailable = true;
685
- const dashChannel = 'server-stats/dashboard';
686
- if (!this.transmitChannels.includes(dashChannel)) {
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 { appImport } = await import('../utils/app_import.js');
726
- let redis;
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
- * Set up live log streaming via Transmit (web environment only).
782
- * Merged from the former standalone LogStreamProvider.
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
- // Fallback: poll the log file directly
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
- const mod = await appImport('@adonisjs/redis/services/main');
837
- redis = mod.default;
173
+ return await this.app.container.make(binding);
838
174
  }
839
175
  catch {
840
- return; // @adonisjs/redis not installed — skip
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
- const config = this.resolvedConfig;
919
- const toolbarConfig = config?.devToolbar;
920
- return {
921
- timers: {
922
- collectionInterval: {
923
- active: this.intervalId !== null,
924
- intervalMs: config?.intervalMs ?? 0,
925
- },
926
- dashboardBroadcast: {
927
- active: this.dashboardBroadcastTimer !== null,
928
- intervalMs: 30_000,
929
- },
930
- debugBroadcast: {
931
- active: this.debugBroadcastTimer !== null,
932
- debounceMs: 200,
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
- if (this.intervalId) {
993
- clearInterval(this.intervalId);
994
- this.intervalId = null;
995
- }
996
- if (this.flushTimer) {
997
- clearInterval(this.flushTimer);
998
- this.flushTimer = null;
999
- }
1000
- if (this.dashboardBroadcastTimer) {
1001
- clearInterval(this.dashboardBroadcastTimer);
1002
- this.dashboardBroadcastTimer = null;
1003
- }
1004
- if (this.debugBroadcastTimer) {
1005
- clearTimeout(this.debugBroadcastTimer);
1006
- this.debugBroadcastTimer = null;
1007
- }
1008
- // Save debug data before stopping collectors
1009
- if (this.persistPath && this.debugStore) {
1010
- try {
1011
- await this.debugStore.saveToDisk(this.persistPath);
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
  }