adonisjs-server-stats 1.10.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 (210) 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 +594 -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/transmit-helpers.d.ts +7 -0
  16. package/dist/core/types-dashboard.d.ts +178 -0
  17. package/dist/core/types-diagnostics.d.ts +85 -0
  18. package/dist/core/types.d.ts +10 -442
  19. package/dist/react/{CacheSection-UCMptWyn.js → CacheSection-baMZotSn.js} +2 -2
  20. package/dist/react/CacheTab-2cw_rMzj.js +117 -0
  21. package/dist/react/{ConfigSection-DfFd-WRq.js → ConfigSection-DGgqjAal.js} +1 -1
  22. package/dist/react/{ConfigTab-Bdg8YMer.js → ConfigTab-H3OnYqmK.js} +1 -1
  23. package/dist/react/CustomPaneTab-B6r7ha0u.js +98 -0
  24. package/dist/react/{EmailsSection-CM7stSyh.js → EmailsSection-C-UZISG-.js} +2 -2
  25. package/dist/react/EmailsTab-DbK4Eobn.js +139 -0
  26. package/dist/react/{EventsSection-ByQ-9blq.js → EventsSection-C7RQW_LY.js} +2 -2
  27. package/dist/react/EventsTab-CfVr7AiM.js +57 -0
  28. package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
  29. package/dist/react/{JobsSection-DF3qEv9O.js → JobsSection-CQHNK_Ls.js} +2 -2
  30. package/dist/react/{JobsTab-BbrBWIOb.js → JobsTab-znzf6jzk.js} +54 -42
  31. package/dist/react/{LogsSection-DcFTZY7b.js → LogsSection-Dmm3rE2B.js} +9 -3
  32. package/dist/react/LogsTab-D8unMV5P.js +108 -0
  33. package/dist/react/{OverviewSection-C4T1ur51.js → OverviewSection-ABP9ueBo.js} +1 -1
  34. package/dist/react/{QueriesSection-PswteoF9.js → QueriesSection-CnmSkznA.js} +2 -2
  35. package/dist/react/{QueriesTab-osLUWd4L.js → QueriesTab-BQzcxEiW.js} +37 -40
  36. package/dist/react/{RelatedLogs-DFDOyUMr.js → RelatedLogs-3A8RuGKH.js} +15 -3
  37. package/dist/react/{RequestsSection-Nag30rEA.js → RequestsSection-kW79_M7k.js} +3 -3
  38. package/dist/react/{RoutesSection-BUSkM6PY.js → RoutesSection-BRhxrtjZ.js} +2 -2
  39. package/dist/react/RoutesTab-CpYH5lUw.js +68 -0
  40. package/dist/react/{TimelineTab-Covg5weo.js → TimelineTab-DjLR35Ce.js} +47 -53
  41. package/dist/react/index-CsImORX6.js +1121 -0
  42. package/dist/react/index.js +1 -1
  43. package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
  44. package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
  45. package/dist/react/style.css +1 -1
  46. package/dist/src/collectors/app_collector.d.ts +0 -8
  47. package/dist/src/collectors/app_collector.js +45 -52
  48. package/dist/src/collectors/auto_detect.d.ts +0 -23
  49. package/dist/src/collectors/auto_detect.js +33 -55
  50. package/dist/src/collectors/db_pool_collector.d.ts +14 -16
  51. package/dist/src/collectors/db_pool_collector.js +72 -57
  52. package/dist/src/collectors/log_collector.d.ts +0 -47
  53. package/dist/src/collectors/log_collector.js +36 -65
  54. package/dist/src/collectors/queue_collector.d.ts +0 -20
  55. package/dist/src/collectors/queue_collector.js +60 -76
  56. package/dist/src/collectors/redis_collector.d.ts +10 -10
  57. package/dist/src/collectors/redis_collector.js +69 -66
  58. package/dist/src/config/deprecation_migration.d.ts +7 -0
  59. package/dist/src/config/deprecation_migration.js +201 -0
  60. package/dist/src/controller/debug_controller.d.ts +1 -1
  61. package/dist/src/controller/debug_controller.js +87 -81
  62. package/dist/src/dashboard/cache_handlers.d.ts +14 -0
  63. package/dist/src/dashboard/cache_handlers.js +52 -0
  64. package/dist/src/dashboard/chart_aggregator.d.ts +0 -7
  65. package/dist/src/dashboard/chart_aggregator.js +68 -50
  66. package/dist/src/dashboard/coalesce_cache.d.ts +25 -0
  67. package/dist/src/dashboard/coalesce_cache.js +47 -0
  68. package/dist/src/dashboard/dashboard_controller.d.ts +11 -37
  69. package/dist/src/dashboard/dashboard_controller.js +51 -544
  70. package/dist/src/dashboard/dashboard_page_assets.d.ts +17 -0
  71. package/dist/src/dashboard/dashboard_page_assets.js +51 -0
  72. package/dist/src/dashboard/dashboard_store.d.ts +19 -218
  73. package/dist/src/dashboard/dashboard_store.js +115 -1116
  74. package/dist/src/dashboard/dashboard_types.d.ts +83 -0
  75. package/dist/src/dashboard/dashboard_types.js +4 -0
  76. package/dist/src/dashboard/detail_queries.d.ts +19 -0
  77. package/dist/src/dashboard/detail_queries.js +98 -0
  78. package/dist/src/dashboard/email_event_builder.d.ts +8 -0
  79. package/dist/src/dashboard/email_event_builder.js +65 -0
  80. package/dist/src/dashboard/explain_query.d.ts +8 -0
  81. package/dist/src/dashboard/explain_query.js +22 -0
  82. package/dist/src/dashboard/filter_handlers.d.ts +23 -0
  83. package/dist/src/dashboard/filter_handlers.js +56 -0
  84. package/dist/src/dashboard/filtered_queries.d.ts +15 -0
  85. package/dist/src/dashboard/filtered_queries.js +155 -0
  86. package/dist/src/dashboard/flush_manager.d.ts +25 -0
  87. package/dist/src/dashboard/flush_manager.js +107 -0
  88. package/dist/src/dashboard/format_helpers.d.ts +126 -0
  89. package/dist/src/dashboard/format_helpers.js +140 -0
  90. package/dist/src/dashboard/inspector_manager.d.ts +36 -0
  91. package/dist/src/dashboard/inspector_manager.js +102 -0
  92. package/dist/src/dashboard/integrations/config_inspector.js +11 -13
  93. package/dist/src/dashboard/integrations/queue_inspector.d.ts +3 -3
  94. package/dist/src/dashboard/integrations/queue_inspector.js +13 -10
  95. package/dist/src/dashboard/jobs_handlers.d.ts +14 -0
  96. package/dist/src/dashboard/jobs_handlers.js +61 -0
  97. package/dist/src/dashboard/knex_factory.d.ts +18 -0
  98. package/dist/src/dashboard/knex_factory.js +91 -0
  99. package/dist/src/dashboard/migrator.js +30 -159
  100. package/dist/src/dashboard/migrator_tables.d.ts +19 -0
  101. package/dist/src/dashboard/migrator_tables.js +153 -0
  102. package/dist/src/dashboard/overview_queries.d.ts +66 -0
  103. package/dist/src/dashboard/overview_queries.js +155 -0
  104. package/dist/src/dashboard/overview_query_runners.d.ts +25 -0
  105. package/dist/src/dashboard/overview_query_runners.js +84 -0
  106. package/dist/src/dashboard/overview_store_queries.d.ts +40 -0
  107. package/dist/src/dashboard/overview_store_queries.js +69 -0
  108. package/dist/src/dashboard/paginate_helper.d.ts +12 -0
  109. package/dist/src/dashboard/paginate_helper.js +33 -0
  110. package/dist/src/dashboard/query_explain_handler.d.ts +10 -0
  111. package/dist/src/dashboard/query_explain_handler.js +80 -0
  112. package/dist/src/dashboard/read_queries.d.ts +32 -0
  113. package/dist/src/dashboard/read_queries.js +107 -0
  114. package/dist/src/dashboard/saved_filter_queries.d.ts +10 -0
  115. package/dist/src/dashboard/saved_filter_queries.js +24 -0
  116. package/dist/src/dashboard/storage_stats.d.ts +41 -0
  117. package/dist/src/dashboard/storage_stats.js +81 -0
  118. package/dist/src/dashboard/write_queue.d.ts +106 -0
  119. package/dist/src/dashboard/write_queue.js +225 -0
  120. package/dist/src/data/data_access.d.ts +2 -39
  121. package/dist/src/data/data_access.js +17 -193
  122. package/dist/src/data/data_access_helpers.d.ts +130 -0
  123. package/dist/src/data/data_access_helpers.js +212 -0
  124. package/dist/src/debug/debug_store.js +37 -32
  125. package/dist/src/debug/email_collector.d.ts +1 -10
  126. package/dist/src/debug/email_collector.js +78 -81
  127. package/dist/src/debug/event_collector.d.ts +0 -9
  128. package/dist/src/debug/event_collector.js +79 -62
  129. package/dist/src/debug/query_collector.js +23 -19
  130. package/dist/src/debug/route_inspector.d.ts +1 -5
  131. package/dist/src/debug/route_inspector.js +50 -51
  132. package/dist/src/debug/trace_collector.d.ts +9 -1
  133. package/dist/src/debug/trace_collector.js +21 -15
  134. package/dist/src/debug/types.d.ts +1 -1
  135. package/dist/src/define_config.d.ts +0 -65
  136. package/dist/src/define_config.js +93 -333
  137. package/dist/src/edge/client/dashboard.js +2 -2
  138. package/dist/src/edge/client/debug-panel-deferred.js +1 -1
  139. package/dist/src/edge/client/stats-bar.js +1 -1
  140. package/dist/src/edge/client-vue/dashboard.js +5 -5
  141. package/dist/src/edge/client-vue/debug-panel-deferred.js +3 -3
  142. package/dist/src/edge/client-vue/stats-bar.js +3 -3
  143. package/dist/src/edge/plugin.d.ts +0 -16
  144. package/dist/src/edge/plugin.js +57 -64
  145. package/dist/src/engine/request_metrics.d.ts +1 -0
  146. package/dist/src/engine/request_metrics.js +32 -42
  147. package/dist/src/middleware/request_tracking_middleware.d.ts +2 -8
  148. package/dist/src/middleware/request_tracking_middleware.js +65 -93
  149. package/dist/src/provider/auth_middleware_detector.d.ts +16 -0
  150. package/dist/src/provider/auth_middleware_detector.js +97 -0
  151. package/dist/src/provider/boot_helpers.d.ts +20 -0
  152. package/dist/src/provider/boot_helpers.js +91 -0
  153. package/dist/src/provider/boot_initializer.d.ts +28 -0
  154. package/dist/src/provider/boot_initializer.js +35 -0
  155. package/dist/src/provider/dashboard_init.d.ts +30 -0
  156. package/dist/src/provider/dashboard_init.js +138 -0
  157. package/dist/src/provider/dashboard_setup.d.ts +25 -0
  158. package/dist/src/provider/dashboard_setup.js +78 -0
  159. package/dist/src/provider/diagnostics.d.ts +134 -0
  160. package/dist/src/provider/diagnostics.js +127 -0
  161. package/dist/src/provider/email_bridge.d.ts +43 -0
  162. package/dist/src/provider/email_bridge.js +80 -0
  163. package/dist/src/provider/email_helpers.d.ts +13 -0
  164. package/dist/src/provider/email_helpers.js +68 -0
  165. package/dist/src/provider/pino_hook.d.ts +17 -0
  166. package/dist/src/provider/pino_hook.js +35 -0
  167. package/dist/src/provider/provider_helpers_extra.d.ts +47 -0
  168. package/dist/src/provider/provider_helpers_extra.js +177 -0
  169. package/dist/src/provider/server_stats_provider.d.ts +39 -85
  170. package/dist/src/provider/server_stats_provider.js +132 -951
  171. package/dist/src/provider/shutdown_helpers.d.ts +43 -0
  172. package/dist/src/provider/shutdown_helpers.js +70 -0
  173. package/dist/src/provider/toolbar_setup.d.ts +57 -0
  174. package/dist/src/provider/toolbar_setup.js +141 -0
  175. package/dist/src/routes/dashboard_routes.d.ts +14 -0
  176. package/dist/src/routes/dashboard_routes.js +197 -0
  177. package/dist/src/routes/debug_routes.d.ts +14 -0
  178. package/dist/src/routes/debug_routes.js +101 -0
  179. package/dist/src/routes/register_routes.d.ts +0 -78
  180. package/dist/src/routes/register_routes.js +22 -352
  181. package/dist/src/routes/stats_routes.d.ts +5 -0
  182. package/dist/src/routes/stats_routes.js +14 -0
  183. package/dist/src/styles/components.css +96 -0
  184. package/dist/src/styles/dashboard.css +8 -90
  185. package/dist/src/styles/debug-panel.css +1 -31
  186. package/dist/src/types.d.ts +305 -14
  187. package/dist/vue/{CacheSection-oFAJL3mo.js → CacheSection-ITqvpfH5.js} +1 -1
  188. package/dist/vue/{ConfigSection-BhfJ4KqL.js → ConfigSection-DTn3GslE.js} +1 -1
  189. package/dist/vue/{EmailsSection-BcNyhyHs.js → EmailsSection-DtLJ4XoS.js} +1 -1
  190. package/dist/vue/{EventsSection-r60Q5Lmu.js → EventsSection-BOYYz0Ty.js} +1 -1
  191. package/dist/vue/{JobsSection-BHL-hkQw.js → JobsSection-BazTxcJL.js} +1 -1
  192. package/dist/vue/{LogsSection-DRMGzJmg.js → LogsSection-D55PjTKX.js} +9 -3
  193. package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
  194. package/dist/vue/{OverviewSection-CXh6Ja1B.js → OverviewSection-1uBKo-Tu.js} +1 -1
  195. package/dist/vue/{QueriesSection-IodIsCJ-.js → QueriesSection-rpoZ4ogd.js} +1 -1
  196. package/dist/vue/{RequestsSection-BPuMdmMc.js → RequestsSection-x7LvT0MC.js} +1 -1
  197. package/dist/vue/{RoutesSection-NKo3Rbq3.js → RoutesSection-CCD0zZqQ.js} +1 -1
  198. package/dist/vue/composables/useDashboardData.d.ts +12 -23
  199. package/dist/vue/index-C8MxnS7Q.js +1232 -0
  200. package/dist/vue/index.js +1 -1
  201. package/dist/vue/style.css +1 -1
  202. package/package.json +1 -1
  203. package/dist/react/CacheTab-CA8LB1J5.js +0 -123
  204. package/dist/react/CustomPaneTab-Bxtv_8Rw.js +0 -104
  205. package/dist/react/EmailsTab-BDhEiomM.js +0 -153
  206. package/dist/react/EventsTab-CMfY98Rl.js +0 -63
  207. package/dist/react/LogsTab-CicucmVk.js +0 -103
  208. package/dist/react/RoutesTab-DgVzd2PZ.js +0 -74
  209. package/dist/react/index-Cflz9Ebj.js +0 -1069
  210. package/dist/vue/index-Dtgysd26.js +0 -1229
