adonisjs-server-stats 1.15.0 → 1.16.1
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 +88 -2
- package/dist/core/debug/types.d.ts +14 -0
- package/dist/core/types.d.ts +109 -4
- package/dist/src/dashboard/dashboard_controller.d.ts +7 -0
- package/dist/src/dashboard/dashboard_controller.js +10 -3
- package/dist/src/dashboard/dashboard_store.d.ts +1 -2
- package/dist/src/dashboard/dashboard_store.js +0 -3
- package/dist/src/dashboard/dashboard_types.d.ts +2 -0
- package/dist/src/dashboard/flush_manager.d.ts +1 -6
- package/dist/src/dashboard/flush_manager.js +3 -10
- package/dist/src/dashboard/integrations/config_inspector.js +9 -49
- package/dist/src/dashboard/sensitive_patterns.d.ts +39 -0
- package/dist/src/dashboard/sensitive_patterns.js +118 -0
- package/dist/src/dashboard/write_queue.d.ts +25 -13
- package/dist/src/dashboard/write_queue.js +63 -37
- package/dist/src/debug/debug_store.d.ts +15 -1
- package/dist/src/debug/debug_store.js +32 -4
- package/dist/src/debug/event_collector.d.ts +8 -0
- package/dist/src/debug/event_collector.js +12 -0
- package/dist/src/debug/types.d.ts +14 -0
- package/dist/src/define_config.js +23 -0
- package/dist/src/middleware/request_tracking_middleware.d.ts +2 -1
- package/dist/src/middleware/request_tracking_middleware.js +16 -1
- package/dist/src/provider/boot_helpers.d.ts +9 -0
- package/dist/src/provider/boot_helpers.js +31 -0
- package/dist/src/provider/dashboard_init.js +14 -1
- package/dist/src/provider/dashboard_setup.d.ts +10 -1
- package/dist/src/provider/dashboard_setup.js +47 -2
- package/dist/src/provider/server_stats_provider.d.ts +7 -0
- package/dist/src/provider/server_stats_provider.js +23 -7
- package/dist/src/provider/toolbar_setup.js +5 -1
- package/dist/src/routes/access_middleware.d.ts +7 -1
- package/dist/src/routes/access_middleware.js +6 -1
- package/dist/src/routes/register_routes.d.ts +13 -3
- package/dist/src/routes/register_routes.js +15 -1
- package/dist/src/stubs/config.stub +8 -0
- package/dist/src/types.d.ts +109 -4
- package/package.json +1 -1
|
@@ -42,7 +42,11 @@ export async function initDashboardStore(opts) {
|
|
|
42
42
|
setDashboardPath(tc.dashboardPath);
|
|
43
43
|
const DCC = (await import('../dashboard/dashboard_controller.js')).default;
|
|
44
44
|
const dashboardController = new DCC(dashboardStore, app);
|
|
45
|
-
|
|
45
|
+
// Log capture is a separate opt-in: it is high-volume and log lines routinely
|
|
46
|
+
// carry request payloads. With it off the dashboard's other panes still work.
|
|
47
|
+
const dashboardLogStream = tc.capture?.logs !== false
|
|
48
|
+
? pipeDashLogs(pinoHookActive, dashboardStore, app.makePath.bind(app))
|
|
49
|
+
: null;
|
|
46
50
|
pipeDashRequests(debugStore, dashboardStore);
|
|
47
51
|
const dashboardBroadcastTimer = await setupDashBroadcast({
|
|
48
52
|
container,
|
|
@@ -114,18 +118,27 @@ function pipeDashRequests(debugStore, dashboardStore) {
|
|
|
114
118
|
}
|
|
115
119
|
dashRequestPipeInstalled = true;
|
|
116
120
|
let lastQueryId = 0;
|
|
121
|
+
let lastEventId = 0;
|
|
117
122
|
setOnRequestComplete(({ method, url, statusCode, duration, trace, httpRequestId }) => {
|
|
118
123
|
if (!dashboardStore.isReady())
|
|
119
124
|
return;
|
|
120
125
|
const q = debugStore.queries.getQueriesSince(lastQueryId);
|
|
121
126
|
if (q.length > 0)
|
|
122
127
|
lastQueryId = q[q.length - 1].id;
|
|
128
|
+
// Events are collected globally rather than per-request, so — exactly like
|
|
129
|
+
// queries above — anything emitted outside a request is attributed to
|
|
130
|
+
// whichever request finishes next. Imprecise, but it keeps events linked to
|
|
131
|
+
// a request row, which is what retention prunes on.
|
|
132
|
+
const events = debugStore.capture.events ? debugStore.events.getEventsSince(lastEventId) : [];
|
|
133
|
+
if (events.length > 0)
|
|
134
|
+
lastEventId = events[events.length - 1].id;
|
|
123
135
|
dashboardStore.persistRequest({
|
|
124
136
|
method,
|
|
125
137
|
url,
|
|
126
138
|
statusCode,
|
|
127
139
|
duration,
|
|
128
140
|
queries: q,
|
|
141
|
+
events,
|
|
129
142
|
trace: trace ?? null,
|
|
130
143
|
httpRequestId: httpRequestId ?? null,
|
|
131
144
|
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Pure helper functions for dashboard setup and configuration.
|
|
3
3
|
*/
|
|
4
4
|
import type { DevToolbarConfig } from '../debug/types.js';
|
|
5
|
+
import type { ProductionConfig } from '../types.js';
|
|
5
6
|
/**
|
|
6
7
|
* Classify a dashboard start() error into a category.
|
|
7
8
|
*/
|
|
@@ -17,9 +18,17 @@ export declare function buildExcludedPrefixes(toolbarConfig: {
|
|
|
17
18
|
debugEndpoint?: string;
|
|
18
19
|
excludeFromTracing?: string[];
|
|
19
20
|
}, statsEndpoint: string | false): string[];
|
|
21
|
+
/** Environment context needed to resolve production-sensitive defaults. */
|
|
22
|
+
export interface ProductionContext {
|
|
23
|
+
inProduction: boolean;
|
|
24
|
+
production?: ProductionConfig;
|
|
25
|
+
}
|
|
20
26
|
/**
|
|
21
27
|
* Resolve a partial DevToolbarConfig by filling in all defaults.
|
|
28
|
+
*
|
|
29
|
+
* Pass `ctx` to apply production-sensitive defaults (capture off, shorter
|
|
30
|
+
* retention). Omitting it resolves as a non-production environment.
|
|
22
31
|
*/
|
|
23
32
|
export declare function resolveToolbarConfig(partial: Partial<DevToolbarConfig> & {
|
|
24
33
|
enabled: boolean;
|
|
25
|
-
}): DevToolbarConfig;
|
|
34
|
+
}, ctx?: ProductionContext): DevToolbarConfig;
|
|
@@ -73,13 +73,58 @@ function stripUndefined(obj) {
|
|
|
73
73
|
}
|
|
74
74
|
return result;
|
|
75
75
|
}
|
|
76
|
+
/** Retention default when running in production — shorter than the usual 7 days. */
|
|
77
|
+
const PRODUCTION_RETENTION_DAYS = 3;
|
|
76
78
|
/**
|
|
77
|
-
* Resolve
|
|
79
|
+
* Resolve which capture subsystems subscribe.
|
|
80
|
+
*
|
|
81
|
+
* Outside production everything captures, as it always has. In production every
|
|
82
|
+
* subsystem is off until asked for by name, because each one is the expensive,
|
|
83
|
+
* secret-adjacent half of this package: query bindings, mail bodies, log lines.
|
|
84
|
+
*
|
|
85
|
+
* `tracing: false` still wins over `capture.traces` — it is the pre-existing
|
|
86
|
+
* documented kill switch and must not be quietly re-enabled.
|
|
78
87
|
*/
|
|
79
|
-
|
|
88
|
+
function resolveCapture(ctx, tracing) {
|
|
89
|
+
const inProduction = ctx?.inProduction === true;
|
|
90
|
+
const requested = (inProduction ? ctx?.production?.capture : undefined) ?? {};
|
|
91
|
+
const isOn = (key) => requested[key] ?? !inProduction;
|
|
80
92
|
return {
|
|
93
|
+
queries: isOn('queries'),
|
|
94
|
+
events: isOn('events'),
|
|
95
|
+
emails: isOn('emails'),
|
|
96
|
+
traces: tracing && isOn('traces'),
|
|
97
|
+
logs: isOn('logs'),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Resolve retention, preferring an explicit value from any source over the
|
|
102
|
+
* production default. Order: `production.retentionDays`, then whatever
|
|
103
|
+
* `dashboard`/`advanced` set, then 3 days in production, then 7.
|
|
104
|
+
*/
|
|
105
|
+
function resolveRetentionDays(ctx, explicit) {
|
|
106
|
+
const fromProduction = ctx?.inProduction ? ctx.production?.retentionDays : undefined;
|
|
107
|
+
if (fromProduction !== undefined)
|
|
108
|
+
return fromProduction;
|
|
109
|
+
if (explicit !== undefined)
|
|
110
|
+
return explicit;
|
|
111
|
+
return ctx?.inProduction ? PRODUCTION_RETENTION_DAYS : TOOLBAR_DEFAULTS.retentionDays;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve a partial DevToolbarConfig by filling in all defaults.
|
|
115
|
+
*
|
|
116
|
+
* Pass `ctx` to apply production-sensitive defaults (capture off, shorter
|
|
117
|
+
* retention). Omitting it resolves as a non-production environment.
|
|
118
|
+
*/
|
|
119
|
+
export function resolveToolbarConfig(partial, ctx) {
|
|
120
|
+
const merged = {
|
|
81
121
|
...TOOLBAR_DEFAULTS,
|
|
82
122
|
...stripUndefined(partial),
|
|
83
123
|
enabled: partial.enabled,
|
|
84
124
|
};
|
|
125
|
+
return {
|
|
126
|
+
...merged,
|
|
127
|
+
retentionDays: resolveRetentionDays(ctx, partial.retentionDays),
|
|
128
|
+
capture: resolveCapture(ctx, merged.tracing),
|
|
129
|
+
};
|
|
85
130
|
}
|
|
@@ -43,6 +43,13 @@ export default class ServerStatsProvider {
|
|
|
43
43
|
whenReady(): Promise<void>;
|
|
44
44
|
boot(): Promise<void>;
|
|
45
45
|
private initBoot;
|
|
46
|
+
/**
|
|
47
|
+
* Whether this package should do anything in the current environment.
|
|
48
|
+
*
|
|
49
|
+
* Everything is on outside production. In production nothing is registered or
|
|
50
|
+
* built unless `production.enabled` opts in.
|
|
51
|
+
*/
|
|
52
|
+
private isEnabledHere;
|
|
46
53
|
private registerRoutes;
|
|
47
54
|
ready(): Promise<void>;
|
|
48
55
|
private initStats;
|
|
@@ -2,7 +2,7 @@ import { StatsEngine } from '../engine/stats_engine.js';
|
|
|
2
2
|
import { setShouldShow, setExcludedPrefixes } from '../middleware/request_tracking_middleware.js';
|
|
3
3
|
import { registerAllRoutes } from '../routes/register_routes.js';
|
|
4
4
|
import { log, dim, setVerbose } from '../utils/logger.js';
|
|
5
|
-
import { deriveEndpointPaths, computeDashboardPath, collectRegisteredPaths, warnAboutAuthMiddleware, warnAboutSessionMiddleware, warnAboutDomainWithToolbar, } from './boot_helpers.js';
|
|
5
|
+
import { deriveEndpointPaths, computeDashboardPath, collectRegisteredPaths, warnAboutAuthMiddleware, warnAboutSessionMiddleware, warnAboutDomainWithToolbar, announceProductionMode, } from './boot_helpers.js';
|
|
6
6
|
import { resolveToolbarConfig, buildExcludedPrefixes } from './dashboard_setup.js';
|
|
7
7
|
import { buildDiagnostics } from './diagnostics.js';
|
|
8
8
|
import { hookPinoToLogStream, setupLogStreamBroadcast, setupStatsIntervalHelper, checkDashboardDepsHelper, registerEdgePluginHelper, setupNonWebBridgeHelper, setupDevToolbarCore, applyToolbarResult, } from './provider_helpers_extra.js';
|
|
@@ -73,16 +73,29 @@ export default class ServerStatsProvider {
|
|
|
73
73
|
if (config.shouldShow)
|
|
74
74
|
setShouldShow(config.shouldShow);
|
|
75
75
|
await this.registerRoutes(config);
|
|
76
|
-
|
|
76
|
+
// Only register the Edge tag when the routes it polls actually exist —
|
|
77
|
+
// otherwise the bar renders in production and 404s on every tick.
|
|
78
|
+
this.edgePluginActive = this.isEnabledHere(config)
|
|
79
|
+
? await registerEdgePluginHelper(this.app, config)
|
|
80
|
+
: false;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Whether this package should do anything in the current environment.
|
|
84
|
+
*
|
|
85
|
+
* Everything is on outside production. In production nothing is registered or
|
|
86
|
+
* built unless `production.enabled` opts in.
|
|
87
|
+
*/
|
|
88
|
+
isEnabledHere(config) {
|
|
89
|
+
return !this.app.inProduction || config.production?.enabled === true;
|
|
77
90
|
}
|
|
78
91
|
async registerRoutes(config) {
|
|
79
92
|
const router = await this.resolve('router');
|
|
80
|
-
if (!router || this.
|
|
93
|
+
if (!router || !this.isEnabledHere(config))
|
|
81
94
|
return;
|
|
82
95
|
this.dashboardDepsAvailable = await checkDashboardDepsHelper(config, this.app);
|
|
83
96
|
const { statsEndpoint, debugEndpoint } = deriveEndpointPaths(config.endpoint, config.devToolbar);
|
|
84
97
|
const dashboardPath = computeDashboardPath(config.devToolbar, this.dashboardDepsAvailable);
|
|
85
|
-
registerAllRoutes({
|
|
98
|
+
const registered = registerAllRoutes({
|
|
86
99
|
router: router,
|
|
87
100
|
getApiController: () => this.apiController,
|
|
88
101
|
getStatsController: () => this.statsController,
|
|
@@ -95,16 +108,19 @@ export default class ServerStatsProvider {
|
|
|
95
108
|
dashboardPath,
|
|
96
109
|
shouldShow: config.shouldShow,
|
|
97
110
|
unsafeAllowNoAuth: config.unsafeAllowNoAuth,
|
|
111
|
+
inProduction: this.app.inProduction,
|
|
98
112
|
whenReady: () => this.whenReady(),
|
|
99
113
|
domain: config.domain,
|
|
100
114
|
});
|
|
101
115
|
const paths = collectRegisteredPaths(statsEndpoint, debugEndpoint, dashboardPath, config.domain);
|
|
102
|
-
if (paths.length === 0)
|
|
116
|
+
if (!registered || paths.length === 0)
|
|
103
117
|
return;
|
|
104
118
|
log.list('routes auto-registered (no manual setup needed):', paths);
|
|
105
119
|
warnAboutAuthMiddleware(config, this.app.makePath.bind(this.app));
|
|
106
120
|
warnAboutSessionMiddleware(this.app.makePath.bind(this.app));
|
|
107
121
|
warnAboutDomainWithToolbar(config);
|
|
122
|
+
if (this.app.inProduction)
|
|
123
|
+
announceProductionMode(config, paths);
|
|
108
124
|
}
|
|
109
125
|
async ready() {
|
|
110
126
|
const config = this.app.config.get('server_stats');
|
|
@@ -132,7 +148,7 @@ export default class ServerStatsProvider {
|
|
|
132
148
|
this.pinoHookActive = hookPinoToLogStream(await this.resolve('logger'));
|
|
133
149
|
const SC = (await import('../controller/server_stats_controller.js')).default;
|
|
134
150
|
this.statsController = new SC(this.engine);
|
|
135
|
-
if (config.devToolbar?.enabled &&
|
|
151
|
+
if (config.devToolbar?.enabled && this.isEnabledHere(config)) {
|
|
136
152
|
this.checkLucidDebugFlag();
|
|
137
153
|
await this.setupDevToolbar(config);
|
|
138
154
|
}
|
|
@@ -158,7 +174,7 @@ export default class ServerStatsProvider {
|
|
|
158
174
|
return r.collectors;
|
|
159
175
|
}
|
|
160
176
|
async setupDevToolbar(config) {
|
|
161
|
-
const tc = resolveToolbarConfig({ enabled: true, ...config.devToolbar });
|
|
177
|
+
const tc = resolveToolbarConfig({ enabled: true, ...config.devToolbar }, { inProduction: this.app.inProduction, production: config.production });
|
|
162
178
|
try {
|
|
163
179
|
const result = await setupDevToolbarCore({
|
|
164
180
|
tc,
|
|
@@ -20,7 +20,11 @@ export async function setupDevToolbarCore(opts) {
|
|
|
20
20
|
await debugStore.start(em, await resolve('router'));
|
|
21
21
|
const emailBridgeRedis = await setupBridgeInternal(em, debugStore, app);
|
|
22
22
|
const debugController = await createDebugController(debugStore, config, getDiagnostics, app);
|
|
23
|
-
|
|
23
|
+
// Also gated on capture: installing the collector is what makes the middleware
|
|
24
|
+
// wrap every request in AsyncLocalStorage and write a trace row per request.
|
|
25
|
+
// Gating only the emitter subscription would leave that cost in place and
|
|
26
|
+
// persist empty traces.
|
|
27
|
+
if (debugStore.capture.traces && debugStore.traces)
|
|
24
28
|
setTraceCollector(debugStore.traces);
|
|
25
29
|
const flushTimer = persistPath ? createFlushTimer(debugStore, persistPath) : null;
|
|
26
30
|
const broadcast = await setupDebugBroadcastInternal(debugStore, resolve);
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import type { AccessGuard } from '../types.js';
|
|
1
2
|
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
3
|
/**
|
|
3
4
|
* Create a middleware function that gates access using the shouldShow callback.
|
|
4
5
|
* Returns 403 if the callback returns false.
|
|
5
6
|
*
|
|
7
|
+
* The guard is awaited, so an async callback (the usual shape when it has to
|
|
8
|
+
* consult `ctx.auth` or the database) is resolved before the decision is made.
|
|
9
|
+
* Returning the promise unawaited would make every async guard pass, since a
|
|
10
|
+
* pending promise is truthy.
|
|
11
|
+
*
|
|
6
12
|
* Shared by stats, debug, and dashboard route registrars.
|
|
7
13
|
*/
|
|
8
|
-
export declare function createAccessMiddleware(shouldShow:
|
|
14
|
+
export declare function createAccessMiddleware(shouldShow: AccessGuard): (ctx: HttpContext, next: () => Promise<void>) => Promise<void>;
|
|
@@ -4,12 +4,17 @@ let warnedShouldShow = false;
|
|
|
4
4
|
* Create a middleware function that gates access using the shouldShow callback.
|
|
5
5
|
* Returns 403 if the callback returns false.
|
|
6
6
|
*
|
|
7
|
+
* The guard is awaited, so an async callback (the usual shape when it has to
|
|
8
|
+
* consult `ctx.auth` or the database) is resolved before the decision is made.
|
|
9
|
+
* Returning the promise unawaited would make every async guard pass, since a
|
|
10
|
+
* pending promise is truthy.
|
|
11
|
+
*
|
|
7
12
|
* Shared by stats, debug, and dashboard route registrars.
|
|
8
13
|
*/
|
|
9
14
|
export function createAccessMiddleware(shouldShow) {
|
|
10
15
|
return async (ctx, next) => {
|
|
11
16
|
try {
|
|
12
|
-
if (!shouldShow(ctx)) {
|
|
17
|
+
if (!(await shouldShow(ctx))) {
|
|
13
18
|
return ctx.response.forbidden({ error: 'Access denied' });
|
|
14
19
|
}
|
|
15
20
|
}
|
|
@@ -3,8 +3,8 @@ import type DebugController from '../controller/debug_controller.js';
|
|
|
3
3
|
import type { DebugStore } from '../debug/debug_store.js';
|
|
4
4
|
import type ServerStatsController from '../controller/server_stats_controller.js';
|
|
5
5
|
import type DashboardController from '../dashboard/dashboard_controller.js';
|
|
6
|
+
import type { AccessGuard } from '../types.js';
|
|
6
7
|
import type { AdonisRouter } from './router_types.js';
|
|
7
|
-
import type { HttpContext } from '@adonisjs/core/http';
|
|
8
8
|
import type { ApplicationService } from '@adonisjs/core/types';
|
|
9
9
|
/**
|
|
10
10
|
* Options for the unified route registration function.
|
|
@@ -20,7 +20,7 @@ export interface RegisterRoutesOptions {
|
|
|
20
20
|
statsEndpoint?: string | false;
|
|
21
21
|
debugEndpoint?: string;
|
|
22
22
|
dashboardPath?: string;
|
|
23
|
-
shouldShow?:
|
|
23
|
+
shouldShow?: AccessGuard;
|
|
24
24
|
/**
|
|
25
25
|
* Escape hatch to register the sensitive dashboard/debug/stats routes WITHOUT
|
|
26
26
|
* any access guard when no `shouldShow` callback is provided. Off by default —
|
|
@@ -29,6 +29,12 @@ export interface RegisterRoutesOptions {
|
|
|
29
29
|
* email bodies, and SQL) to anyone who can reach it. Local dev only.
|
|
30
30
|
*/
|
|
31
31
|
unsafeAllowNoAuth?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether the app is running in production. When true, `unsafeAllowNoAuth` is
|
|
34
|
+
* ignored — an unauthenticated dashboard in production is never correct, so
|
|
35
|
+
* the only way in is a real `shouldShow` guard.
|
|
36
|
+
*/
|
|
37
|
+
inProduction?: boolean;
|
|
32
38
|
/** Optional promise that resolves when controllers are initialized. */
|
|
33
39
|
whenReady?: () => Promise<void>;
|
|
34
40
|
/**
|
|
@@ -39,5 +45,9 @@ export interface RegisterRoutesOptions {
|
|
|
39
45
|
}
|
|
40
46
|
/**
|
|
41
47
|
* Register all server-stats routes in a single call.
|
|
48
|
+
*
|
|
49
|
+
* Returns whether the routes were actually registered — false means the
|
|
50
|
+
* fail-closed guard check rejected the configuration, which callers need to
|
|
51
|
+
* know before announcing that anything is reachable.
|
|
42
52
|
*/
|
|
43
|
-
export declare function registerAllRoutes(options: RegisterRoutesOptions):
|
|
53
|
+
export declare function registerAllRoutes(options: RegisterRoutesOptions): boolean;
|
|
@@ -8,16 +8,29 @@ import { log } from '../utils/logger.js';
|
|
|
8
8
|
let _warnedNoAuth = false;
|
|
9
9
|
/**
|
|
10
10
|
* Register all server-stats routes in a single call.
|
|
11
|
+
*
|
|
12
|
+
* Returns whether the routes were actually registered — false means the
|
|
13
|
+
* fail-closed guard check rejected the configuration, which callers need to
|
|
14
|
+
* know before announcing that anything is reachable.
|
|
11
15
|
*/
|
|
12
16
|
export function registerAllRoutes(options) {
|
|
13
17
|
// Fail closed: without an access guard (`shouldShow`) the sensitive routes must
|
|
14
18
|
// not be registered unless the caller explicitly opts in via `unsafeAllowNoAuth`.
|
|
15
19
|
if (!options.shouldShow) {
|
|
20
|
+
// In production the escape hatch does not apply — there is no legitimate
|
|
21
|
+
// reason to serve secrets, email bodies, and SQL to unauthenticated callers
|
|
22
|
+
// on a production host, so the guard is the only way through.
|
|
23
|
+
if (options.inProduction) {
|
|
24
|
+
log.warn('server-stats: production mode is enabled but no `authorize`/`shouldShow` guard is ' +
|
|
25
|
+
'configured — sensitive routes (dashboard, debug API, stats) will NOT be registered. ' +
|
|
26
|
+
'`unsafeAllowNoAuth` is ignored in production. Provide an authorize callback.');
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
16
29
|
if (!options.unsafeAllowNoAuth) {
|
|
17
30
|
log.warn('server-stats: no `authorize`/`shouldShow` guard configured — sensitive routes ' +
|
|
18
31
|
'(dashboard, debug API, stats) will NOT be registered. Provide an authorize callback, ' +
|
|
19
32
|
'or set `unsafeAllowNoAuth: true` to expose them without auth (local dev only).');
|
|
20
|
-
return;
|
|
33
|
+
return false;
|
|
21
34
|
}
|
|
22
35
|
if (!_warnedNoAuth) {
|
|
23
36
|
_warnedNoAuth = true;
|
|
@@ -63,4 +76,5 @@ export function registerAllRoutes(options) {
|
|
|
63
76
|
domain: options.domain,
|
|
64
77
|
});
|
|
65
78
|
}
|
|
79
|
+
return true;
|
|
66
80
|
}
|
|
@@ -19,4 +19,12 @@ export default defineConfig({
|
|
|
19
19
|
|
|
20
20
|
// Log detailed initialization steps (SQLite, migrations, routes, etc.)
|
|
21
21
|
// verbose: true,
|
|
22
|
+
|
|
23
|
+
// Nothing is registered in production unless you opt in here. Requires an
|
|
24
|
+
// `authorize` guard, and data capture stays off until you name a subsystem.
|
|
25
|
+
// production: {
|
|
26
|
+
// enabled: true,
|
|
27
|
+
// capture: { queries: true },
|
|
28
|
+
// retentionDays: 3,
|
|
29
|
+
// },
|
|
22
30
|
})
|
package/dist/src/types.d.ts
CHANGED
|
@@ -435,6 +435,94 @@ export interface DashboardConfig {
|
|
|
435
435
|
*/
|
|
436
436
|
retentionDays?: number;
|
|
437
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Access-control callback signature.
|
|
440
|
+
*
|
|
441
|
+
* May be sync or async — the route guard awaits it. An async guard is the usual
|
|
442
|
+
* shape once the check has to consult `ctx.auth` or the database.
|
|
443
|
+
*
|
|
444
|
+
* One caveat: the `@serverStats()` Edge tag evaluates the guard synchronously
|
|
445
|
+
* while rendering the template, so with an **async** guard the toolbar hides
|
|
446
|
+
* itself rather than risk showing when it shouldn't. The HTTP routes await it
|
|
447
|
+
* properly either way — only the cosmetic bar is affected.
|
|
448
|
+
*/
|
|
449
|
+
export type AccessGuard = (ctx: import('@adonisjs/core/http').HttpContext) => boolean | Promise<boolean>;
|
|
450
|
+
/**
|
|
451
|
+
* Which capture subsystems are active.
|
|
452
|
+
*
|
|
453
|
+
* Each one hooks a global: queries and events subscribe to the Lucid/app
|
|
454
|
+
* emitter, emails subscribe to the mail events, traces wrap every request in
|
|
455
|
+
* `AsyncLocalStorage`. Turning one off means its collector is never subscribed,
|
|
456
|
+
* so it costs nothing — its dashboard pane simply stays empty.
|
|
457
|
+
*
|
|
458
|
+
* Outside production every field defaults to `true`. In production every field
|
|
459
|
+
* defaults to **`false`** and must be opted into individually.
|
|
460
|
+
*/
|
|
461
|
+
export interface CaptureConfig {
|
|
462
|
+
/** SQL text, bindings, and timings for every query. */
|
|
463
|
+
queries?: boolean;
|
|
464
|
+
/** Application events (in-memory only — never persisted to SQLite). */
|
|
465
|
+
events?: boolean;
|
|
466
|
+
/** Sent mail, including subject and body. */
|
|
467
|
+
emails?: boolean;
|
|
468
|
+
/** Per-request spans and the request timeline. */
|
|
469
|
+
traces?: boolean;
|
|
470
|
+
/** Log lines written into the dashboard's SQLite store. */
|
|
471
|
+
logs?: boolean;
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Opt in to running the dashboard in production.
|
|
475
|
+
*
|
|
476
|
+
* By default this package registers **no routes** when `NODE_ENV=production`
|
|
477
|
+
* and never builds the debug or dashboard stores. Setting `enabled: true` lifts
|
|
478
|
+
* that, but only with an {@link ServerStatsConfig.authorize} guard in place —
|
|
479
|
+
* `unsafeAllowNoAuth` is ignored in production.
|
|
480
|
+
*
|
|
481
|
+
* Data capture stays **off** unless you ask for it. Without any `capture`
|
|
482
|
+
* flags you still get the request list, the overview, and the charts (those
|
|
483
|
+
* come from request rows and 1-minute metric buckets), at a small fraction of
|
|
484
|
+
* the write volume of a full dev-mode capture.
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```ts
|
|
488
|
+
* export default defineConfig({
|
|
489
|
+
* authorize: async (ctx) => (await ctx.auth.check()) && ctx.auth.user?.isAdmin === true,
|
|
490
|
+
* dashboard: true,
|
|
491
|
+
* production: {
|
|
492
|
+
* enabled: true,
|
|
493
|
+
* capture: { queries: true },
|
|
494
|
+
* retentionDays: 3,
|
|
495
|
+
* },
|
|
496
|
+
* })
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
export interface ProductionConfig {
|
|
500
|
+
/**
|
|
501
|
+
* Register routes and build the dashboard when `NODE_ENV=production`.
|
|
502
|
+
*
|
|
503
|
+
* Requires an `authorize` guard — without one the routes are still not
|
|
504
|
+
* registered, and a warning explains why.
|
|
505
|
+
*
|
|
506
|
+
* @default false
|
|
507
|
+
*/
|
|
508
|
+
enabled?: boolean;
|
|
509
|
+
/**
|
|
510
|
+
* Which capture subsystems to switch on. Every field defaults to `false` in
|
|
511
|
+
* production, so capture is opt-in one subsystem at a time.
|
|
512
|
+
*
|
|
513
|
+
* @see {@link CaptureConfig}
|
|
514
|
+
*/
|
|
515
|
+
capture?: CaptureConfig;
|
|
516
|
+
/**
|
|
517
|
+
* How many days of history to keep in SQLite, overriding the usual default
|
|
518
|
+
* of 7. Retention deletes rows hourly but never runs `VACUUM`, so the
|
|
519
|
+
* database file reuses pages rather than shrinking — watch actual size via
|
|
520
|
+
* the dashboard's storage panel.
|
|
521
|
+
*
|
|
522
|
+
* @default 3
|
|
523
|
+
*/
|
|
524
|
+
retentionDays?: number;
|
|
525
|
+
}
|
|
438
526
|
/**
|
|
439
527
|
* Advanced options that most users never need to touch.
|
|
440
528
|
*
|
|
@@ -699,7 +787,7 @@ export interface ServerStatsConfig {
|
|
|
699
787
|
*
|
|
700
788
|
* @deprecated Use {@link authorize} instead. Will be removed in the next major version.
|
|
701
789
|
*/
|
|
702
|
-
shouldShow?:
|
|
790
|
+
shouldShow?: AccessGuard;
|
|
703
791
|
/**
|
|
704
792
|
* How often (in **milliseconds**) to run all collectors and
|
|
705
793
|
* broadcast updated stats.
|
|
@@ -750,7 +838,7 @@ export interface ServerStatsConfig {
|
|
|
750
838
|
* authorize: () => process.env.NODE_ENV === 'development'
|
|
751
839
|
* ```
|
|
752
840
|
*/
|
|
753
|
-
authorize?:
|
|
841
|
+
authorize?: AccessGuard;
|
|
754
842
|
/**
|
|
755
843
|
* Register the sensitive dashboard/debug/stats routes WITHOUT any access
|
|
756
844
|
* guard when no {@link authorize} callback is provided.
|
|
@@ -833,6 +921,21 @@ export interface ServerStatsConfig {
|
|
|
833
921
|
* ```
|
|
834
922
|
*/
|
|
835
923
|
domain?: string;
|
|
924
|
+
/**
|
|
925
|
+
* Opt in to running in production, where this package otherwise registers
|
|
926
|
+
* nothing at all.
|
|
927
|
+
*
|
|
928
|
+
* Requires an {@link authorize} guard, and leaves data capture off unless you
|
|
929
|
+
* enable it per subsystem.
|
|
930
|
+
*
|
|
931
|
+
* @see {@link ProductionConfig}
|
|
932
|
+
*
|
|
933
|
+
* @example
|
|
934
|
+
* ```ts
|
|
935
|
+
* production: { enabled: true, capture: { queries: true } }
|
|
936
|
+
* ```
|
|
937
|
+
*/
|
|
938
|
+
production?: ProductionConfig;
|
|
836
939
|
/**
|
|
837
940
|
* Advanced options for fine-tuning internal behavior.
|
|
838
941
|
*
|
|
@@ -878,7 +981,7 @@ export interface ResolvedServerStatsConfig {
|
|
|
878
981
|
/** Optional dev toolbar configuration. */
|
|
879
982
|
devToolbar?: DevToolbarOptions;
|
|
880
983
|
/** Optional access-control callback. */
|
|
881
|
-
shouldShow?:
|
|
984
|
+
shouldShow?: AccessGuard;
|
|
882
985
|
/** Collection interval in milliseconds (new name for {@link intervalMs}). */
|
|
883
986
|
pollInterval?: number;
|
|
884
987
|
/** Whether real-time (SSE) broadcasting is enabled (new name for {@link transport}). */
|
|
@@ -886,7 +989,7 @@ export interface ResolvedServerStatsConfig {
|
|
|
886
989
|
/** HTTP endpoint path or `false` to disable (new name for {@link endpoint}). */
|
|
887
990
|
statsEndpoint?: string | false;
|
|
888
991
|
/** Access-control callback (new name for {@link shouldShow}). */
|
|
889
|
-
authorize?:
|
|
992
|
+
authorize?: AccessGuard;
|
|
890
993
|
/**
|
|
891
994
|
* Escape hatch to register sensitive routes without an access guard.
|
|
892
995
|
* Exposes the dashboard without auth — local development only.
|
|
@@ -902,4 +1005,6 @@ export interface ResolvedServerStatsConfig {
|
|
|
902
1005
|
verbose: boolean;
|
|
903
1006
|
/** Optional domain restriction for all routes. */
|
|
904
1007
|
domain?: string;
|
|
1008
|
+
/** Opt-in production behavior. Absent means this package is inert in production. */
|
|
1009
|
+
production?: ProductionConfig;
|
|
905
1010
|
}
|