adonisjs-server-stats 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/dist/core/core/dashboard-api.d.ts +6 -4
- package/dist/core/index.js +9 -6
- package/dist/core/types.d.ts +24 -1
- package/dist/react/{CacheSection-DkLtukWz.js → CacheSection-C-X2Tnca.js} +48 -44
- package/dist/react/{CacheTab-DWeXnEBI.js → CacheTab-D55mfmeE.js} +1 -1
- package/dist/react/{ConfigSection-DaVbFPDg.js → ConfigSection-C-HGnZfd.js} +1 -1
- package/dist/react/{ConfigTab-D4640WJZ.js → ConfigTab-CNnsOXBh.js} +1 -1
- package/dist/react/{CustomPaneTab-DglmAVMC.js → CustomPaneTab-D-267UVd.js} +1 -1
- package/dist/react/{EmailsSection-vYx6ExTb.js → EmailsSection-Bqw4DI4r.js} +65 -57
- package/dist/react/{EmailsTab-DKDrRmRH.js → EmailsTab-CkHOhj2F.js} +1 -1
- package/dist/react/{EventsSection-CapH5xut.js → EventsSection-CMeyjzVC.js} +1 -1
- package/dist/react/{EventsTab-BNVEySAm.js → EventsTab-DgS2v1HX.js} +1 -1
- package/dist/react/{JobsSection-ZIQsJWid.js → JobsSection-oWH6pbrG.js} +1 -1
- package/dist/react/{JobsTab-DXQzXrDt.js → JobsTab-zdxX_as-.js} +1 -1
- package/dist/react/{LogEntryRow-BWkHE51-.js → LogEntryRow-Cn8Ee_X0.js} +1 -1
- package/dist/react/{LogsSection-ConXdBkL.js → LogsSection-XmiI2MMs.js} +2 -2
- package/dist/react/{LogsTab-CJM47LPn.js → LogsTab-ixusY4v7.js} +2 -2
- package/dist/react/{OverviewSection-DOMu2qvl.js → OverviewSection-CvhObTLP.js} +119 -111
- package/dist/react/{QueriesSection-DFsaOSJI.js → QueriesSection-BvledwBI.js} +1 -1
- package/dist/react/{QueriesTab-CY9CG_7L.js → QueriesTab-smQQJvJa.js} +1 -1
- package/dist/react/{RequestsSection-CC-eVHsl.js → RequestsSection-DKuYpViv.js} +2 -2
- package/dist/react/{RoutesSection-p1DMq41y.js → RoutesSection-5RNVurQ6.js} +1 -1
- package/dist/react/{RoutesTab-Cnqy8UcK.js → RoutesTab-CpXNO-Rx.js} +1 -1
- package/dist/react/{SplitPaneWrapper-XgkA0QxE.js → SplitPaneWrapper-DQvJnsuk.js} +1 -1
- package/dist/react/{TimelineTab-1YOERxe5.js → TimelineTab-LHQHHKkO.js} +2 -2
- package/dist/react/components/Dashboard/overview-data-context.d.ts +15 -0
- package/dist/react/components/Dashboard/sections/OverviewSection.d.ts +9 -0
- package/dist/react/{index-DOSlCpZ9.js → index-LlFvooUN.js} +148 -141
- package/dist/react/index.js +1 -1
- package/dist/src/collectors/app_collector.d.ts +11 -0
- package/dist/src/collectors/app_collector.js +11 -0
- package/dist/src/collectors/redis_collector.js +13 -13
- package/dist/src/dashboard/cache_handlers.d.ts +17 -3
- package/dist/src/dashboard/cache_handlers.js +14 -19
- package/dist/src/dashboard/coalesce_cache.d.ts +2 -8
- package/dist/src/dashboard/coalesce_cache.js +25 -0
- package/dist/src/dashboard/dashboard_controller.d.ts +1 -0
- package/dist/src/dashboard/dashboard_controller.js +8 -3
- package/dist/src/dashboard/dashboard_store.d.ts +1 -3
- package/dist/src/dashboard/dashboard_store.js +14 -9
- package/dist/src/dashboard/dashboard_types.d.ts +2 -0
- package/dist/src/dashboard/filter_handlers.js +26 -4
- package/dist/src/dashboard/flush_manager.d.ts +2 -6
- package/dist/src/dashboard/flush_manager.js +9 -11
- package/dist/src/dashboard/format_helpers.d.ts +0 -4
- package/dist/src/dashboard/format_helpers.js +0 -19
- package/dist/src/dashboard/integrations/config_inspector.js +15 -52
- package/dist/src/dashboard/migrator_tables.js +4 -0
- package/dist/src/dashboard/paginate_helper.js +21 -11
- package/dist/src/dashboard/sensitive_patterns.d.ts +76 -0
- package/dist/src/dashboard/sensitive_patterns.js +259 -0
- package/dist/src/dashboard/write_queue.d.ts +27 -16
- package/dist/src/dashboard/write_queue.js +50 -47
- package/dist/src/debug/event_collector.d.ts +8 -0
- package/dist/src/debug/event_collector.js +12 -0
- package/dist/src/debug/query_collector.js +6 -1
- package/dist/src/debug/trace_collector.js +26 -13
- package/dist/src/edge/client/dashboard.js +2 -2
- package/dist/src/edge/client-vue/dashboard.js +5 -5
- package/dist/src/edge/views/dashboard.edge +1 -1
- package/dist/src/edge/views/debug-panel.edge +1 -1
- package/dist/src/edge/views/stats-bar.edge +1 -1
- package/dist/src/middleware/request_tracking_middleware.js +7 -2
- package/dist/src/provider/dashboard_init.d.ts +9 -1
- package/dist/src/provider/dashboard_init.js +19 -0
- package/dist/src/provider/email_helpers.js +20 -5
- package/dist/src/provider/server_stats_provider.d.ts +9 -0
- package/dist/src/provider/server_stats_provider.js +26 -7
- package/dist/src/provider/shutdown_helpers.js +3 -1
- package/dist/src/provider/toolbar_setup.js +21 -15
- package/dist/src/types.d.ts +24 -1
- package/dist/vue/CacheSection-Bm0goqX6.js +161 -0
- package/dist/vue/{ConfigSection-BiRAiaHj.js → ConfigSection-BrYslIMN.js} +1 -1
- package/dist/vue/{EmailsSection-Dl44qyqY.js → EmailsSection-B8N5vaWA.js} +89 -85
- package/dist/vue/{EventsSection-CWjeitjU.js → EventsSection-D2Y_xt0m.js} +1 -1
- package/dist/vue/{JobsSection-DOBb4LjZ.js → JobsSection-cEKqELLC.js} +1 -1
- package/dist/vue/{LogsSection-CXx-HOWJ.js → LogsSection-C3H2c0S3.js} +1 -1
- package/dist/vue/{OverviewSection-CyfNQ8uV.js → OverviewSection-I2swuHDa.js} +1 -1
- package/dist/vue/{QueriesSection-CXBsFp-y.js → QueriesSection-CF1IE3SN.js} +1 -1
- package/dist/vue/{RequestsSection-YTIaZGZd.js → RequestsSection-CuwgXzmU.js} +1 -1
- package/dist/vue/{RoutesSection-CDKMey49.js → RoutesSection-BhJCF7Rn.js} +1 -1
- package/dist/vue/composables/useDashboardData.d.ts +1 -1
- package/dist/vue/{index-14x39RI_.js → index-DUsImuIN.js} +95 -95
- package/dist/vue/index.js +1 -1
- package/package.json +12 -3
- package/dist/src/dashboard/explain_query.d.ts +0 -8
- package/dist/src/dashboard/explain_query.js +0 -22
- package/dist/src/provider/boot_initializer.d.ts +0 -28
- package/dist/src/provider/boot_initializer.js +0 -35
- package/dist/vue/CacheSection-CxEBVVkF.js +0 -156
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
</head>
|
|
9
9
|
<body>
|
|
10
10
|
<div id="ss-dash"></div>
|
|
11
|
-
<script type="application/json" id="ss-dash-config">{{{ JSON.stringify(dashConfig) }}}</script>
|
|
11
|
+
<script type="application/json" id="ss-dash-config">{{{ JSON.stringify(dashConfig).replace(/</g, '\\u003c') }}}</script>
|
|
12
12
|
|
|
13
13
|
@if(transmitClient)
|
|
14
14
|
<script>
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
<div id="ss-dbg-panel"></div>
|
|
2
|
-
<script type="application/json" id="ss-dbg-config">{{{ JSON.stringify(debugConfig) }}}</script>
|
|
2
|
+
<script type="application/json" id="ss-dbg-config">{{{ JSON.stringify(debugConfig).replace(/</g, '\\u003c') }}}</script>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</style>
|
|
4
4
|
|
|
5
5
|
<div id="ss-bar"></div>
|
|
6
|
-
<script type="application/json" id="ss-bar-config">{{{ JSON.stringify(barConfig) }}}</script>
|
|
6
|
+
<script type="application/json" id="ss-bar-config">{{{ JSON.stringify(barConfig).replace(/</g, '\\u003c') }}}</script>
|
|
7
7
|
|
|
8
8
|
@if(showDebug)
|
|
9
9
|
<style>{{{ debugCss }}}</style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { performance } from 'node:perf_hooks';
|
|
3
3
|
import { getRequestMetrics } from '../collectors/http_collector.js';
|
|
4
|
+
import { sanitizeUrlQuery } from '../dashboard/sensitive_patterns.js';
|
|
4
5
|
import { log } from '../utils/logger.js';
|
|
5
6
|
const excludedRequestAls = new AsyncLocalStorage();
|
|
6
7
|
/** Returns true if the current async context is inside an excluded request. */
|
|
@@ -71,10 +72,14 @@ function recordRequestCompletion(opts) {
|
|
|
71
72
|
if (opts.skipTracing)
|
|
72
73
|
return;
|
|
73
74
|
const reqId = typeof opts.ctx.request.id === 'function' ? String(opts.ctx.request.id()) : undefined;
|
|
74
|
-
|
|
75
|
+
// Redact credential-bearing query params (reset tokens, signed URLs) before
|
|
76
|
+
// the URL enters any store — this is the single choke point for both the
|
|
77
|
+
// trace buffer and the persisted request row.
|
|
78
|
+
const storedUrl = sanitizeUrlQuery(opts.ctx.request.url(true));
|
|
79
|
+
const traceRecord = traceCollector?.finishTrace(opts.ctx.request.method(), storedUrl, opts.ctx.response.getStatus(), reqId);
|
|
75
80
|
onRequestCompleteFn?.({
|
|
76
81
|
method: opts.ctx.request.method(),
|
|
77
|
-
url:
|
|
82
|
+
url: storedUrl,
|
|
78
83
|
statusCode: opts.ctx.response.getStatus(),
|
|
79
84
|
duration,
|
|
80
85
|
trace: traceRecord ?? undefined,
|
|
@@ -9,7 +9,7 @@ import type { DebugStore } from '../debug/debug_store.js';
|
|
|
9
9
|
import type { DevToolbarConfig } from '../debug/types.js';
|
|
10
10
|
import type { StatsEngine } from '../engine/stats_engine.js';
|
|
11
11
|
import type { ApplicationService } from '@adonisjs/core/types';
|
|
12
|
-
interface DashboardResult {
|
|
12
|
+
export interface DashboardResult {
|
|
13
13
|
dashboardStore: DashboardStore | null;
|
|
14
14
|
dashboardController: DashboardController | null;
|
|
15
15
|
dashboardLogStream: LogStreamService | null;
|
|
@@ -27,4 +27,12 @@ interface DashboardStoreOptions {
|
|
|
27
27
|
}
|
|
28
28
|
/** Initialize the full-page dashboard: SQLite store, controller, log piping, broadcasting. */
|
|
29
29
|
export declare function initDashboardStore(opts: DashboardStoreOptions): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Forget the installed request pipe. Must be called on shutdown: the handler
|
|
32
|
+
* slot is nulled there via `setOnRequestComplete(null)`, and without this
|
|
33
|
+
* reset a later re-init in the same process (test harness, programmatic
|
|
34
|
+
* restart) would see the flag, decline to reinstall, and leave dashboard
|
|
35
|
+
* persistence silently dead for the rest of the process's life.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resetDashRequestPipe(): void;
|
|
30
38
|
export {};
|
|
@@ -110,6 +110,16 @@ function pipeDashLogs(pinoHookActive, dashboardStore, makePath) {
|
|
|
110
110
|
* dashboard request pipe, so accumulating handlers would just double-persist.
|
|
111
111
|
*/
|
|
112
112
|
let dashRequestPipeInstalled = false;
|
|
113
|
+
/**
|
|
114
|
+
* Forget the installed request pipe. Must be called on shutdown: the handler
|
|
115
|
+
* slot is nulled there via `setOnRequestComplete(null)`, and without this
|
|
116
|
+
* reset a later re-init in the same process (test harness, programmatic
|
|
117
|
+
* restart) would see the flag, decline to reinstall, and leave dashboard
|
|
118
|
+
* persistence silently dead for the rest of the process's life.
|
|
119
|
+
*/
|
|
120
|
+
export function resetDashRequestPipe() {
|
|
121
|
+
dashRequestPipeInstalled = false;
|
|
122
|
+
}
|
|
113
123
|
function pipeDashRequests(debugStore, dashboardStore) {
|
|
114
124
|
if (dashRequestPipeInstalled) {
|
|
115
125
|
log.warn('dashboard: request pipe already installed — ignoring duplicate registration ' +
|
|
@@ -118,18 +128,27 @@ function pipeDashRequests(debugStore, dashboardStore) {
|
|
|
118
128
|
}
|
|
119
129
|
dashRequestPipeInstalled = true;
|
|
120
130
|
let lastQueryId = 0;
|
|
131
|
+
let lastEventId = 0;
|
|
121
132
|
setOnRequestComplete(({ method, url, statusCode, duration, trace, httpRequestId }) => {
|
|
122
133
|
if (!dashboardStore.isReady())
|
|
123
134
|
return;
|
|
124
135
|
const q = debugStore.queries.getQueriesSince(lastQueryId);
|
|
125
136
|
if (q.length > 0)
|
|
126
137
|
lastQueryId = q[q.length - 1].id;
|
|
138
|
+
// Events are collected globally rather than per-request, so — exactly like
|
|
139
|
+
// queries above — anything emitted outside a request is attributed to
|
|
140
|
+
// whichever request finishes next. Imprecise, but it keeps events linked to
|
|
141
|
+
// a request row, which is what retention prunes on.
|
|
142
|
+
const events = debugStore.capture.events ? debugStore.events.getEventsSince(lastEventId) : [];
|
|
143
|
+
if (events.length > 0)
|
|
144
|
+
lastEventId = events[events.length - 1].id;
|
|
127
145
|
dashboardStore.persistRequest({
|
|
128
146
|
method,
|
|
129
147
|
url,
|
|
130
148
|
statusCode,
|
|
131
149
|
duration,
|
|
132
150
|
queries: q,
|
|
151
|
+
events,
|
|
133
152
|
trace: trace ?? null,
|
|
134
153
|
httpRequestId: httpRequestId ?? null,
|
|
135
154
|
});
|
|
@@ -3,13 +3,26 @@ const MAX_HTML = 50_000;
|
|
|
3
3
|
/**
|
|
4
4
|
* Sensitive query-parameter names whose values are stripped from captured
|
|
5
5
|
* URLs. These carry one-time login/reset/verify credentials.
|
|
6
|
+
*
|
|
7
|
+
* The name may carry a prefix (`reset_token=`, `resetToken=`, `id_token=`):
|
|
8
|
+
* a bare `\b` boundary cannot see those, because `_` is a word character and
|
|
9
|
+
* camelCase has no boundary at all. Over-matching a benign param that merely
|
|
10
|
+
* ends in a keyword (`postcode=`) costs a little readability; missing
|
|
11
|
+
* `reset_token=` costs a live credential.
|
|
6
12
|
*/
|
|
7
|
-
const SENSITIVE_QUERY_PARAMS =
|
|
13
|
+
const SENSITIVE_QUERY_PARAMS = /[\w-]*(?:token|code|api_key|apikey|key|secret|signature|sig|password|pwd|auth|otp|nonce|state)=[^\s&"'<>]*/gi;
|
|
8
14
|
/**
|
|
9
15
|
* URL path segments that indicate a credential-bearing link even when the
|
|
10
16
|
* secret is in the path rather than the query string (reset/verify/magic).
|
|
11
17
|
*/
|
|
12
|
-
const SENSITIVE_URL_PATHS = /(https?:\/\/[^\s"'<>]*?\/(?:reset|verify|verification|confirm|magic|magic-link|activate|invite|unsubscribe)[^\s"'<>]*)/gi;
|
|
18
|
+
const SENSITIVE_URL_PATHS = /(https?:\/\/[^\s"'<>]*?\/(?:reset|verify|verification|confirm|magic|magic-link|activate|invite|unsubscribe|(?:set-|new-)?password)[^\s"'<>]*)/gi;
|
|
19
|
+
const SENSITIVE_PATH_MARKER = /\/(reset|verify|verification|confirm|magic|magic-link|activate|invite|unsubscribe|(?:set-|new-)?password)/i;
|
|
20
|
+
/**
|
|
21
|
+
* A long opaque path segment (24+ unhyphenated word chars) inside a URL —
|
|
22
|
+
* the shape of a bare token in the path. Hyphens are excluded so ordinary
|
|
23
|
+
* hyphenated slugs in newsletters survive.
|
|
24
|
+
*/
|
|
25
|
+
const OPAQUE_PATH_SEGMENT = /(https?:\/\/[^\s"'<>]*?\/)([A-Za-z0-9_]{24,})(?=[/?#"'\s<>&]|$)/g;
|
|
13
26
|
/**
|
|
14
27
|
* Redact token-bearing links from an email body before persistence.
|
|
15
28
|
*
|
|
@@ -19,13 +32,15 @@ const SENSITIVE_URL_PATHS = /(https?:\/\/[^\s"'<>]*?\/(?:reset|verify|verificati
|
|
|
19
32
|
*/
|
|
20
33
|
export function redactTokenLinks(body) {
|
|
21
34
|
return (body
|
|
22
|
-
// ?token=… / &
|
|
35
|
+
// ?token=… / &reset_token=… → keep the key, drop the value
|
|
23
36
|
.replace(SENSITIVE_QUERY_PARAMS, (m) => m.slice(0, m.indexOf('=') + 1) + '[redacted]')
|
|
24
37
|
// reset/verify/magic URLs → redact everything after the sensitive segment
|
|
25
38
|
.replace(SENSITIVE_URL_PATHS, (_m, url) => {
|
|
26
|
-
const marker = url.search(
|
|
39
|
+
const marker = url.search(SENSITIVE_PATH_MARKER);
|
|
27
40
|
return marker === -1 ? url : url.slice(0, marker) + '/[redacted]';
|
|
28
|
-
})
|
|
41
|
+
})
|
|
42
|
+
// a bare token as a path segment → redact just that segment
|
|
43
|
+
.replace(OPAQUE_PATH_SEGMENT, (_m, head) => head + '[redacted]'));
|
|
29
44
|
}
|
|
30
45
|
/**
|
|
31
46
|
* Cap an HTML/text body to a maximum size, stripping token-bearing links first.
|
|
@@ -50,6 +50,15 @@ export default class ServerStatsProvider {
|
|
|
50
50
|
* built unless `production.enabled` opts in.
|
|
51
51
|
*/
|
|
52
52
|
private isEnabledHere;
|
|
53
|
+
/**
|
|
54
|
+
* Publisher-only email bridge for non-web processes (queue workers).
|
|
55
|
+
*
|
|
56
|
+
* The bridge publishes full mail bodies to Redis. A production worker must
|
|
57
|
+
* stay silent unless production mode AND email capture are both opted in —
|
|
58
|
+
* same rules the web process applies.
|
|
59
|
+
*/
|
|
60
|
+
private setupNonWebEmailBridge;
|
|
61
|
+
/** Returns true only when the HTTP routes were actually registered. */
|
|
53
62
|
private registerRoutes;
|
|
54
63
|
ready(): Promise<void>;
|
|
55
64
|
private initStats;
|
|
@@ -72,10 +72,13 @@ export default class ServerStatsProvider {
|
|
|
72
72
|
setAppRoot(this.app.makePath(''));
|
|
73
73
|
if (config.shouldShow)
|
|
74
74
|
setShouldShow(config.shouldShow);
|
|
75
|
-
await this.registerRoutes(config);
|
|
75
|
+
const routesRegistered = await this.registerRoutes(config);
|
|
76
76
|
// Only register the Edge tag when the routes it polls actually exist —
|
|
77
|
-
//
|
|
78
|
-
|
|
77
|
+
// not merely when the environment allows them. The fail-closed guard in
|
|
78
|
+
// registerAllRoutes can refuse registration (no authorize callback), and
|
|
79
|
+
// rendering the bar then would advertise endpoints that 404 — or, in
|
|
80
|
+
// production, hand every anonymous visitor a pointer at the stats API.
|
|
81
|
+
this.edgePluginActive = routesRegistered
|
|
79
82
|
? await registerEdgePluginHelper(this.app, config)
|
|
80
83
|
: false;
|
|
81
84
|
}
|
|
@@ -88,10 +91,25 @@ export default class ServerStatsProvider {
|
|
|
88
91
|
isEnabledHere(config) {
|
|
89
92
|
return !this.app.inProduction || config.production?.enabled === true;
|
|
90
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Publisher-only email bridge for non-web processes (queue workers).
|
|
96
|
+
*
|
|
97
|
+
* The bridge publishes full mail bodies to Redis. A production worker must
|
|
98
|
+
* stay silent unless production mode AND email capture are both opted in —
|
|
99
|
+
* same rules the web process applies.
|
|
100
|
+
*/
|
|
101
|
+
async setupNonWebEmailBridge(config) {
|
|
102
|
+
const emailCaptureOn = !this.app.inProduction || config.production?.capture?.emails === true;
|
|
103
|
+
if (!this.isEnabledHere(config) || !emailCaptureOn)
|
|
104
|
+
return;
|
|
105
|
+
const em = await this.resolve('emitter');
|
|
106
|
+
await setupNonWebBridgeHelper(em, this.emailBridgeChannel);
|
|
107
|
+
}
|
|
108
|
+
/** Returns true only when the HTTP routes were actually registered. */
|
|
91
109
|
async registerRoutes(config) {
|
|
92
110
|
const router = await this.resolve('router');
|
|
93
111
|
if (!router || !this.isEnabledHere(config))
|
|
94
|
-
return;
|
|
112
|
+
return false;
|
|
95
113
|
this.dashboardDepsAvailable = await checkDashboardDepsHelper(config, this.app);
|
|
96
114
|
const { statsEndpoint, debugEndpoint } = deriveEndpointPaths(config.endpoint, config.devToolbar);
|
|
97
115
|
const dashboardPath = computeDashboardPath(config.devToolbar, this.dashboardDepsAvailable);
|
|
@@ -114,21 +132,22 @@ export default class ServerStatsProvider {
|
|
|
114
132
|
});
|
|
115
133
|
const paths = collectRegisteredPaths(statsEndpoint, debugEndpoint, dashboardPath, config.domain);
|
|
116
134
|
if (!registered || paths.length === 0)
|
|
117
|
-
return;
|
|
135
|
+
return false;
|
|
118
136
|
log.list('routes auto-registered (no manual setup needed):', paths);
|
|
119
137
|
warnAboutAuthMiddleware(config, this.app.makePath.bind(this.app));
|
|
120
138
|
warnAboutSessionMiddleware(this.app.makePath.bind(this.app));
|
|
121
139
|
warnAboutDomainWithToolbar(config);
|
|
122
140
|
if (this.app.inProduction)
|
|
123
141
|
announceProductionMode(config, paths);
|
|
142
|
+
return true;
|
|
124
143
|
}
|
|
125
144
|
async ready() {
|
|
126
145
|
const config = this.app.config.get('server_stats');
|
|
127
146
|
if (!config || (this.app.inTest && config.skipInTest !== false))
|
|
128
147
|
return;
|
|
148
|
+
this.emailBridgeChannel = config.advanced?.emailBridgeChannel ?? this.emailBridgeChannel;
|
|
129
149
|
if (this.app.getEnvironment() !== 'web') {
|
|
130
|
-
|
|
131
|
-
await setupNonWebBridgeHelper(em, this.emailBridgeChannel);
|
|
150
|
+
await this.setupNonWebEmailBridge(config);
|
|
132
151
|
return;
|
|
133
152
|
}
|
|
134
153
|
this.initPromise = this.initStats(config).catch((err) => {
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Shutdown helper functions extracted from ServerStatsProvider.
|
|
3
3
|
*/
|
|
4
4
|
import { setOnRequestComplete, setDashboardPath, setExcludedPrefixes, } from '../middleware/request_tracking_middleware.js';
|
|
5
|
-
import { unhookPinoFromLogStream } from './provider_helpers_extra.js';
|
|
6
5
|
import { log } from '../utils/logger.js';
|
|
6
|
+
import { resetDashRequestPipe } from './dashboard_init.js';
|
|
7
|
+
import { unhookPinoFromLogStream } from './provider_helpers_extra.js';
|
|
7
8
|
/**
|
|
8
9
|
* Clear all active timers and null out the references.
|
|
9
10
|
* Returns the (now-nulled) timer refs for convenience.
|
|
@@ -64,6 +65,7 @@ export async function cleanupResources(deps) {
|
|
|
64
65
|
deps.dashboardLogStream?.stop();
|
|
65
66
|
unhookPinoFromLogStream();
|
|
66
67
|
setOnRequestComplete(null);
|
|
68
|
+
resetDashRequestPipe();
|
|
67
69
|
setDashboardPath(null);
|
|
68
70
|
setExcludedPrefixes([]);
|
|
69
71
|
await deps.dashboardStore?.stop();
|
|
@@ -18,7 +18,11 @@ export async function setupDevToolbarCore(opts) {
|
|
|
18
18
|
if (!em)
|
|
19
19
|
log.warn('emitter not available — query/event collection disabled');
|
|
20
20
|
await debugStore.start(em, await resolve('router'));
|
|
21
|
-
|
|
21
|
+
// The bridge publishes full mail bodies to Redis — a shared-Redis exposure —
|
|
22
|
+
// so it must obey the same capture flag as local email collection.
|
|
23
|
+
const emailBridgeRedis = debugStore.capture.emails
|
|
24
|
+
? await setupBridgeInternal(em, debugStore, app, config.advanced?.emailBridgeChannel ?? 'adonisjs-server-stats:emails')
|
|
25
|
+
: null;
|
|
22
26
|
const debugController = await createDebugController(debugStore, config, getDiagnostics, app);
|
|
23
27
|
// Also gated on capture: installing the collector is what makes the middleware
|
|
24
28
|
// wrap every request in AsyncLocalStorage and write a trace row per request.
|
|
@@ -70,7 +74,7 @@ function createFlushTimer(debugStore, persistPath) {
|
|
|
70
74
|
catch { }
|
|
71
75
|
}, 30_000);
|
|
72
76
|
}
|
|
73
|
-
async function setupBridgeInternal(emitter, debugStore, app) {
|
|
77
|
+
async function setupBridgeInternal(emitter, debugStore, app, channel) {
|
|
74
78
|
if (!emitter)
|
|
75
79
|
return null;
|
|
76
80
|
try {
|
|
@@ -89,7 +93,7 @@ async function setupBridgeInternal(emitter, debugStore, app) {
|
|
|
89
93
|
return null;
|
|
90
94
|
}
|
|
91
95
|
};
|
|
92
|
-
return await setupFullEmailBridge(emitter, redis,
|
|
96
|
+
return await setupFullEmailBridge(emitter, redis, channel, { debugEmails: debugStore.emails ?? null, dashboardStore: getDashboardStore });
|
|
93
97
|
}
|
|
94
98
|
catch {
|
|
95
99
|
return null;
|
|
@@ -122,6 +126,19 @@ async function setupDebugBroadcastInternal(debugStore, resolve) {
|
|
|
122
126
|
});
|
|
123
127
|
return { timer, transmitAvailable: true, channels: [ch] };
|
|
124
128
|
}
|
|
129
|
+
/** Copy dashboard-init results onto the provider once async init finishes. */
|
|
130
|
+
function applyDashboardResult(provider, r) {
|
|
131
|
+
provider.dashboardStore = r.dashboardStore;
|
|
132
|
+
provider.dashboardController = r.dashboardController;
|
|
133
|
+
provider.dashboardLogStream = r.dashboardLogStream;
|
|
134
|
+
provider.dashboardBroadcastTimer = r.dashboardBroadcastTimer;
|
|
135
|
+
if (r.dashboardController) {
|
|
136
|
+
log.info('dashboard: controller ready — API endpoints are now live');
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
log.warn('dashboard: init completed but controller is null — dashboard API will return 503');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
125
142
|
/** Apply the toolbar core result back onto the provider instance. */
|
|
126
143
|
export function applyToolbarResult(result, tc, provider) {
|
|
127
144
|
provider.debugStore = result.debugStore;
|
|
@@ -154,18 +171,7 @@ export function applyToolbarResult(result, tc, provider) {
|
|
|
154
171
|
engine: provider.engine,
|
|
155
172
|
pinoHookActive: provider.pinoHookActive,
|
|
156
173
|
transmitChannels: provider.transmitChannels,
|
|
157
|
-
onResult: (r) =>
|
|
158
|
-
provider.dashboardStore = r.dashboardStore;
|
|
159
|
-
provider.dashboardController = r.dashboardController;
|
|
160
|
-
provider.dashboardLogStream = r.dashboardLogStream;
|
|
161
|
-
provider.dashboardBroadcastTimer = r.dashboardBroadcastTimer;
|
|
162
|
-
if (r.dashboardController) {
|
|
163
|
-
log.info('dashboard: controller ready — API endpoints are now live');
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
log.warn('dashboard: init completed but controller is null — dashboard API will return 503');
|
|
167
|
-
}
|
|
168
|
-
},
|
|
174
|
+
onResult: (r) => applyDashboardResult(provider, r),
|
|
169
175
|
}).catch((e) => {
|
|
170
176
|
log.warn(`dashboard: setup failed: ${e?.message ?? e}`);
|
|
171
177
|
if (e?.stack)
|
package/dist/src/types.d.ts
CHANGED
|
@@ -461,7 +461,7 @@ export type AccessGuard = (ctx: import('@adonisjs/core/http').HttpContext) => bo
|
|
|
461
461
|
export interface CaptureConfig {
|
|
462
462
|
/** SQL text, bindings, and timings for every query. */
|
|
463
463
|
queries?: boolean;
|
|
464
|
-
/** Application events
|
|
464
|
+
/** Application events, linked to the request that follows them and persisted with it. */
|
|
465
465
|
events?: boolean;
|
|
466
466
|
/** Sent mail, including subject and body. */
|
|
467
467
|
emails?: boolean;
|
|
@@ -622,6 +622,29 @@ export interface AdvancedConfig {
|
|
|
622
622
|
* @default 200
|
|
623
623
|
*/
|
|
624
624
|
maxTraces?: number;
|
|
625
|
+
/**
|
|
626
|
+
* Allow-list prefix for the dashboard's cache inspector.
|
|
627
|
+
*
|
|
628
|
+
* Scopes which Redis keys the Cache tab may list, read, and delete
|
|
629
|
+
* (e.g. `'myapp:cache:'`). Unset keeps full access in development for
|
|
630
|
+
* back-compat, but in production single-key reads and deletes are
|
|
631
|
+
* refused until a prefix is configured — a production dashboard must
|
|
632
|
+
* not be able to read session keys or delete rate-limit counters.
|
|
633
|
+
*
|
|
634
|
+
* The `SERVER_STATS_CACHE_KEY_PREFIX` env var is used as a fallback.
|
|
635
|
+
*/
|
|
636
|
+
cacheKeyPrefix?: string;
|
|
637
|
+
/**
|
|
638
|
+
* Redis pub/sub channel for the cross-process email bridge.
|
|
639
|
+
*
|
|
640
|
+
* Queue workers publish captured mail on this channel and the web
|
|
641
|
+
* process persists it. On a Redis instance shared between apps or
|
|
642
|
+
* tenants, namespace this (e.g. `myapp:server-stats:emails`) so
|
|
643
|
+
* other subscribers cannot read your outbound mail.
|
|
644
|
+
*
|
|
645
|
+
* @default 'adonisjs-server-stats:emails'
|
|
646
|
+
*/
|
|
647
|
+
emailBridgeChannel?: string;
|
|
625
648
|
}
|
|
626
649
|
/**
|
|
627
650
|
* Top-level configuration for `adonisjs-server-stats`.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { defineComponent as L, inject as y, ref as h, onMounted as x, computed as w, onUnmounted as F, openBlock as l, createElementBlock as n, createElementVNode as e, toDisplayString as c, createCommentVNode as z, createVNode as N, unref as p, Fragment as U, renderList as B, withModifiers as M, createBlock as H } from "vue";
|
|
2
|
+
import { u as I } from "./index-DUsImuIN.js";
|
|
3
|
+
import { u as j } from "./useResizableTable-BoivAevK.js";
|
|
4
|
+
import { DashboardApi as q, formatCacheSize as P, formatTtl as G } from "adonisjs-server-stats/core";
|
|
5
|
+
import { u as J } from "./useApiClient-BQQ9CF-q.js";
|
|
6
|
+
import { _ as O } from "./JsonViewer.vue_vue_type_script_setup_true_lang-Bid05zpm.js";
|
|
7
|
+
import { _ as Q } from "./FilterBar.vue_vue_type_script_setup_true_lang-ClJ37hhT.js";
|
|
8
|
+
const W = {
|
|
9
|
+
key: 0,
|
|
10
|
+
class: "ss-dash-cache-stats"
|
|
11
|
+
}, X = { class: "ss-dash-cache-stat" }, Y = { class: "ss-dash-cache-stat-value" }, Z = { class: "ss-dash-cache-stat" }, ee = { class: "ss-dash-cache-stat-value" }, se = { class: "ss-dash-cache-stat" }, ae = { class: "ss-dash-cache-stat-value" }, te = { class: "ss-dash-cache-stat" }, le = { class: "ss-dash-cache-stat-value" }, ne = {
|
|
12
|
+
key: 1,
|
|
13
|
+
class: "ss-dash-empty"
|
|
14
|
+
}, oe = {
|
|
15
|
+
key: 2,
|
|
16
|
+
class: "ss-dash-empty"
|
|
17
|
+
}, ce = {
|
|
18
|
+
key: 3,
|
|
19
|
+
class: "ss-dash-table-wrap"
|
|
20
|
+
}, ue = ["onClick"], ie = ["title"], re = { style: { color: "var(--ss-muted)" } }, de = ["onClick"], he = {
|
|
21
|
+
key: 1,
|
|
22
|
+
class: "ss-dash-empty"
|
|
23
|
+
}, ve = {
|
|
24
|
+
key: 4,
|
|
25
|
+
class: "ss-dash-cache-detail"
|
|
26
|
+
}, ye = {
|
|
27
|
+
key: 0,
|
|
28
|
+
class: "ss-dash-empty"
|
|
29
|
+
}, pe = {
|
|
30
|
+
key: 1,
|
|
31
|
+
class: "ss-dash-empty",
|
|
32
|
+
style: { color: "var(--ss-red-fg)" }
|
|
33
|
+
}, ze = /* @__PURE__ */ L({
|
|
34
|
+
__name: "CacheSection",
|
|
35
|
+
setup(_e) {
|
|
36
|
+
const D = y("ss-refresh-key", h(0)), m = y("ss-dashboard-endpoint", "/__stats/api"), b = y("ss-auth-token", void 0), k = y("ss-base-url", ""), { data: C, loading: K, setSearch: A, mutate: E } = I(() => "cache", {
|
|
37
|
+
baseUrl: k,
|
|
38
|
+
dashboardEndpoint: m,
|
|
39
|
+
authToken: b,
|
|
40
|
+
refreshKey: D
|
|
41
|
+
});
|
|
42
|
+
let _ = null;
|
|
43
|
+
x(() => {
|
|
44
|
+
const t = J(k || "", b);
|
|
45
|
+
_ = new q(t(), m || "/__stats/api");
|
|
46
|
+
});
|
|
47
|
+
const g = h(""), u = h(null), d = h(null), f = h(!1), i = h(null), o = w(() => C.value), v = w(() => o.value?.keys || o.value?.data || []);
|
|
48
|
+
function V(t) {
|
|
49
|
+
g.value = t, A(t);
|
|
50
|
+
}
|
|
51
|
+
async function $(t) {
|
|
52
|
+
if (confirm(`Delete cache key "${t}"?`))
|
|
53
|
+
try {
|
|
54
|
+
await E(`cache/${encodeURIComponent(t)}`, "delete"), u.value === t && (u.value = null, d.value = null, i.value = null);
|
|
55
|
+
} catch {
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const { tableRef: R } = j(() => v.value);
|
|
59
|
+
let r = null;
|
|
60
|
+
async function S(t) {
|
|
61
|
+
if (r?.abort(), r = null, u.value === t) {
|
|
62
|
+
u.value = null, d.value = null, i.value = null;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
u.value = t, d.value = null, i.value = null, f.value = !0;
|
|
66
|
+
const a = new AbortController();
|
|
67
|
+
r = a;
|
|
68
|
+
try {
|
|
69
|
+
if (!_) throw new Error("Cache API not initialized");
|
|
70
|
+
const s = await _.fetchCacheKey(t, { signal: a.signal });
|
|
71
|
+
if (r !== a) return;
|
|
72
|
+
d.value = s.value !== void 0 ? s.value : s.data !== void 0 ? s.data : s, i.value = null;
|
|
73
|
+
} catch (s) {
|
|
74
|
+
if (s instanceof Error && s.name === "AbortError" || r !== a) return;
|
|
75
|
+
d.value = null, i.value = "Failed to fetch key value";
|
|
76
|
+
} finally {
|
|
77
|
+
r === a && (f.value = !1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return F(() => r?.abort()), (t, a) => (l(), n("div", null, [
|
|
81
|
+
o.value?.available && o.value?.stats ? (l(), n("div", W, [
|
|
82
|
+
e("div", X, [
|
|
83
|
+
a[0] || (a[0] = e("span", { class: "ss-dash-cache-stat-label" }, "Hit Rate:", -1)),
|
|
84
|
+
e("span", Y, c((o.value.stats.hitRate ?? 0).toFixed(1)) + "%", 1)
|
|
85
|
+
]),
|
|
86
|
+
e("div", Z, [
|
|
87
|
+
a[1] || (a[1] = e("span", { class: "ss-dash-cache-stat-label" }, "Hits:", -1)),
|
|
88
|
+
e("span", ee, c(o.value.stats.hits ?? 0), 1)
|
|
89
|
+
]),
|
|
90
|
+
e("div", se, [
|
|
91
|
+
a[2] || (a[2] = e("span", { class: "ss-dash-cache-stat-label" }, "Misses:", -1)),
|
|
92
|
+
e("span", ae, c(o.value.stats.misses ?? 0), 1)
|
|
93
|
+
]),
|
|
94
|
+
e("div", te, [
|
|
95
|
+
a[3] || (a[3] = e("span", { class: "ss-dash-cache-stat-label" }, "Keys:", -1)),
|
|
96
|
+
e("span", le, c(o.value.stats.totalKeys || o.value.stats.keyCount || v.value.length || 0), 1)
|
|
97
|
+
])
|
|
98
|
+
])) : z("", !0),
|
|
99
|
+
N(Q, {
|
|
100
|
+
"model-value": g.value,
|
|
101
|
+
placeholder: "Filter cache keys...",
|
|
102
|
+
summary: `${v.value.length} keys`,
|
|
103
|
+
"onUpdate:modelValue": V
|
|
104
|
+
}, null, 8, ["model-value", "summary"]),
|
|
105
|
+
p(K) && !p(C) ? (l(), n("div", ne, "Loading cache...")) : !o.value || !o.value.available ? (l(), n("div", oe, " Cache inspector not available ")) : (l(), n("div", ce, [
|
|
106
|
+
v.value.length > 0 ? (l(), n("table", {
|
|
107
|
+
key: 0,
|
|
108
|
+
ref_key: "tableRef",
|
|
109
|
+
ref: R,
|
|
110
|
+
class: "ss-dash-table"
|
|
111
|
+
}, [
|
|
112
|
+
a[4] || (a[4] = e("thead", null, [
|
|
113
|
+
e("tr", null, [
|
|
114
|
+
e("th", null, "Key"),
|
|
115
|
+
e("th", null, "Type"),
|
|
116
|
+
e("th", null, "Size"),
|
|
117
|
+
e("th", null, "TTL"),
|
|
118
|
+
e("th")
|
|
119
|
+
])
|
|
120
|
+
], -1)),
|
|
121
|
+
e("tbody", null, [
|
|
122
|
+
(l(!0), n(U, null, B(v.value, (s) => (l(), n("tr", {
|
|
123
|
+
key: s.key,
|
|
124
|
+
class: "ss-dash-clickable",
|
|
125
|
+
onClick: (T) => S(s.key)
|
|
126
|
+
}, [
|
|
127
|
+
e("td", null, [
|
|
128
|
+
e("span", {
|
|
129
|
+
title: s.key,
|
|
130
|
+
style: { color: "var(--ss-sql-color)", overflow: "hidden", "text-overflow": "ellipsis", "white-space": "nowrap", display: "block" }
|
|
131
|
+
}, c(s.key), 9, ie)
|
|
132
|
+
]),
|
|
133
|
+
e("td", null, [
|
|
134
|
+
e("span", re, c(s.type), 1)
|
|
135
|
+
]),
|
|
136
|
+
e("td", null, c(s.size !== null && s.size !== void 0 && s.size > 0 ? p(P)(s.size) : "-"), 1),
|
|
137
|
+
e("td", null, c(s.ttl > 0 ? p(G)(s.ttl) : "-"), 1),
|
|
138
|
+
e("td", null, [
|
|
139
|
+
e("button", {
|
|
140
|
+
type: "button",
|
|
141
|
+
class: "ss-dash-retry-btn",
|
|
142
|
+
onClick: M((T) => $(s.key), ["stop"])
|
|
143
|
+
}, " Delete ", 8, de)
|
|
144
|
+
])
|
|
145
|
+
], 8, ue))), 128))
|
|
146
|
+
])
|
|
147
|
+
], 512)) : (l(), n("div", he, "No cache keys found"))
|
|
148
|
+
])),
|
|
149
|
+
u.value ? (l(), n("div", ve, [
|
|
150
|
+
e("h4", null, "Key: " + c(u.value), 1),
|
|
151
|
+
f.value ? (l(), n("div", ye, "Loading value...")) : i.value ? (l(), n("div", pe, c(i.value), 1)) : (l(), H(O, {
|
|
152
|
+
key: 2,
|
|
153
|
+
value: d.value
|
|
154
|
+
}, null, 8, ["value"]))
|
|
155
|
+
])) : z("", !0)
|
|
156
|
+
]));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
export {
|
|
160
|
+
ze as default
|
|
161
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineComponent as nt, inject as L, ref as h, computed as k, openBlock as r, createElementBlock as c, createElementVNode as n, normalizeClass as a, createCommentVNode as g, Fragment as x, toDisplayString as u, unref as d, renderList as E, normalizeStyle as j } from "vue";
|
|
2
|
-
import { u as lt } from "./index-
|
|
2
|
+
import { u as lt } from "./index-DUsImuIN.js";
|
|
3
3
|
import { isRedactedValue as p, flattenConfig as O, countLeaves as ot, collectTopLevelObjectKeys as st, copyWithFeedback as at, formatFlatValue as rt } from "adonisjs-server-stats/core";
|
|
4
4
|
const ct = { style: { position: "relative", flex: 1 } }, ut = ["value"], it = ["onClick"], pt = { key: 0 }, dt = ["onClick"], vt = { key: 0 }, ft = { style: { padding: "4px 16px", fontSize: "10px", color: "var(--ss-muted)" } }, yt = ["onClick"], gt = ["title"], bt = ["title"], ht = ["onClick"], $t = {
|
|
5
5
|
key: 1,
|