@@ -0,0 +1,107 @@
1
+ import { log } from '../utils/logger.js';
2
+ import { prepareRequestRows, prepareLogRows, flushRequests, flushEvents, flushEmails, flushLogs, hasWarned, markWarned, } from './write_queue.js';
3
+ const FLUSH_MS = 500;
4
+ const MAX_Q = 200;
5
+ export class FlushManager {
6
+ writeQueue = [];
7
+ pendingEvents = [];
8
+ pendingLogs = [];
9
+ pendingEmails = [];
10
+ flushTimer = null;
11
+ flushing = false;
12
+ db;
13
+ constructor(getDb) {
14
+ this.db = getDb;
15
+ }
16
+ persistRequest(input, dashboardPath) {
17
+ if (!this.db() || input.url.startsWith(dashboardPath))
18
+ return;
19
+ this.backpressure(this.writeQueue);
20
+ this.writeQueue.push(input);
21
+ this.scheduleFlush();
22
+ }
23
+ queueEvents(requestIndex, events) {
24
+ if (events.length > 0)
25
+ this.pendingEvents.push({ requestIndex, events });
26
+ }
27
+ recordLog(entry) {
28
+ if (!this.db())
29
+ return;
30
+ this.backpressure(this.pendingLogs);
31
+ this.pendingLogs.push(entry);
32
+ this.scheduleFlush();
33
+ }
34
+ recordEmail(record) {
35
+ if (!this.db())
36
+ return;
37
+ this.backpressure(this.pendingEmails);
38
+ this.pendingEmails.push(record);
39
+ this.scheduleFlush();
40
+ }
41
+ async stop() {
42
+ if (this.flushTimer) {
43
+ clearTimeout(this.flushTimer);
44
+ this.flushTimer = null;
45
+ }
46
+ await this.flush().catch(() => { });
47
+ }
48
+ backpressure(q) {
49
+ if (q.length >= MAX_Q)
50
+ q.splice(0, Math.floor(MAX_Q / 4));
51
+ }
52
+ scheduleFlush() {
53
+ if (this.flushTimer)
54
+ return;
55
+ this.flushTimer = setTimeout(() => {
56
+ this.flushTimer = null;
57
+ this.flush().catch((err) => {
58
+ if (!hasWarned('flush')) {
59
+ markWarned('flush');
60
+ log.warn(`dashboard: flush failed — ${err?.message}`);
61
+ }
62
+ });
63
+ }, FLUSH_MS);
64
+ }
65
+ async flush() {
66
+ const db = this.db();
67
+ if (this.flushing || !db)
68
+ return;
69
+ this.flushing = true;
70
+ const snap = this.takeSnapshot();
71
+ if (!snap) {
72
+ this.flushing = false;
73
+ return;
74
+ }
75
+ try {
76
+ const pr = prepareRequestRows(snap.requests);
77
+ const pl = prepareLogRows(snap.logs);
78
+ await db.transaction(async (trx) => {
79
+ await flushRequests(trx, pr);
80
+ await flushEvents(trx, snap.events);
81
+ await flushEmails(trx, snap.emails);
82
+ await flushLogs(trx, pl);
83
+ });
84
+ }
85
+ catch (err) {
86
+ if (!hasWarned('flush')) {
87
+ markWarned('flush');
88
+ log.warn(`dashboard: flush transaction failed — ${err?.message}`);
89
+ }
90
+ }
91
+ finally {
92
+ this.flushing = false;
93
+ }
94
+ await new Promise((resolve) => setImmediate(resolve));
95
+ if (this.writeQueue.length > 0 || this.pendingLogs.length > 0 || this.pendingEmails.length > 0)
96
+ this.scheduleFlush();
97
+ }
98
+ takeSnapshot() {
99
+ const requests = this.writeQueue.splice(0);
100
+ const logs = this.pendingLogs.splice(0);
101
+ const events = this.pendingEvents.splice(0);
102
+ const emails = this.pendingEmails.splice(0);
103
+ if (requests.length === 0 && logs.length === 0 && events.length === 0 && emails.length === 0)
104
+ return null;
105
+ return { requests, logs, events, emails };
106
+ }
107
+ }
@@ -0,0 +1,126 @@
1
+ export interface PaginatedResponse<T> {
2
+ data: T[];
3
+ meta: {
4
+ total: number;
5
+ page: number;
6
+ perPage: number;
7
+ lastPage: number;
8
+ };
9
+ }
10
+ export interface ChartBucket {
11
+ bucket: string;
12
+ requestCount: number;
13
+ avgDuration: number;
14
+ p95Duration: number;
15
+ errorCount: number;
16
+ queryCount: number;
17
+ }
18
+ export declare function paginatedResponse<T>(data: T[], total: number, page: number, perPage: number): PaginatedResponse<T>;
19
+ export declare function emptyPage<T>(page: number, perPage: number): PaginatedResponse<T>;
20
+ export declare function formatRequest(r: Record<string, unknown>): {
21
+ httpRequestId?: {} | undefined;
22
+ id: unknown;
23
+ method: unknown;
24
+ url: unknown;
25
+ statusCode: unknown;
26
+ duration: unknown;
27
+ spanCount: unknown;
28
+ warningCount: unknown;
29
+ createdAt: unknown;
30
+ };
31
+ export declare function formatQuery(q: Record<string, unknown>): {
32
+ id: unknown;
33
+ requestId: unknown;
34
+ sql: unknown;
35
+ sqlNormalized: unknown;
36
+ bindings: unknown;
37
+ duration: unknown;
38
+ method: unknown;
39
+ model: unknown;
40
+ connection: unknown;
41
+ inTransaction: boolean;
42
+ createdAt: unknown;
43
+ };
44
+ export declare function formatTrace(t: Record<string, unknown>): {
45
+ httpRequestId?: {} | undefined;
46
+ id: unknown;
47
+ requestId: unknown;
48
+ method: unknown;
49
+ url: unknown;
50
+ statusCode: unknown;
51
+ totalDuration: unknown;
52
+ spanCount: unknown;
53
+ spans: {};
54
+ warnings: unknown[];
55
+ createdAt: unknown;
56
+ };
57
+ export declare function formatLog(l: Record<string, unknown>): {
58
+ id: unknown;
59
+ level: unknown;
60
+ message: unknown;
61
+ requestId: unknown;
62
+ data: unknown;
63
+ createdAt: unknown;
64
+ };
65
+ export declare function mapChartBucket(b: Record<string, unknown>): ChartBucket;
66
+ export declare function emptyOverview(): {
67
+ avgResponseTime: number;
68
+ p95ResponseTime: number;
69
+ requestsPerMinute: number;
70
+ errorRate: number;
71
+ sparklines: {
72
+ avgResponseTime: never[];
73
+ p95ResponseTime: never[];
74
+ requestsPerMinute: never[];
75
+ errorRate: never[];
76
+ };
77
+ slowestEndpoints: never[];
78
+ queryStats: {
79
+ total: number;
80
+ avgDuration: number;
81
+ perRequest: number;
82
+ };
83
+ recentErrors: never[];
84
+ topEvents: never[];
85
+ emailActivity: {
86
+ sent: number;
87
+ queued: number;
88
+ failed: number;
89
+ };
90
+ logLevelBreakdown: {
91
+ error: number;
92
+ warn: number;
93
+ info: number;
94
+ debug: number;
95
+ };
96
+ statusDistribution: {
97
+ '2xx': number;
98
+ '3xx': number;
99
+ '4xx': number;
100
+ '5xx': number;
101
+ };
102
+ slowestQueries: never[];
103
+ cacheStats: null;
104
+ jobQueueStatus: null;
105
+ };
106
+ /** Build sparklines from raw sparkline data. */
107
+ export declare function buildSparklines(data: Record<string, unknown>[]): {
108
+ avgResponseTime: unknown[];
109
+ p95ResponseTime: unknown[];
110
+ requestsPerMinute: unknown[];
111
+ errorRate: number[];
112
+ };
113
+ /** Format grouped query results. */
114
+ export declare function formatGroupedQuery(g: Record<string, unknown>, totalTime: number): {
115
+ sqlNormalized: unknown;
116
+ count: unknown;
117
+ avgDuration: number;
118
+ minDuration: number;
119
+ maxDuration: number;
120
+ totalDuration: number;
121
+ percentOfTotal: number;
122
+ };
123
+ /** Run EXPLAIN on a query, returning the plan. */
124
+ export declare function runExplain(appDb: {
125
+ raw: (sql: string, bindings: unknown[]) => Promise<Record<string, unknown>>;
126
+ }, query: Record<string, unknown>): Promise<unknown[]>;
@@ -0,0 +1,140 @@
1
+ import { safeParseJson, safeParseJsonArray } from '../utils/json_helpers.js';
2
+ export function paginatedResponse(data, total, page, perPage) {
3
+ return {
4
+ data,
5
+ meta: { total, page, perPage, lastPage: Math.max(1, Math.ceil(total / perPage)) },
6
+ };
7
+ }
8
+ export function emptyPage(page, perPage) {
9
+ return { data: [], meta: { total: 0, page, perPage, lastPage: 1 } };
10
+ }
11
+ export function formatRequest(r) {
12
+ return {
13
+ id: r.id,
14
+ method: r.method,
15
+ url: r.url,
16
+ statusCode: r.status_code,
17
+ duration: r.duration,
18
+ spanCount: r.span_count,
19
+ warningCount: r.warning_count,
20
+ createdAt: r.created_at,
21
+ ...(r.http_request_id ? { httpRequestId: r.http_request_id } : {}),
22
+ };
23
+ }
24
+ export function formatQuery(q) {
25
+ return {
26
+ id: q.id,
27
+ requestId: q.request_id,
28
+ sql: q.sql_text,
29
+ sqlNormalized: q.sql_normalized,
30
+ bindings: safeParseJson(q.bindings),
31
+ duration: q.duration,
32
+ method: q.method,
33
+ model: q.model,
34
+ connection: q.connection,
35
+ inTransaction: !!q.in_transaction,
36
+ createdAt: q.created_at,
37
+ };
38
+ }
39
+ export function formatTrace(t) {
40
+ return {
41
+ id: t.id,
42
+ requestId: t.request_id,
43
+ method: t.method,
44
+ url: t.url,
45
+ statusCode: t.status_code,
46
+ totalDuration: t.total_duration,
47
+ spanCount: t.span_count,
48
+ spans: safeParseJson(t.spans) ?? [],
49
+ warnings: safeParseJsonArray(t.warnings),
50
+ createdAt: t.created_at,
51
+ ...(t.http_request_id ? { httpRequestId: t.http_request_id } : {}),
52
+ };
53
+ }
54
+ export function formatLog(l) {
55
+ return {
56
+ id: l.id,
57
+ level: l.level,
58
+ message: l.message,
59
+ requestId: l.request_id,
60
+ data: l.data,
61
+ createdAt: l.created_at,
62
+ };
63
+ }
64
+ export function mapChartBucket(b) {
65
+ return {
66
+ bucket: b.bucket,
67
+ requestCount: b.request_count,
68
+ avgDuration: b.avg_duration,
69
+ p95Duration: b.p95_duration,
70
+ errorCount: b.error_count,
71
+ queryCount: b.query_count,
72
+ };
73
+ }
74
+ export function emptyOverview() {
75
+ return {
76
+ avgResponseTime: 0,
77
+ p95ResponseTime: 0,
78
+ requestsPerMinute: 0,
79
+ errorRate: 0,
80
+ sparklines: {
81
+ avgResponseTime: [],
82
+ p95ResponseTime: [],
83
+ requestsPerMinute: [],
84
+ errorRate: [],
85
+ },
86
+ slowestEndpoints: [],
87
+ queryStats: { total: 0, avgDuration: 0, perRequest: 0 },
88
+ recentErrors: [],
89
+ topEvents: [],
90
+ emailActivity: { sent: 0, queued: 0, failed: 0 },
91
+ logLevelBreakdown: { error: 0, warn: 0, info: 0, debug: 0 },
92
+ statusDistribution: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
93
+ slowestQueries: [],
94
+ cacheStats: null,
95
+ jobQueueStatus: null,
96
+ };
97
+ }
98
+ import { round } from '../utils/math_helpers.js';
99
+ /** Build sparklines from raw sparkline data. */
100
+ export function buildSparklines(data) {
101
+ return {
102
+ avgResponseTime: data.map((m) => m.avg_duration),
103
+ p95ResponseTime: data.map((m) => m.p95_duration),
104
+ requestsPerMinute: data.map((m) => m.request_count),
105
+ errorRate: data.map((m) => m.request_count > 0
106
+ ? round((m.error_count / m.request_count) * 100)
107
+ : 0),
108
+ };
109
+ }
110
+ /** Format grouped query results. */
111
+ export function formatGroupedQuery(g, totalTime) {
112
+ return {
113
+ sqlNormalized: g.sql_normalized,
114
+ count: g.count,
115
+ avgDuration: round(g.avg_duration),
116
+ minDuration: round(g.min_duration),
117
+ maxDuration: round(g.max_duration),
118
+ totalDuration: round(g.total_duration),
119
+ percentOfTotal: totalTime > 0 ? round((g.total_duration / totalTime) * 100) : 0,
120
+ };
121
+ }
122
+ /** Run EXPLAIN on a query, returning the plan. */
123
+ export async function runExplain(appDb, query) {
124
+ let bindings = [];
125
+ if (query.bindings) {
126
+ try {
127
+ bindings = JSON.parse(query.bindings);
128
+ }
129
+ catch {
130
+ /* skip */
131
+ }
132
+ }
133
+ const explainResult = await appDb.raw(`EXPLAIN (FORMAT JSON) ${query.sql_text}`, bindings);
134
+ const rawRows = explainResult?.rows ??
135
+ (Array.isArray(explainResult) ? explainResult : []);
136
+ if (rawRows.length > 0 && rawRows[0]['QUERY PLAN']) {
137
+ return rawRows[0]['QUERY PLAN'];
138
+ }
139
+ return rawRows;
140
+ }
@@ -0,0 +1,36 @@
1
+ import { CacheInspector } from './integrations/cache_inspector.js';
2
+ import { QueueInspector } from './integrations/queue_inspector.js';
3
+ import type { ApplicationService } from '@adonisjs/core/types';
4
+ /**
5
+ * Manages lazy initialization and availability detection for
6
+ * cache (Redis) and queue (BullMQ) inspectors.
7
+ *
8
+ * Extracted from DashboardController to reduce file size and complexity.
9
+ */
10
+ export declare class InspectorManager {
11
+ private app;
12
+ private cacheInspector;
13
+ private queueInspector;
14
+ private cacheAvailable;
15
+ private queueAvailable;
16
+ constructor(app: ApplicationService);
17
+ /** Lazy-init the cache inspector. Returns null if Redis is unavailable. */
18
+ getCacheInspector(): Promise<CacheInspector | null>;
19
+ /** Lazy-init the queue inspector. Returns null if BullMQ is unavailable. */
20
+ getQueueInspector(): Promise<QueueInspector | null>;
21
+ /** Fetch cache overview stats for the overview page. */
22
+ fetchCacheOverview(): Promise<{
23
+ available: boolean;
24
+ totalKeys: number;
25
+ hitRate: number;
26
+ memoryUsedHuman: string;
27
+ } | null>;
28
+ /** Fetch queue overview stats for the overview page. */
29
+ fetchQueueOverview(): Promise<{
30
+ available: boolean;
31
+ active: number;
32
+ waiting: number;
33
+ failed: number;
34
+ completed: number;
35
+ } | null>;
36
+ }
@@ -0,0 +1,102 @@
1
+ import { log } from '../utils/logger.js';
2
+ import { CacheInspector } from './integrations/cache_inspector.js';
3
+ import { QueueInspector } from './integrations/queue_inspector.js';
4
+ /**
5
+ * Manages lazy initialization and availability detection for
6
+ * cache (Redis) and queue (BullMQ) inspectors.
7
+ *
8
+ * Extracted from DashboardController to reduce file size and complexity.
9
+ */
10
+ export class InspectorManager {
11
+ app;
12
+ cacheInspector = null;
13
+ queueInspector = null;
14
+ cacheAvailable = null;
15
+ queueAvailable = null;
16
+ constructor(app) {
17
+ this.app = app;
18
+ }
19
+ /** Lazy-init the cache inspector. Returns null if Redis is unavailable. */
20
+ async getCacheInspector() {
21
+ if (this.cacheAvailable === false)
22
+ return null;
23
+ if (this.cacheInspector)
24
+ return this.cacheInspector;
25
+ try {
26
+ const available = await CacheInspector.isAvailable(this.app);
27
+ this.cacheAvailable = available;
28
+ if (!available) {
29
+ log.info('dashboard: Redis not detected — Cache panel disabled');
30
+ return null;
31
+ }
32
+ const redis = await this.app.container.make('redis');
33
+ this.cacheInspector = new CacheInspector(redis);
34
+ return this.cacheInspector;
35
+ }
36
+ catch (err) {
37
+ this.cacheAvailable = false;
38
+ log.warn('dashboard: CacheInspector init failed — ' + err?.message);
39
+ return null;
40
+ }
41
+ }
42
+ /** Lazy-init the queue inspector. Returns null if BullMQ is unavailable. */
43
+ async getQueueInspector() {
44
+ if (this.queueAvailable === false)
45
+ return null;
46
+ if (this.queueInspector)
47
+ return this.queueInspector;
48
+ try {
49
+ const available = await QueueInspector.isAvailable(this.app);
50
+ this.queueAvailable = available;
51
+ if (!available) {
52
+ log.info('dashboard: Queue not detected — Jobs panel disabled');
53
+ return null;
54
+ }
55
+ const queue = await this.app.container.make('rlanz/queue');
56
+ this.queueInspector = new QueueInspector(queue);
57
+ return this.queueInspector;
58
+ }
59
+ catch (err) {
60
+ this.queueAvailable = false;
61
+ log.warn('dashboard: QueueInspector init failed — ' + err?.message);
62
+ return null;
63
+ }
64
+ }
65
+ /** Fetch cache overview stats for the overview page. */
66
+ async fetchCacheOverview() {
67
+ try {
68
+ const inspector = await this.getCacheInspector();
69
+ if (!inspector)
70
+ return null;
71
+ const stats = await inspector.getStats();
72
+ return {
73
+ available: true,
74
+ totalKeys: stats.totalKeys,
75
+ hitRate: stats.hitRate,
76
+ memoryUsedHuman: stats.memoryUsedHuman,
77
+ };
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ }
83
+ /** Fetch queue overview stats for the overview page. */
84
+ async fetchQueueOverview() {
85
+ try {
86
+ const inspector = await this.getQueueInspector();
87
+ if (!inspector)
88
+ return null;
89
+ const overview = await inspector.getOverview();
90
+ return {
91
+ available: true,
92
+ active: overview.active,
93
+ waiting: overview.waiting,
94
+ failed: overview.failed,
95
+ completed: overview.completed,
96
+ };
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ }
@@ -134,6 +134,16 @@ function isSensitiveValue(value) {
134
134
  return true;
135
135
  return false;
136
136
  }
137
+ /** Sanitize a single key-value pair, redacting sensitive strings. */
138
+ function sanitizeValue(key, value, seen) {
139
+ if (typeof value === 'string' && (isSensitiveKey(key) || isSensitiveValue(value))) {
140
+ return redact(value);
141
+ }
142
+ if (typeof value === 'object' && value !== null) {
143
+ return sanitizeObject(value, seen);
144
+ }
145
+ return value;
146
+ }
137
147
  /**
138
148
  * Recursively sanitize an object, redacting string values whose keys
139
149
  * match sensitive patterns. Booleans and numbers are never redacted.
@@ -141,10 +151,8 @@ function isSensitiveValue(value) {
141
151
  function sanitizeObject(obj, seen = new WeakSet()) {
142
152
  if (obj === null || obj === undefined)
143
153
  return obj;
144
- // Primitive types pass through
145
154
  if (typeof obj !== 'object')
146
155
  return obj;
147
- // Avoid circular references
148
156
  if (seen.has(obj))
149
157
  return '[Circular]';
150
158
  seen.add(obj);
@@ -154,17 +162,7 @@ function sanitizeObject(obj, seen = new WeakSet()) {
154
162
  const record = obj;
155
163
  const result = {};
156
164
  for (const key of Object.keys(record)) {
157
- const value = record[key];
158
- if (typeof value === 'string' && (isSensitiveKey(key) || isSensitiveValue(value))) {
159
- result[key] = redact(value);
160
- }
161
- else if (typeof value === 'object' && value !== null) {
162
- result[key] = sanitizeObject(value, seen);
163
- }
164
- else {
165
- // Booleans, numbers, and non-sensitive strings pass through
166
- result[key] = value;
167
- }
165
+ result[key] = sanitizeValue(key, record[key], seen);
168
166
  }
169
167
  return result;
170
168
  }
@@ -142,9 +142,9 @@ export declare class QueueInspector {
142
142
  * the class-style name from the filename.
143
143
  */
144
144
  private static cleanJobName;
145
- /**
146
- * Format a Bull job into our summary shape.
147
- */
145
+ /** Extract timing fields from a BullMQ job. */
146
+ private static extractTimings;
147
+ /** Format a Bull job into our summary shape. */
148
148
  private formatJobSummary;
149
149
  }
150
150
  export {};
@@ -199,14 +199,17 @@ export class QueueInspector {
199
199
  }
200
200
  return raw;
201
201
  }
202
- /**
203
- * Format a Bull job into our summary shape.
204
- */
205
- formatJobSummary(job, status) {
202
+ /** Extract timing fields from a BullMQ job. */
203
+ static extractTimings(job) {
206
204
  const processedAt = job.processedOn ?? null;
207
205
  const finishedAt = job.finishedOn ?? null;
208
- const duration = processedAt !== null && finishedAt !== null ? finishedAt - processedAt : null;
209
206
  const createdAt = job.timestamp ?? 0;
207
+ const duration = processedAt !== null && finishedAt !== null ? finishedAt - processedAt : null;
208
+ return { processedAt, finishedAt, createdAt, duration };
209
+ }
210
+ /** Format a Bull job into our summary shape. */
211
+ formatJobSummary(job, status) {
212
+ const timings = QueueInspector.extractTimings(job);
210
213
  const data = job.data ?? null;
211
214
  return {
212
215
  id: String(job.id),
@@ -218,11 +221,11 @@ export class QueueInspector {
218
221
  maxAttempts: job.opts?.attempts ?? 1,
219
222
  progress: job.progress ?? 0,
220
223
  failedReason: job.failedReason ?? null,
221
- createdAt,
222
- timestamp: createdAt,
223
- processedAt,
224
- finishedAt,
225
- duration,
224
+ createdAt: timings.createdAt,
225
+ timestamp: timings.createdAt,
226
+ processedAt: timings.processedAt,
227
+ finishedAt: timings.finishedAt,
228
+ duration: timings.duration,
226
229
  };
227
230
  }
228
231
  }
@@ -0,0 +1,14 @@
1
+ import type { InspectorManager } from './inspector_manager.js';
2
+ import type { HttpContext } from '@adonisjs/core/http';
3
+ /**
4
+ * Handle GET /jobs — list jobs with optional status filter and search.
5
+ */
6
+ export declare function handleJobs(inspectors: InspectorManager, { request, response }: HttpContext): Promise<void>;
7
+ /**
8
+ * Handle GET /jobs/:id — get a single job's details.
9
+ */
10
+ export declare function handleJobDetail(inspectors: InspectorManager, { params, response }: HttpContext): Promise<void>;
11
+ /**
12
+ * Handle POST /jobs/:id/retry — retry a failed job.
13
+ */
14
+ export declare function handleJobRetry(inspectors: InspectorManager, { params, response }: HttpContext): Promise<void>;
@@ -0,0 +1,61 @@
1
+ import { clamp } from '../utils/math_helpers.js';
2
+ const EMPTY_JOBS = { available: false, overview: null, stats: null, jobs: [], total: 0 };
3
+ /**
4
+ * Handle GET /jobs — list jobs with optional status filter and search.
5
+ */
6
+ export async function handleJobs(inspectors, { request, response }) {
7
+ const inspector = await inspectors.getQueueInspector();
8
+ if (!inspector)
9
+ return response.json(EMPTY_JOBS);
10
+ const qs = request.qs();
11
+ const page = Math.max(1, Number(qs.page) || 1);
12
+ const perPage = clamp(Number(qs.perPage) || Number(qs.limit) || 25, 1, 100);
13
+ try {
14
+ const [overview, jobList] = await Promise.all([
15
+ inspector.getOverview(),
16
+ inspector.listJobs(qs.status || 'all', page, perPage),
17
+ ]);
18
+ let jobs = jobList.jobs;
19
+ let total = jobList.total;
20
+ if (qs.search) {
21
+ const term = qs.search.toLowerCase();
22
+ jobs = jobs.filter((j) => j.name?.toLowerCase().includes(term) || j.id?.toString().toLowerCase().includes(term));
23
+ total = jobs.length;
24
+ }
25
+ return response.json({ available: true, overview, stats: overview, jobs, total, page, perPage });
26
+ }
27
+ catch {
28
+ return response.json(EMPTY_JOBS);
29
+ }
30
+ }
31
+ /**
32
+ * Handle GET /jobs/:id — get a single job's details.
33
+ */
34
+ export async function handleJobDetail(inspectors, { params, response }) {
35
+ const inspector = await inspectors.getQueueInspector();
36
+ if (!inspector)
37
+ return response.notFound({ error: 'Queue not available' });
38
+ try {
39
+ const detail = await inspector.getJob(String(params.id));
40
+ return detail ? response.json(detail) : response.notFound({ error: 'Job not found' });
41
+ }
42
+ catch {
43
+ return response.notFound({ error: 'Job not found' });
44
+ }
45
+ }
46
+ /**
47
+ * Handle POST /jobs/:id/retry — retry a failed job.
48
+ */
49
+ export async function handleJobRetry(inspectors, { params, response }) {
50
+ const inspector = await inspectors.getQueueInspector();
51
+ if (!inspector)
52
+ return response.notFound({ error: 'Queue not available' });
53
+ try {
54
+ return (await inspector.retryJob(String(params.id)))
55
+ ? response.json({ success: true })
56
+ : response.badRequest({ error: 'Job could not be retried (not in failed state)' });
57
+ }
58
+ catch {
59
+ return response.internalServerError({ error: 'Retry failed' });
60
+ }
61
+ }