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.
- package/README.md +23 -14
- package/dist/core/config-utils.d.ts +8 -0
- package/dist/core/constants.d.ts +4 -0
- package/dist/core/dashboard-data-controller.d.ts +16 -0
- package/dist/core/dashboard-data-helpers.d.ts +12 -0
- package/dist/core/debug-data-controller.d.ts +4 -0
- package/dist/core/define-config-helpers.d.ts +25 -0
- package/dist/core/feature-detect-helpers.d.ts +36 -0
- package/dist/core/formatters-helpers.d.ts +23 -0
- package/dist/core/index.js +594 -509
- package/dist/core/log-utils-helpers.d.ts +13 -0
- package/dist/core/metrics.d.ts +3 -28
- package/dist/core/pagination.d.ts +0 -9
- package/dist/core/server-stats-controller.d.ts +6 -0
- package/dist/core/transmit-helpers.d.ts +7 -0
- package/dist/core/types-dashboard.d.ts +178 -0
- package/dist/core/types-diagnostics.d.ts +85 -0
- package/dist/core/types.d.ts +10 -442
- package/dist/react/{CacheSection-UCMptWyn.js → CacheSection-baMZotSn.js} +2 -2
- package/dist/react/CacheTab-2cw_rMzj.js +117 -0
- package/dist/react/{ConfigSection-DfFd-WRq.js → ConfigSection-DGgqjAal.js} +1 -1
- package/dist/react/{ConfigTab-Bdg8YMer.js → ConfigTab-H3OnYqmK.js} +1 -1
- package/dist/react/CustomPaneTab-B6r7ha0u.js +98 -0
- package/dist/react/{EmailsSection-CM7stSyh.js → EmailsSection-C-UZISG-.js} +2 -2
- package/dist/react/EmailsTab-DbK4Eobn.js +139 -0
- package/dist/react/{EventsSection-ByQ-9blq.js → EventsSection-C7RQW_LY.js} +2 -2
- package/dist/react/EventsTab-CfVr7AiM.js +57 -0
- package/dist/react/{FilterBar-DQRXpWrb.js → FilterBar-CQ7bD669.js} +15 -15
- package/dist/react/{JobsSection-DF3qEv9O.js → JobsSection-CQHNK_Ls.js} +2 -2
- package/dist/react/{JobsTab-BbrBWIOb.js → JobsTab-znzf6jzk.js} +54 -42
- package/dist/react/{LogsSection-DcFTZY7b.js → LogsSection-Dmm3rE2B.js} +9 -3
- package/dist/react/LogsTab-D8unMV5P.js +108 -0
- package/dist/react/{OverviewSection-C4T1ur51.js → OverviewSection-ABP9ueBo.js} +1 -1
- package/dist/react/{QueriesSection-PswteoF9.js → QueriesSection-CnmSkznA.js} +2 -2
- package/dist/react/{QueriesTab-osLUWd4L.js → QueriesTab-BQzcxEiW.js} +37 -40
- package/dist/react/{RelatedLogs-DFDOyUMr.js → RelatedLogs-3A8RuGKH.js} +15 -3
- package/dist/react/{RequestsSection-Nag30rEA.js → RequestsSection-kW79_M7k.js} +3 -3
- package/dist/react/{RoutesSection-BUSkM6PY.js → RoutesSection-BRhxrtjZ.js} +2 -2
- package/dist/react/RoutesTab-CpYH5lUw.js +68 -0
- package/dist/react/{TimelineTab-Covg5weo.js → TimelineTab-DjLR35Ce.js} +47 -53
- package/dist/react/index-CsImORX6.js +1121 -0
- package/dist/react/index.js +1 -1
- package/dist/react/react/components/{Dashboard/shared → shared}/FilterBar.d.ts +4 -3
- package/dist/react/react/hooks/useDashboardData.d.ts +4 -8
- package/dist/react/style.css +1 -1
- package/dist/src/collectors/app_collector.d.ts +0 -8
- package/dist/src/collectors/app_collector.js +45 -52
- package/dist/src/collectors/auto_detect.d.ts +0 -23
- package/dist/src/collectors/auto_detect.js +33 -55
- package/dist/src/collectors/db_pool_collector.d.ts +14 -16
- package/dist/src/collectors/db_pool_collector.js +72 -57
- package/dist/src/collectors/log_collector.d.ts +0 -47
- package/dist/src/collectors/log_collector.js +36 -65
- package/dist/src/collectors/queue_collector.d.ts +0 -20
- package/dist/src/collectors/queue_collector.js +60 -76
- package/dist/src/collectors/redis_collector.d.ts +10 -10
- package/dist/src/collectors/redis_collector.js +69 -66
- package/dist/src/config/deprecation_migration.d.ts +7 -0
- package/dist/src/config/deprecation_migration.js +201 -0
- package/dist/src/controller/debug_controller.d.ts +1 -1
- package/dist/src/controller/debug_controller.js +87 -81
- package/dist/src/dashboard/cache_handlers.d.ts +14 -0
- package/dist/src/dashboard/cache_handlers.js +52 -0
- package/dist/src/dashboard/chart_aggregator.d.ts +0 -7
- package/dist/src/dashboard/chart_aggregator.js +68 -50
- package/dist/src/dashboard/coalesce_cache.d.ts +25 -0
- package/dist/src/dashboard/coalesce_cache.js +47 -0
- package/dist/src/dashboard/dashboard_controller.d.ts +11 -37
- package/dist/src/dashboard/dashboard_controller.js +51 -544
- package/dist/src/dashboard/dashboard_page_assets.d.ts +17 -0
- package/dist/src/dashboard/dashboard_page_assets.js +51 -0
- package/dist/src/dashboard/dashboard_store.d.ts +19 -218
- package/dist/src/dashboard/dashboard_store.js +115 -1116
- package/dist/src/dashboard/dashboard_types.d.ts +83 -0
- package/dist/src/dashboard/dashboard_types.js +4 -0
- package/dist/src/dashboard/detail_queries.d.ts +19 -0
- package/dist/src/dashboard/detail_queries.js +98 -0
- package/dist/src/dashboard/email_event_builder.d.ts +8 -0
- package/dist/src/dashboard/email_event_builder.js +65 -0
- package/dist/src/dashboard/explain_query.d.ts +8 -0
- package/dist/src/dashboard/explain_query.js +22 -0
- package/dist/src/dashboard/filter_handlers.d.ts +23 -0
- package/dist/src/dashboard/filter_handlers.js +56 -0
- package/dist/src/dashboard/filtered_queries.d.ts +15 -0
- package/dist/src/dashboard/filtered_queries.js +155 -0
- package/dist/src/dashboard/flush_manager.d.ts +25 -0
- package/dist/src/dashboard/flush_manager.js +107 -0
- package/dist/src/dashboard/format_helpers.d.ts +126 -0
- package/dist/src/dashboard/format_helpers.js +140 -0
- package/dist/src/dashboard/inspector_manager.d.ts +36 -0
- package/dist/src/dashboard/inspector_manager.js +102 -0
- package/dist/src/dashboard/integrations/config_inspector.js +11 -13
- package/dist/src/dashboard/integrations/queue_inspector.d.ts +3 -3
- package/dist/src/dashboard/integrations/queue_inspector.js +13 -10
- package/dist/src/dashboard/jobs_handlers.d.ts +14 -0
- package/dist/src/dashboard/jobs_handlers.js +61 -0
- package/dist/src/dashboard/knex_factory.d.ts +18 -0
- package/dist/src/dashboard/knex_factory.js +91 -0
- package/dist/src/dashboard/migrator.js +30 -159
- package/dist/src/dashboard/migrator_tables.d.ts +19 -0
- package/dist/src/dashboard/migrator_tables.js +153 -0
- package/dist/src/dashboard/overview_queries.d.ts +66 -0
- package/dist/src/dashboard/overview_queries.js +155 -0
- package/dist/src/dashboard/overview_query_runners.d.ts +25 -0
- package/dist/src/dashboard/overview_query_runners.js +84 -0
- package/dist/src/dashboard/overview_store_queries.d.ts +40 -0
- package/dist/src/dashboard/overview_store_queries.js +69 -0
- package/dist/src/dashboard/paginate_helper.d.ts +12 -0
- package/dist/src/dashboard/paginate_helper.js +33 -0
- package/dist/src/dashboard/query_explain_handler.d.ts +10 -0
- package/dist/src/dashboard/query_explain_handler.js +80 -0
- package/dist/src/dashboard/read_queries.d.ts +32 -0
- package/dist/src/dashboard/read_queries.js +107 -0
- package/dist/src/dashboard/saved_filter_queries.d.ts +10 -0
- package/dist/src/dashboard/saved_filter_queries.js +24 -0
- package/dist/src/dashboard/storage_stats.d.ts +41 -0
- package/dist/src/dashboard/storage_stats.js +81 -0
- package/dist/src/dashboard/write_queue.d.ts +106 -0
- package/dist/src/dashboard/write_queue.js +225 -0
- package/dist/src/data/data_access.d.ts +2 -39
- package/dist/src/data/data_access.js +17 -193
- package/dist/src/data/data_access_helpers.d.ts +130 -0
- package/dist/src/data/data_access_helpers.js +212 -0
- package/dist/src/debug/debug_store.js +37 -32
- package/dist/src/debug/email_collector.d.ts +1 -10
- package/dist/src/debug/email_collector.js +78 -81
- package/dist/src/debug/event_collector.d.ts +0 -9
- package/dist/src/debug/event_collector.js +79 -62
- package/dist/src/debug/query_collector.js +23 -19
- package/dist/src/debug/route_inspector.d.ts +1 -5
- package/dist/src/debug/route_inspector.js +50 -51
- package/dist/src/debug/trace_collector.d.ts +9 -1
- package/dist/src/debug/trace_collector.js +21 -15
- package/dist/src/debug/types.d.ts +1 -1
- package/dist/src/define_config.d.ts +0 -65
- package/dist/src/define_config.js +93 -333
- package/dist/src/edge/client/dashboard.js +2 -2
- package/dist/src/edge/client/debug-panel-deferred.js +1 -1
- package/dist/src/edge/client/stats-bar.js +1 -1
- package/dist/src/edge/client-vue/dashboard.js +5 -5
- package/dist/src/edge/client-vue/debug-panel-deferred.js +3 -3
- package/dist/src/edge/client-vue/stats-bar.js +3 -3
- package/dist/src/edge/plugin.d.ts +0 -16
- package/dist/src/edge/plugin.js +57 -64
- package/dist/src/engine/request_metrics.d.ts +1 -0
- package/dist/src/engine/request_metrics.js +32 -42
- package/dist/src/middleware/request_tracking_middleware.d.ts +2 -8
- package/dist/src/middleware/request_tracking_middleware.js +65 -93
- package/dist/src/provider/auth_middleware_detector.d.ts +16 -0
- package/dist/src/provider/auth_middleware_detector.js +97 -0
- package/dist/src/provider/boot_helpers.d.ts +20 -0
- package/dist/src/provider/boot_helpers.js +91 -0
- package/dist/src/provider/boot_initializer.d.ts +28 -0
- package/dist/src/provider/boot_initializer.js +35 -0
- package/dist/src/provider/dashboard_init.d.ts +30 -0
- package/dist/src/provider/dashboard_init.js +138 -0
- package/dist/src/provider/dashboard_setup.d.ts +25 -0
- package/dist/src/provider/dashboard_setup.js +78 -0
- package/dist/src/provider/diagnostics.d.ts +134 -0
- package/dist/src/provider/diagnostics.js +127 -0
- package/dist/src/provider/email_bridge.d.ts +43 -0
- package/dist/src/provider/email_bridge.js +80 -0
- package/dist/src/provider/email_helpers.d.ts +13 -0
- package/dist/src/provider/email_helpers.js +68 -0
- package/dist/src/provider/pino_hook.d.ts +17 -0
- package/dist/src/provider/pino_hook.js +35 -0
- package/dist/src/provider/provider_helpers_extra.d.ts +47 -0
- package/dist/src/provider/provider_helpers_extra.js +177 -0
- package/dist/src/provider/server_stats_provider.d.ts +39 -85
- package/dist/src/provider/server_stats_provider.js +132 -951
- package/dist/src/provider/shutdown_helpers.d.ts +43 -0
- package/dist/src/provider/shutdown_helpers.js +70 -0
- package/dist/src/provider/toolbar_setup.d.ts +57 -0
- package/dist/src/provider/toolbar_setup.js +141 -0
- package/dist/src/routes/dashboard_routes.d.ts +14 -0
- package/dist/src/routes/dashboard_routes.js +197 -0
- package/dist/src/routes/debug_routes.d.ts +14 -0
- package/dist/src/routes/debug_routes.js +101 -0
- package/dist/src/routes/register_routes.d.ts +0 -78
- package/dist/src/routes/register_routes.js +22 -352
- package/dist/src/routes/stats_routes.d.ts +5 -0
- package/dist/src/routes/stats_routes.js +14 -0
- package/dist/src/styles/components.css +96 -0
- package/dist/src/styles/dashboard.css +8 -90
- package/dist/src/styles/debug-panel.css +1 -31
- package/dist/src/types.d.ts +305 -14
- package/dist/vue/{CacheSection-oFAJL3mo.js → CacheSection-ITqvpfH5.js} +1 -1
- package/dist/vue/{ConfigSection-BhfJ4KqL.js → ConfigSection-DTn3GslE.js} +1 -1
- package/dist/vue/{EmailsSection-BcNyhyHs.js → EmailsSection-DtLJ4XoS.js} +1 -1
- package/dist/vue/{EventsSection-r60Q5Lmu.js → EventsSection-BOYYz0Ty.js} +1 -1
- package/dist/vue/{JobsSection-BHL-hkQw.js → JobsSection-BazTxcJL.js} +1 -1
- package/dist/vue/{LogsSection-DRMGzJmg.js → LogsSection-D55PjTKX.js} +9 -3
- package/dist/vue/{LogsTab-Bg3o0Mm6.js → LogsTab-47zEK7jL.js} +4 -1
- package/dist/vue/{OverviewSection-CXh6Ja1B.js → OverviewSection-1uBKo-Tu.js} +1 -1
- package/dist/vue/{QueriesSection-IodIsCJ-.js → QueriesSection-rpoZ4ogd.js} +1 -1
- package/dist/vue/{RequestsSection-BPuMdmMc.js → RequestsSection-x7LvT0MC.js} +1 -1
- package/dist/vue/{RoutesSection-NKo3Rbq3.js → RoutesSection-CCD0zZqQ.js} +1 -1
- package/dist/vue/composables/useDashboardData.d.ts +12 -23
- package/dist/vue/index-C8MxnS7Q.js +1232 -0
- package/dist/vue/index.js +1 -1
- package/dist/vue/style.css +1 -1
- package/package.json +1 -1
- package/dist/react/CacheTab-CA8LB1J5.js +0 -123
- package/dist/react/CustomPaneTab-Bxtv_8Rw.js +0 -104
- package/dist/react/EmailsTab-BDhEiomM.js +0 -153
- package/dist/react/EventsTab-CMfY98Rl.js +0 -63
- package/dist/react/LogsTab-CicucmVk.js +0 -103
- package/dist/react/RoutesTab-DgVzd2PZ.js +0 -74
- package/dist/react/index-Cflz9Ebj.js +0 -1069
- package/dist/vue/index-Dtgysd26.js +0 -1229
|
@@ -6,6 +6,37 @@ import { EventCollector } from './event_collector.js';
|
|
|
6
6
|
import { QueryCollector } from './query_collector.js';
|
|
7
7
|
import { RouteInspector } from './route_inspector.js';
|
|
8
8
|
import { TraceCollector } from './trace_collector.js';
|
|
9
|
+
/** Read and parse persisted debug data from disk. Returns null on failure. */
|
|
10
|
+
async function readPersistedData(filePath) {
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = await readFile(filePath, 'utf-8');
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error?.code !== 'ENOENT') {
|
|
17
|
+
log.warn(`Failed to read persisted debug data from ${bold(filePath)}: ${error?.message}`);
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(raw);
|
|
23
|
+
if (typeof data !== 'object' || data === null)
|
|
24
|
+
return null;
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
log.warn(`Persisted debug data corrupted, resetting: ${bold(filePath)}`);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Load an array from the record into a collector if present and non-empty. */
|
|
33
|
+
function loadIfPresent(record, key, loader) {
|
|
34
|
+
if (!loader)
|
|
35
|
+
return;
|
|
36
|
+
if (Array.isArray(record[key]) && record[key].length > 0) {
|
|
37
|
+
loader.loadRecords(record[key]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
9
40
|
/**
|
|
10
41
|
* Singleton store holding all debug data collectors.
|
|
11
42
|
* Bound to the AdonisJS container as `debug.store`.
|
|
@@ -88,38 +119,12 @@ export class DebugStore {
|
|
|
88
119
|
}
|
|
89
120
|
/** Restore collector data from a JSON file on disk. */
|
|
90
121
|
async loadFromDisk(filePath) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
raw = await readFile(filePath, 'utf-8');
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
if (error?.code !== 'ENOENT') {
|
|
97
|
-
log.warn(`Failed to read persisted debug data from ${bold(filePath)}: ${error?.message}`);
|
|
98
|
-
}
|
|
122
|
+
const record = await readPersistedData(filePath);
|
|
123
|
+
if (!record)
|
|
99
124
|
return;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
log.warn(`Persisted debug data corrupted, resetting: ${bold(filePath)}`);
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (typeof data !== 'object' || data === null)
|
|
110
|
-
return;
|
|
111
|
-
const record = data;
|
|
112
|
-
if (Array.isArray(record.queries) && record.queries.length > 0) {
|
|
113
|
-
this.queries.loadRecords(record.queries);
|
|
114
|
-
}
|
|
115
|
-
if (Array.isArray(record.events) && record.events.length > 0) {
|
|
116
|
-
this.events.loadRecords(record.events);
|
|
117
|
-
}
|
|
118
|
-
if (Array.isArray(record.emails) && record.emails.length > 0) {
|
|
119
|
-
this.emails.loadRecords(record.emails);
|
|
120
|
-
}
|
|
121
|
-
if (this.traces && Array.isArray(record.traces) && record.traces.length > 0) {
|
|
122
|
-
this.traces.loadRecords(record.traces);
|
|
123
|
-
}
|
|
125
|
+
loadIfPresent(record, 'queries', this.queries);
|
|
126
|
+
loadIfPresent(record, 'events', this.events);
|
|
127
|
+
loadIfPresent(record, 'emails', this.emails);
|
|
128
|
+
loadIfPresent(record, 'traces', this.traces);
|
|
124
129
|
}
|
|
125
130
|
}
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import type { EmailRecord, Emitter } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Listens to AdonisJS mail events and stores captured emails in a ring buffer.
|
|
4
|
-
*
|
|
5
|
-
* Events:
|
|
6
|
-
* - `mail:sending` — email about to be sent
|
|
7
|
-
* - `mail:sent` — email successfully sent (updates matching 'sending' record)
|
|
8
|
-
* - `mail:queueing` — email about to be queued (sendLater)
|
|
9
|
-
* - `mail:queued` — email successfully queued (updates matching 'queueing' record)
|
|
10
|
-
* - `queued:mail:error` — queued email failed
|
|
11
4
|
*/
|
|
12
5
|
export declare class EmailCollector {
|
|
13
|
-
private
|
|
6
|
+
#private;
|
|
14
7
|
private buffer;
|
|
15
8
|
private emitter;
|
|
16
9
|
private handlers;
|
|
@@ -26,8 +19,6 @@ export declare class EmailCollector {
|
|
|
26
19
|
max: number;
|
|
27
20
|
};
|
|
28
21
|
clear(): void;
|
|
29
|
-
private buildRecord;
|
|
30
|
-
private capSize;
|
|
31
22
|
/**
|
|
32
23
|
* Push an externally-created email record into the buffer.
|
|
33
24
|
*
|
|
@@ -1,17 +1,52 @@
|
|
|
1
1
|
import { extractAddresses } from '../utils/mail_helpers.js';
|
|
2
2
|
import { RingBuffer } from './ring_buffer.js';
|
|
3
|
+
const MAX_HTML_SIZE = 50_000;
|
|
4
|
+
/** Extract the message object from the event data. */
|
|
5
|
+
function extractMessage(data) {
|
|
6
|
+
return data?.message || data;
|
|
7
|
+
}
|
|
8
|
+
/** Extract message ID from event data. */
|
|
9
|
+
function extractMessageId(data) {
|
|
10
|
+
return data?.response?.messageId || data?.messageId || null;
|
|
11
|
+
}
|
|
12
|
+
/** Truncate large HTML/text bodies to prevent memory bloat. */
|
|
13
|
+
function capSize(value) {
|
|
14
|
+
if (!value)
|
|
15
|
+
return null;
|
|
16
|
+
if (value.length <= MAX_HTML_SIZE)
|
|
17
|
+
return value;
|
|
18
|
+
return value.slice(0, MAX_HTML_SIZE) + '\n<!-- truncated -->';
|
|
19
|
+
}
|
|
20
|
+
/** Resolve an address field to a string, defaulting when empty. */
|
|
21
|
+
function resolveAddress(field, fallback) {
|
|
22
|
+
return extractAddresses(field) || fallback;
|
|
23
|
+
}
|
|
24
|
+
/** Resolve the mailer name from event data. */
|
|
25
|
+
function resolveMailer(data) {
|
|
26
|
+
return data?.mailerName || data?.mailer || 'unknown';
|
|
27
|
+
}
|
|
28
|
+
/** Count attachments safely. */
|
|
29
|
+
function countAttachments(msg) {
|
|
30
|
+
return Array.isArray(msg?.attachments) ? msg.attachments.length : 0;
|
|
31
|
+
}
|
|
32
|
+
/** Build the core fields of an email record from a mail message. */
|
|
33
|
+
function buildRecordFields(msg, data) {
|
|
34
|
+
return {
|
|
35
|
+
from: resolveAddress(msg?.from, 'unknown'),
|
|
36
|
+
to: resolveAddress(msg?.to, 'unknown'),
|
|
37
|
+
cc: resolveAddress(msg?.cc, null),
|
|
38
|
+
bcc: resolveAddress(msg?.bcc, null),
|
|
39
|
+
subject: msg?.subject || '(no subject)',
|
|
40
|
+
html: capSize(msg?.html),
|
|
41
|
+
text: capSize(msg?.text),
|
|
42
|
+
mailer: resolveMailer(data),
|
|
43
|
+
attachmentCount: countAttachments(msg),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
3
46
|
/**
|
|
4
47
|
* Listens to AdonisJS mail events and stores captured emails in a ring buffer.
|
|
5
|
-
*
|
|
6
|
-
* Events:
|
|
7
|
-
* - `mail:sending` — email about to be sent
|
|
8
|
-
* - `mail:sent` — email successfully sent (updates matching 'sending' record)
|
|
9
|
-
* - `mail:queueing` — email about to be queued (sendLater)
|
|
10
|
-
* - `mail:queued` — email successfully queued (updates matching 'queueing' record)
|
|
11
|
-
* - `queued:mail:error` — queued email failed
|
|
12
48
|
*/
|
|
13
49
|
export class EmailCollector {
|
|
14
|
-
static MAX_HTML_SIZE = 50_000; // 50 KB cap per email body
|
|
15
50
|
buffer;
|
|
16
51
|
emitter = null;
|
|
17
52
|
handlers = [];
|
|
@@ -22,61 +57,47 @@ export class EmailCollector {
|
|
|
22
57
|
if (!emitter || typeof emitter.on !== 'function')
|
|
23
58
|
return;
|
|
24
59
|
this.emitter = emitter;
|
|
25
|
-
const onSending = (data) => {
|
|
26
|
-
const msg = data?.message || data;
|
|
27
|
-
const record = this.buildRecord(msg, 'sending', data);
|
|
28
|
-
this.buffer.push(record);
|
|
29
|
-
};
|
|
30
|
-
const onSent = (data) => {
|
|
31
|
-
const msg = data?.message || data;
|
|
32
|
-
const to = extractAddresses(msg?.to);
|
|
33
|
-
const subject = msg?.subject || '';
|
|
34
|
-
// Try to find the matching 'sending' record and update it (no buffer copy)
|
|
35
|
-
const match = this.buffer.findFromEnd((rec) => rec.status === 'sending' && rec.to === to && rec.subject === subject);
|
|
36
|
-
if (match) {
|
|
37
|
-
match.status = 'sent';
|
|
38
|
-
match.messageId = data?.response?.messageId || data?.messageId || null;
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
// No matching 'sending' record — insert a new 'sent' record
|
|
42
|
-
const record = this.buildRecord(msg, 'sent', data);
|
|
43
|
-
record.messageId = data?.response?.messageId || data?.messageId || null;
|
|
44
|
-
this.buffer.push(record);
|
|
45
|
-
};
|
|
46
|
-
const onQueueing = (data) => {
|
|
47
|
-
const msg = data?.message || data;
|
|
48
|
-
const record = this.buildRecord(msg, 'queueing', data);
|
|
49
|
-
this.buffer.push(record);
|
|
50
|
-
};
|
|
51
|
-
const onQueued = (data) => {
|
|
52
|
-
const msg = data?.message || data;
|
|
53
|
-
const to = extractAddresses(msg?.to);
|
|
54
|
-
const subject = msg?.subject || '';
|
|
55
|
-
// Try to find the matching 'queueing' record and update it
|
|
56
|
-
const match = this.buffer.findFromEnd((rec) => rec.status === 'queueing' && rec.to === to && rec.subject === subject);
|
|
57
|
-
if (match) {
|
|
58
|
-
match.status = 'queued';
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const record = this.buildRecord(msg, 'queued', data);
|
|
62
|
-
this.buffer.push(record);
|
|
63
|
-
};
|
|
64
|
-
const onQueuedError = (data) => {
|
|
65
|
-
const msg = data?.message || data;
|
|
66
|
-
const record = this.buildRecord(msg, 'failed', data);
|
|
67
|
-
this.buffer.push(record);
|
|
68
|
-
};
|
|
69
60
|
this.handlers = [
|
|
70
|
-
{ event: 'mail:sending', fn:
|
|
71
|
-
{ event: 'mail:sent', fn:
|
|
72
|
-
{ event: 'mail:queueing', fn:
|
|
73
|
-
{ event: 'mail:queued', fn:
|
|
74
|
-
{ event: 'queued:mail:error', fn:
|
|
61
|
+
{ event: 'mail:sending', fn: (data) => this.#onSimpleEvent(data, 'sending') },
|
|
62
|
+
{ event: 'mail:sent', fn: (data) => this.#onTransitionEvent(data, 'sending', 'sent') },
|
|
63
|
+
{ event: 'mail:queueing', fn: (data) => this.#onSimpleEvent(data, 'queueing') },
|
|
64
|
+
{ event: 'mail:queued', fn: (data) => this.#onTransitionEvent(data, 'queueing', 'queued') },
|
|
65
|
+
{ event: 'queued:mail:error', fn: (data) => this.#onSimpleEvent(data, 'failed') },
|
|
75
66
|
];
|
|
76
67
|
for (const h of this.handlers) {
|
|
77
68
|
emitter.on(h.event, h.fn);
|
|
78
69
|
}
|
|
79
70
|
}
|
|
71
|
+
/** Handle events that simply push a new record. */
|
|
72
|
+
#onSimpleEvent(data, status) {
|
|
73
|
+
const msg = extractMessage(data);
|
|
74
|
+
const record = this.#buildRecord(msg, status, data);
|
|
75
|
+
this.buffer.push(record);
|
|
76
|
+
}
|
|
77
|
+
/** Handle events that update an existing record or create a new one. */
|
|
78
|
+
#onTransitionEvent(data, fromStatus, toStatus) {
|
|
79
|
+
const msg = extractMessage(data);
|
|
80
|
+
const to = extractAddresses(msg?.to);
|
|
81
|
+
const subject = msg?.subject || '';
|
|
82
|
+
const match = this.buffer.findFromEnd((rec) => rec.status === fromStatus && rec.to === to && rec.subject === subject);
|
|
83
|
+
if (match) {
|
|
84
|
+
match.status = toStatus;
|
|
85
|
+
match.messageId = extractMessageId(data);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const record = this.#buildRecord(msg, toStatus, data);
|
|
89
|
+
record.messageId = extractMessageId(data);
|
|
90
|
+
this.buffer.push(record);
|
|
91
|
+
}
|
|
92
|
+
#buildRecord(msg, status, data) {
|
|
93
|
+
return {
|
|
94
|
+
id: this.buffer.getNextId(),
|
|
95
|
+
...buildRecordFields(msg, data),
|
|
96
|
+
status,
|
|
97
|
+
messageId: null,
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
80
101
|
stop() {
|
|
81
102
|
if (this.emitter && typeof this.emitter.off === 'function') {
|
|
82
103
|
for (const h of this.handlers) {
|
|
@@ -105,30 +126,6 @@ export class EmailCollector {
|
|
|
105
126
|
clear() {
|
|
106
127
|
this.buffer.clear();
|
|
107
128
|
}
|
|
108
|
-
buildRecord(msg, status, data) {
|
|
109
|
-
return {
|
|
110
|
-
id: this.buffer.getNextId(),
|
|
111
|
-
from: extractAddresses(msg?.from) || 'unknown',
|
|
112
|
-
to: extractAddresses(msg?.to) || 'unknown',
|
|
113
|
-
cc: extractAddresses(msg?.cc) || null,
|
|
114
|
-
bcc: extractAddresses(msg?.bcc) || null,
|
|
115
|
-
subject: msg?.subject || '(no subject)',
|
|
116
|
-
html: this.capSize(msg?.html),
|
|
117
|
-
text: this.capSize(msg?.text),
|
|
118
|
-
mailer: data?.mailerName || data?.mailer || 'unknown',
|
|
119
|
-
status,
|
|
120
|
-
messageId: null,
|
|
121
|
-
attachmentCount: Array.isArray(msg?.attachments) ? msg.attachments.length : 0,
|
|
122
|
-
timestamp: Date.now(),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
capSize(value) {
|
|
126
|
-
if (!value)
|
|
127
|
-
return null;
|
|
128
|
-
if (value.length <= EmailCollector.MAX_HTML_SIZE)
|
|
129
|
-
return value;
|
|
130
|
-
return value.slice(0, EmailCollector.MAX_HTML_SIZE) + '\n<!-- truncated -->';
|
|
131
|
-
}
|
|
132
129
|
/**
|
|
133
130
|
* Push an externally-created email record into the buffer.
|
|
134
131
|
*
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { EventRecord, Emitter } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Wraps the AdonisJS emitter to log all events with timestamps.
|
|
4
|
-
* Uses monkey-patching of emitter.emit to intercept all events.
|
|
5
4
|
*/
|
|
6
5
|
export declare class EventCollector {
|
|
7
6
|
private buffer;
|
|
@@ -10,14 +9,8 @@ export declare class EventCollector {
|
|
|
10
9
|
constructor(maxEvents?: number);
|
|
11
10
|
start(emitter: Emitter): void;
|
|
12
11
|
stop(): void;
|
|
13
|
-
/** Reusable WeakSet to avoid GC churn on every event. */
|
|
14
12
|
private circulars;
|
|
15
13
|
private summarizeData;
|
|
16
|
-
/**
|
|
17
|
-
* Recursively limit object depth to prevent deeply-nested payloads
|
|
18
|
-
* from causing expensive serialization.
|
|
19
|
-
*/
|
|
20
|
-
private limitDepth;
|
|
21
14
|
getEvents(): EventRecord[];
|
|
22
15
|
getLatest(n?: number): EventRecord[];
|
|
23
16
|
getTotalCount(): number;
|
|
@@ -26,8 +19,6 @@ export declare class EventCollector {
|
|
|
26
19
|
max: number;
|
|
27
20
|
};
|
|
28
21
|
clear(): void;
|
|
29
|
-
/** Register a callback that fires whenever a new event is recorded. */
|
|
30
22
|
onNewItem(cb: ((item: EventRecord) => void) | null): void;
|
|
31
|
-
/** Restore persisted records into the buffer and reset the ID counter. */
|
|
32
23
|
loadRecords(records: EventRecord[]): void;
|
|
33
24
|
}
|
|
@@ -1,8 +1,81 @@
|
|
|
1
1
|
import { isExcludedRequest } from '../middleware/request_tracking_middleware.js';
|
|
2
2
|
import { RingBuffer } from './ring_buffer.js';
|
|
3
|
+
/** Events to skip (handled by other collectors or noisy). */
|
|
4
|
+
const SKIPPED_EVENTS = new Set([
|
|
5
|
+
'db:query',
|
|
6
|
+
'mail:sending',
|
|
7
|
+
'mail:sent',
|
|
8
|
+
'mail:queueing',
|
|
9
|
+
'mail:queued',
|
|
10
|
+
'queued:mail:error',
|
|
11
|
+
'http:request_completed',
|
|
12
|
+
]);
|
|
13
|
+
/** Check if an event should be recorded. */
|
|
14
|
+
function shouldRecord(eventName) {
|
|
15
|
+
if (eventName.startsWith('__'))
|
|
16
|
+
return false;
|
|
17
|
+
if (SKIPPED_EVENTS.has(eventName))
|
|
18
|
+
return false;
|
|
19
|
+
if (isExcludedRequest())
|
|
20
|
+
return false;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
/** Truncate a string to a max length. */
|
|
24
|
+
function truncateString(s, maxLen) {
|
|
25
|
+
return s.length > maxLen ? s.slice(0, maxLen) + '...' : s;
|
|
26
|
+
}
|
|
27
|
+
/** Fallback stringification when JSON.stringify fails. */
|
|
28
|
+
function fallbackStringify(data) {
|
|
29
|
+
if (typeof data === 'object' && data !== null) {
|
|
30
|
+
const ctorName = data.constructor?.name;
|
|
31
|
+
if (ctorName)
|
|
32
|
+
return `[${ctorName}]`;
|
|
33
|
+
}
|
|
34
|
+
return typeof data;
|
|
35
|
+
}
|
|
36
|
+
/** Limit depth of an array value. */
|
|
37
|
+
function limitDepthArray(value, maxDepth, seen) {
|
|
38
|
+
const take = Math.min(value.length, 20);
|
|
39
|
+
const arr = Array.from({ length: take });
|
|
40
|
+
for (let i = 0; i < take; i++) {
|
|
41
|
+
arr[i] = limitDepth(value[i], maxDepth - 1, seen);
|
|
42
|
+
}
|
|
43
|
+
return arr;
|
|
44
|
+
}
|
|
45
|
+
/** Limit depth of an object value. */
|
|
46
|
+
function limitDepthObject(value, maxDepth, seen) {
|
|
47
|
+
const keys = Object.keys(value);
|
|
48
|
+
const result = {};
|
|
49
|
+
const limit = Math.min(keys.length, 50);
|
|
50
|
+
for (let i = 0; i < limit; i++) {
|
|
51
|
+
result[keys[i]] = limitDepth(value[keys[i]], maxDepth - 1, seen);
|
|
52
|
+
}
|
|
53
|
+
if (keys.length > 50) {
|
|
54
|
+
result['...'] = `(${keys.length - 50} more keys)`;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
/** Recursively limit object depth to prevent expensive serialization. */
|
|
59
|
+
function limitDepth(value, maxDepth, seen) {
|
|
60
|
+
if (maxDepth <= 0)
|
|
61
|
+
return '[...]';
|
|
62
|
+
if (value === null || value === undefined)
|
|
63
|
+
return value;
|
|
64
|
+
if (typeof value === 'function')
|
|
65
|
+
return `[Function: ${value.name || 'anonymous'}]`;
|
|
66
|
+
if (typeof value === 'bigint')
|
|
67
|
+
return value.toString();
|
|
68
|
+
if (typeof value !== 'object')
|
|
69
|
+
return value;
|
|
70
|
+
if (seen.has(value))
|
|
71
|
+
return '[Circular]';
|
|
72
|
+
seen.add(value);
|
|
73
|
+
if (Array.isArray(value))
|
|
74
|
+
return limitDepthArray(value, maxDepth, seen);
|
|
75
|
+
return limitDepthObject(value, maxDepth, seen);
|
|
76
|
+
}
|
|
3
77
|
/**
|
|
4
78
|
* Wraps the AdonisJS emitter to log all events with timestamps.
|
|
5
|
-
* Uses monkey-patching of emitter.emit to intercept all events.
|
|
6
79
|
*/
|
|
7
80
|
export class EventCollector {
|
|
8
81
|
buffer;
|
|
@@ -17,17 +90,8 @@ export class EventCollector {
|
|
|
17
90
|
this.emitter = emitter;
|
|
18
91
|
this.originalEmit = emitter.emit.bind(emitter);
|
|
19
92
|
emitter.emit = (event, data) => {
|
|
20
|
-
// Resolve event name: class-based events use the class name, string events are used as-is
|
|
21
93
|
const eventName = typeof event === 'string' ? event : event?.name || 'unknown';
|
|
22
|
-
|
|
23
|
-
// HTTP lifecycle events (redundant with timeline), and events triggered
|
|
24
|
-
// by debug panel polling requests
|
|
25
|
-
if (!eventName.startsWith('__') &&
|
|
26
|
-
eventName !== 'db:query' &&
|
|
27
|
-
!eventName.startsWith('mail:') &&
|
|
28
|
-
eventName !== 'queued:mail:error' &&
|
|
29
|
-
eventName !== 'http:request_completed' &&
|
|
30
|
-
!isExcludedRequest()) {
|
|
94
|
+
if (shouldRecord(eventName)) {
|
|
31
95
|
const record = {
|
|
32
96
|
id: this.buffer.getNextId(),
|
|
33
97
|
event: eventName,
|
|
@@ -46,68 +110,23 @@ export class EventCollector {
|
|
|
46
110
|
this.originalEmit = null;
|
|
47
111
|
this.emitter = null;
|
|
48
112
|
}
|
|
49
|
-
/** Reusable WeakSet to avoid GC churn on every event. */
|
|
50
113
|
circulars = new WeakSet();
|
|
51
114
|
summarizeData(data) {
|
|
52
115
|
if (data === undefined || data === null)
|
|
53
116
|
return null;
|
|
54
117
|
try {
|
|
55
118
|
if (typeof data === 'string')
|
|
56
|
-
return data
|
|
119
|
+
return truncateString(data, 4096);
|
|
57
120
|
if (typeof data !== 'object')
|
|
58
121
|
return String(data);
|
|
59
|
-
// Reuse the WeakSet across calls to avoid per-event allocation
|
|
60
122
|
this.circulars = new WeakSet();
|
|
61
|
-
const limited =
|
|
62
|
-
// Compact JSON (no indent) to reduce string size and serialization time
|
|
123
|
+
const limited = limitDepth(data, 3, this.circulars);
|
|
63
124
|
const result = JSON.stringify(limited) ?? '';
|
|
64
|
-
return result
|
|
125
|
+
return truncateString(result, 4096);
|
|
65
126
|
}
|
|
66
127
|
catch {
|
|
67
|
-
|
|
68
|
-
const ctorName = data.constructor?.name;
|
|
69
|
-
if (ctorName)
|
|
70
|
-
return `[${ctorName}]`;
|
|
71
|
-
}
|
|
72
|
-
return typeof data;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Recursively limit object depth to prevent deeply-nested payloads
|
|
77
|
-
* from causing expensive serialization.
|
|
78
|
-
*/
|
|
79
|
-
limitDepth(value, maxDepth, seen) {
|
|
80
|
-
if (maxDepth <= 0)
|
|
81
|
-
return '[...]';
|
|
82
|
-
if (value === null || value === undefined)
|
|
83
|
-
return value;
|
|
84
|
-
if (typeof value === 'function')
|
|
85
|
-
return `[Function: ${value.name || 'anonymous'}]`;
|
|
86
|
-
if (typeof value === 'bigint')
|
|
87
|
-
return value.toString();
|
|
88
|
-
if (typeof value !== 'object')
|
|
89
|
-
return value;
|
|
90
|
-
if (seen.has(value))
|
|
91
|
-
return '[Circular]';
|
|
92
|
-
seen.add(value);
|
|
93
|
-
if (Array.isArray(value)) {
|
|
94
|
-
const take = Math.min(value.length, 20);
|
|
95
|
-
const arr = Array.from({ length: take });
|
|
96
|
-
for (let i = 0; i < take; i++) {
|
|
97
|
-
arr[i] = this.limitDepth(value[i], maxDepth - 1, seen);
|
|
98
|
-
}
|
|
99
|
-
return arr;
|
|
100
|
-
}
|
|
101
|
-
const keys = Object.keys(value);
|
|
102
|
-
const result = {};
|
|
103
|
-
const limit = Math.min(keys.length, 50);
|
|
104
|
-
for (let i = 0; i < limit; i++) {
|
|
105
|
-
result[keys[i]] = this.limitDepth(value[keys[i]], maxDepth - 1, seen);
|
|
106
|
-
}
|
|
107
|
-
if (keys.length > 50) {
|
|
108
|
-
result['...'] = `(${keys.length - 50} more keys)`;
|
|
128
|
+
return fallbackStringify(data);
|
|
109
129
|
}
|
|
110
|
-
return result;
|
|
111
130
|
}
|
|
112
131
|
getEvents() {
|
|
113
132
|
return this.buffer.toArray().reverse();
|
|
@@ -124,11 +143,9 @@ export class EventCollector {
|
|
|
124
143
|
clear() {
|
|
125
144
|
this.buffer.clear();
|
|
126
145
|
}
|
|
127
|
-
/** Register a callback that fires whenever a new event is recorded. */
|
|
128
146
|
onNewItem(cb) {
|
|
129
147
|
this.buffer.onPush(cb);
|
|
130
148
|
}
|
|
131
|
-
/** Restore persisted records into the buffer and reset the ID counter. */
|
|
132
149
|
loadRecords(records) {
|
|
133
150
|
this.buffer.load(records);
|
|
134
151
|
const maxId = records.reduce((m, r) => Math.max(m, r.id), 0);
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { isExcludedRequest } from '../middleware/request_tracking_middleware.js';
|
|
2
2
|
import { round } from '../utils/math_helpers.js';
|
|
3
3
|
import { RingBuffer } from './ring_buffer.js';
|
|
4
|
+
/** Parse duration from DbQueryEvent (may be number, hrtime tuple, or absent). */
|
|
5
|
+
function parseDuration(duration) {
|
|
6
|
+
if (typeof duration === 'number')
|
|
7
|
+
return duration;
|
|
8
|
+
if (Array.isArray(duration))
|
|
9
|
+
return duration[0] * 1e3 + duration[1] / 1e6;
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
/** Build a QueryRecord from a db:query event. */
|
|
13
|
+
function buildQueryRecord(data, id) {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
sql: data.sql || '',
|
|
17
|
+
bindings: data.bindings || [],
|
|
18
|
+
duration: round(parseDuration(data.duration)),
|
|
19
|
+
method: data.method || 'unknown',
|
|
20
|
+
model: data.model || null,
|
|
21
|
+
connection: data.connection || 'default',
|
|
22
|
+
inTransaction: data.inTransaction || false,
|
|
23
|
+
timestamp: Date.now(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
4
26
|
/**
|
|
5
27
|
* Listens to Lucid's `db:query` event and stores queries in a ring buffer.
|
|
6
28
|
*
|
|
@@ -21,29 +43,11 @@ export class QueryCollector {
|
|
|
21
43
|
async start(emitter) {
|
|
22
44
|
this.emitter = emitter;
|
|
23
45
|
this.handler = (data) => {
|
|
24
|
-
// Self-exclude: skip queries from the dashboard's dedicated SQLite connection
|
|
25
46
|
if (data.connection === 'server_stats')
|
|
26
47
|
return;
|
|
27
|
-
// Self-exclude: skip queries triggered by debug panel polling requests
|
|
28
48
|
if (isExcludedRequest())
|
|
29
49
|
return;
|
|
30
|
-
|
|
31
|
-
? data.duration
|
|
32
|
-
: Array.isArray(data.duration)
|
|
33
|
-
? data.duration[0] * 1e3 + data.duration[1] / 1e6
|
|
34
|
-
: 0;
|
|
35
|
-
const record = {
|
|
36
|
-
id: this.buffer.getNextId(),
|
|
37
|
-
sql: data.sql || '',
|
|
38
|
-
bindings: data.bindings || [],
|
|
39
|
-
duration: round(duration),
|
|
40
|
-
method: data.method || 'unknown',
|
|
41
|
-
model: data.model || null,
|
|
42
|
-
connection: data.connection || 'default',
|
|
43
|
-
inTransaction: data.inTransaction || false,
|
|
44
|
-
timestamp: Date.now(),
|
|
45
|
-
};
|
|
46
|
-
this.buffer.push(record);
|
|
50
|
+
this.buffer.push(buildQueryRecord(data, this.buffer.getNextId()));
|
|
47
51
|
};
|
|
48
52
|
if (emitter && typeof emitter.on === 'function') {
|
|
49
53
|
emitter.on('db:query', this.handler);
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import type { RouteRecord, Router } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Reads the router's route table at boot time and caches it.
|
|
4
|
-
* Static data computed once — routes don't change after boot.
|
|
5
4
|
*/
|
|
6
5
|
export declare class RouteInspector {
|
|
6
|
+
#private;
|
|
7
7
|
private routes;
|
|
8
|
-
/**
|
|
9
|
-
* Inspect the router and cache all routes.
|
|
10
|
-
* Call this in the provider's `ready()` hook.
|
|
11
|
-
*/
|
|
12
8
|
inspect(router: Router): void;
|
|
13
9
|
private resolveHandler;
|
|
14
10
|
private resolveMiddleware;
|