adonisjs-server-stats 1.11.0 → 1.11.2
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/dist/src/provider/dashboard_init.js +15 -0
- package/dist/src/provider/provider_helpers_extra.d.ts +1 -1
- package/dist/src/provider/provider_helpers_extra.js +40 -13
- package/dist/src/provider/server_stats_provider.js +9 -2
- package/dist/src/provider/toolbar_setup.js +17 -2
- package/dist/src/routes/dashboard_routes.js +15 -4
- package/dist/src/utils/app_import.d.ts +11 -10
- package/dist/src/utils/app_import.js +73 -23
- package/package.json +7 -7
|
@@ -18,6 +18,16 @@ export async function initDashboardStore(opts) {
|
|
|
18
18
|
log.info('dashboard: SQLite store ready');
|
|
19
19
|
}
|
|
20
20
|
catch (err) {
|
|
21
|
+
const errMsg = err?.message ?? String(err);
|
|
22
|
+
const errStack = err?.stack ?? '(no stack)';
|
|
23
|
+
// Write to file since console.error may be swallowed by AdonisJS watcher
|
|
24
|
+
try {
|
|
25
|
+
const { writeFileSync } = await import('node:fs');
|
|
26
|
+
writeFileSync(app.makePath('server-stats-dashboard-error.log'), `${new Date().toISOString()}\nError: ${errMsg}\nStack: ${errStack}\n`);
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
29
|
+
console.error('[server-stats] dashboard start error:', errMsg);
|
|
30
|
+
console.error('[server-stats] dashboard start stack:', errStack);
|
|
21
31
|
logDashboardStartError(err);
|
|
22
32
|
onResult({
|
|
23
33
|
dashboardStore: null,
|
|
@@ -107,6 +117,7 @@ function pipeDashRequests(debugStore, dashboardStore) {
|
|
|
107
117
|
});
|
|
108
118
|
}
|
|
109
119
|
function logDashboardStartError(err) {
|
|
120
|
+
const errMsg = err?.message ?? String(err);
|
|
110
121
|
const c = classifyDashboardError(err);
|
|
111
122
|
if (c === 'missing-dep') {
|
|
112
123
|
log.block('Dashboard could not start — missing dependencies. Install with:', [
|
|
@@ -115,7 +126,11 @@ function logDashboardStartError(err) {
|
|
|
115
126
|
'',
|
|
116
127
|
dim('Dashboard has been disabled for this session.'),
|
|
117
128
|
dim('Everything else (stats bar, debug panel) works without it.'),
|
|
129
|
+
'',
|
|
130
|
+
dim(`Error: ${errMsg}`),
|
|
118
131
|
]);
|
|
132
|
+
if (err?.stack)
|
|
133
|
+
console.error(err.stack);
|
|
119
134
|
return;
|
|
120
135
|
}
|
|
121
136
|
if (c === 'timeout') {
|
|
@@ -30,7 +30,7 @@ export declare function setupLogStreamBroadcast(transmit: {
|
|
|
30
30
|
broadcast(ch: string, d: unknown): void;
|
|
31
31
|
}, channelName: string, pinoHookActive: boolean, makePath: (...parts: string[]) => string): LogStreamService | null;
|
|
32
32
|
/** Check if dashboard dependencies are available. Returns true if available, false if missing. */
|
|
33
|
-
export declare function checkDashboardDepsHelper(config: ResolvedServerStatsConfig,
|
|
33
|
+
export declare function checkDashboardDepsHelper(config: ResolvedServerStatsConfig, app: ApplicationService): Promise<boolean>;
|
|
34
34
|
/** Register the Edge.js plugin if Edge is available. Returns true if registered. */
|
|
35
35
|
export declare function registerEdgePluginHelper(app: ApplicationService, config: ResolvedServerStatsConfig): Promise<boolean>;
|
|
36
36
|
/** Set up the publisher-only email bridge for non-web environments. */
|
|
@@ -67,31 +67,58 @@ export function setupLogStreamBroadcast(transmit, channelName, pinoHookActive, m
|
|
|
67
67
|
}
|
|
68
68
|
// ── checkDashboardDepsHelper ────────────────────────────────────
|
|
69
69
|
/** Check if dashboard dependencies are available. Returns true if available, false if missing. */
|
|
70
|
-
export async function checkDashboardDepsHelper(config,
|
|
70
|
+
export async function checkDashboardDepsHelper(config, app) {
|
|
71
71
|
if (!config.devToolbar?.enabled || !config.devToolbar.dashboard)
|
|
72
72
|
return true;
|
|
73
|
-
const {
|
|
73
|
+
const { createRequire } = await import('node:module');
|
|
74
|
+
const { join } = await import('node:path');
|
|
75
|
+
const { pathToFileURL } = await import('node:url');
|
|
74
76
|
const missing = [];
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
const errors = [];
|
|
78
|
+
// Try multiple resolution strategies for monorepo compatibility
|
|
79
|
+
const tryImport = async (specifier) => {
|
|
80
|
+
// Strategy 1: resolve from app root (handles monorepo hoisting)
|
|
81
|
+
const appRoot = app.makePath('');
|
|
82
|
+
try {
|
|
83
|
+
const appRequire = createRequire(join(appRoot, 'package.json'));
|
|
84
|
+
const resolved = appRequire.resolve(specifier);
|
|
85
|
+
await import(pathToFileURL(resolved).href);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
// Strategy 2: resolve from cwd (original approach)
|
|
90
|
+
try {
|
|
91
|
+
const cwdRequire = createRequire(join(process.cwd(), 'package.json'));
|
|
92
|
+
const resolved = cwdRequire.resolve(specifier);
|
|
93
|
+
await import(pathToFileURL(resolved).href);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
// Strategy 3: bare import (works when installed normally)
|
|
98
|
+
try {
|
|
99
|
+
await import(specifier);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
errors.push(`${specifier}: ${err?.message ?? err}`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
if (!(await tryImport('knex')))
|
|
79
108
|
missing.push('knex');
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
await appImport('better-sqlite3');
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
109
|
+
if (!(await tryImport('better-sqlite3')))
|
|
85
110
|
missing.push('better-sqlite3');
|
|
86
|
-
}
|
|
87
111
|
if (missing.length === 0)
|
|
88
112
|
return true;
|
|
89
113
|
log.block(`Dashboard requires ${missing.join(' and ')}. Install with:`, [
|
|
90
114
|
'',
|
|
91
115
|
bold(`npm install ${missing.join(' ')}`),
|
|
92
116
|
'',
|
|
93
|
-
dim('Dashboard
|
|
117
|
+
dim('Dashboard has been disabled for this session.'),
|
|
94
118
|
dim('Everything else (stats bar, debug panel) works without it.'),
|
|
119
|
+
...(errors.length > 0
|
|
120
|
+
? ['', dim('Resolution errors:'), ...errors.map((e) => dim(` ${e}`))]
|
|
121
|
+
: []),
|
|
95
122
|
]);
|
|
96
123
|
return false;
|
|
97
124
|
}
|
|
@@ -56,6 +56,9 @@ export default class ServerStatsProvider {
|
|
|
56
56
|
}
|
|
57
57
|
setVerbose(config.verbose);
|
|
58
58
|
log.info('booting...');
|
|
59
|
+
// Register app root for module resolution (critical in monorepos)
|
|
60
|
+
const { setAppRoot } = await import('../utils/app_import.js');
|
|
61
|
+
setAppRoot(this.app.makePath(''));
|
|
59
62
|
if (config.shouldShow)
|
|
60
63
|
setShouldShow(config.shouldShow);
|
|
61
64
|
await this.registerRoutes(config);
|
|
@@ -155,11 +158,14 @@ export default class ServerStatsProvider {
|
|
|
155
158
|
const pfx = buildExcludedPrefixes(tc, config.endpoint);
|
|
156
159
|
if (pfx.length > 0)
|
|
157
160
|
setExcludedPrefixes(pfx);
|
|
158
|
-
if (!this.debugStore)
|
|
161
|
+
if (!this.debugStore) {
|
|
162
|
+
log.warn('debugStore is null after toolbar setup — apiController will not be created');
|
|
159
163
|
return;
|
|
164
|
+
}
|
|
160
165
|
const { DataAccess: DA } = await import('../data/data_access.js');
|
|
161
166
|
const { ApiController: AC } = await import('../controller/api_controller.js');
|
|
162
167
|
this.apiController = new AC(new DA(this.debugStore, () => this.dashboardStore, this.app.makePath('logs', 'adonisjs.log')));
|
|
168
|
+
log.info('apiController created');
|
|
163
169
|
}
|
|
164
170
|
async setupLogBroadcast() {
|
|
165
171
|
const t = (await this.resolve('transmit'));
|
|
@@ -172,7 +178,8 @@ export default class ServerStatsProvider {
|
|
|
172
178
|
try {
|
|
173
179
|
return await this.app.container.make(binding);
|
|
174
180
|
}
|
|
175
|
-
catch {
|
|
181
|
+
catch (err) {
|
|
182
|
+
log.info(`resolve('${binding}') failed: ${err?.message ?? err}`);
|
|
176
183
|
return null;
|
|
177
184
|
}
|
|
178
185
|
}
|
|
@@ -117,8 +117,15 @@ export function applyToolbarResult(result, tc, provider) {
|
|
|
117
117
|
provider.transmitChannels.push(ch);
|
|
118
118
|
}
|
|
119
119
|
provider.emailBridgeRedis = result.emailBridgeRedis;
|
|
120
|
-
if (!tc.dashboard
|
|
120
|
+
if (!tc.dashboard) {
|
|
121
|
+
log.info('dashboard: skipped — dashboard is disabled in config');
|
|
121
122
|
return;
|
|
123
|
+
}
|
|
124
|
+
if (!provider.dashboardDepsAvailable) {
|
|
125
|
+
log.info('dashboard: skipped — dependencies (knex/better-sqlite3) not available');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
log.info('dashboard: scheduling async initialization...');
|
|
122
129
|
setImmediate(() => {
|
|
123
130
|
initDashboardStore({
|
|
124
131
|
tc,
|
|
@@ -133,9 +140,17 @@ export function applyToolbarResult(result, tc, provider) {
|
|
|
133
140
|
provider.dashboardController = r.dashboardController;
|
|
134
141
|
provider.dashboardLogStream = r.dashboardLogStream;
|
|
135
142
|
provider.dashboardBroadcastTimer = r.dashboardBroadcastTimer;
|
|
143
|
+
if (r.dashboardController) {
|
|
144
|
+
log.info('dashboard: controller ready — API endpoints are now live');
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
log.warn('dashboard: init completed but controller is null — dashboard API will return 503');
|
|
148
|
+
}
|
|
136
149
|
},
|
|
137
150
|
}).catch((e) => {
|
|
138
|
-
log.warn(`dashboard setup failed: ${e?.message ?? e}`);
|
|
151
|
+
log.warn(`dashboard: setup failed: ${e?.message ?? e}`);
|
|
152
|
+
if (e?.stack)
|
|
153
|
+
log.warn(e.stack);
|
|
139
154
|
});
|
|
140
155
|
});
|
|
141
156
|
}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
import { log } from '../utils/logger.js';
|
|
1
2
|
function bindDash(getController, method) {
|
|
2
3
|
return async (ctx) => {
|
|
3
4
|
const controller = getController();
|
|
4
|
-
if (!controller)
|
|
5
|
-
|
|
5
|
+
if (!controller) {
|
|
6
|
+
log.warn(`503 on ${ctx.request.url()} — dashboardController is null (method: ${method}). ` +
|
|
7
|
+
'Dashboard may still be initializing or failed to start. Check for earlier errors.');
|
|
8
|
+
return ctx.response.serviceUnavailable({
|
|
9
|
+
error: 'Dashboard is not available. Check server logs for initialization errors.',
|
|
10
|
+
});
|
|
11
|
+
}
|
|
6
12
|
return controller[method].call(controller, ctx);
|
|
7
13
|
};
|
|
8
14
|
}
|
|
9
15
|
function bindApi(getApi, fn) {
|
|
10
16
|
return async (ctx) => {
|
|
11
17
|
const api = getApi();
|
|
12
|
-
if (!api)
|
|
13
|
-
|
|
18
|
+
if (!api) {
|
|
19
|
+
log.warn(`503 on ${ctx.request.url()} — apiController is null. ` +
|
|
20
|
+
'Dashboard may still be initializing or failed to start. Check for earlier errors.');
|
|
21
|
+
return ctx.response.serviceUnavailable({
|
|
22
|
+
error: 'Dashboard is not available. Check server logs for initialization errors.',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
14
25
|
return fn(api, ctx);
|
|
15
26
|
};
|
|
16
27
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Set the AdonisJS application root for module resolution.
|
|
3
|
+
* In monorepos, cwd may be the workspace root while the app lives
|
|
4
|
+
* in a sub-directory — this ensures we also look there.
|
|
5
|
+
*/
|
|
6
|
+
export declare function setAppRoot(appRoot: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Dynamically import a module, resolving from the app root
|
|
3
9
|
* instead of from this package's location.
|
|
4
10
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* stubs with different module identity than the app's actual packages.
|
|
10
|
-
*
|
|
11
|
-
* Falls back to a normal `import()` when `createRequire` resolution fails
|
|
12
|
-
* (e.g. when the package is installed normally, not symlinked).
|
|
11
|
+
* Tries multiple strategies for monorepo/symlink compatibility:
|
|
12
|
+
* 1. createRequire from each resolution root + ESM import
|
|
13
|
+
* 2. createRequire from each resolution root + CJS require (for native modules)
|
|
14
|
+
* 3. Bare import() fallback
|
|
13
15
|
*/
|
|
14
16
|
export declare function appImport<T = unknown>(specifier: string): Promise<T>;
|
|
15
17
|
/**
|
|
16
18
|
* Same as {@link appImport} but also returns the resolved file path.
|
|
17
|
-
* Useful for diagnostic logging.
|
|
18
19
|
*/
|
|
19
20
|
export declare function appImportWithPath<T = unknown>(specifier: string): Promise<{
|
|
20
21
|
module: T;
|
|
@@ -2,43 +2,93 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Resolution roots used by appImport, in priority order.
|
|
6
|
+
* By default uses process.cwd(). Call {@link setAppRoot} to prepend
|
|
7
|
+
* the AdonisJS app root (important in monorepos where cwd != app root).
|
|
8
|
+
*/
|
|
9
|
+
const resolutionRoots = [process.cwd()];
|
|
10
|
+
/**
|
|
11
|
+
* Set the AdonisJS application root for module resolution.
|
|
12
|
+
* In monorepos, cwd may be the workspace root while the app lives
|
|
13
|
+
* in a sub-directory — this ensures we also look there.
|
|
14
|
+
*/
|
|
15
|
+
export function setAppRoot(appRoot) {
|
|
16
|
+
if (!resolutionRoots.includes(appRoot)) {
|
|
17
|
+
resolutionRoots.unshift(appRoot);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Dynamically import a module, resolving from the app root
|
|
6
22
|
* instead of from this package's location.
|
|
7
23
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* stubs with different module identity than the app's actual packages.
|
|
13
|
-
*
|
|
14
|
-
* Falls back to a normal `import()` when `createRequire` resolution fails
|
|
15
|
-
* (e.g. when the package is installed normally, not symlinked).
|
|
24
|
+
* Tries multiple strategies for monorepo/symlink compatibility:
|
|
25
|
+
* 1. createRequire from each resolution root + ESM import
|
|
26
|
+
* 2. createRequire from each resolution root + CJS require (for native modules)
|
|
27
|
+
* 3. Bare import() fallback
|
|
16
28
|
*/
|
|
17
29
|
export async function appImport(specifier) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
const errors = [];
|
|
31
|
+
for (const root of resolutionRoots) {
|
|
32
|
+
try {
|
|
33
|
+
const appRequire = createRequire(join(root, 'package.json'));
|
|
34
|
+
const resolved = appRequire.resolve(specifier);
|
|
35
|
+
// Strategy A: ESM import via file URL
|
|
36
|
+
try {
|
|
37
|
+
return await import(pathToFileURL(resolved).href);
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
// Strategy B: CJS require (works for native addons like better-sqlite3)
|
|
41
|
+
try {
|
|
42
|
+
return appRequire(specifier);
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
errors.push(err);
|
|
48
|
+
}
|
|
22
49
|
}
|
|
23
|
-
|
|
24
|
-
|
|
50
|
+
// Strategy C: bare import (works when installed normally, not symlinked)
|
|
51
|
+
try {
|
|
25
52
|
return await import(specifier);
|
|
26
53
|
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
errors.push(err);
|
|
56
|
+
}
|
|
57
|
+
throw errors[0] ?? new Error(`Could not import '${specifier}'`);
|
|
27
58
|
}
|
|
28
59
|
/**
|
|
29
60
|
* Same as {@link appImport} but also returns the resolved file path.
|
|
30
|
-
* Useful for diagnostic logging.
|
|
31
61
|
*/
|
|
32
62
|
export async function appImportWithPath(specifier) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
const errors = [];
|
|
64
|
+
for (const root of resolutionRoots) {
|
|
65
|
+
try {
|
|
66
|
+
const appRequire = createRequire(join(root, 'package.json'));
|
|
67
|
+
const resolved = appRequire.resolve(specifier);
|
|
68
|
+
// Strategy A: ESM import via file URL
|
|
69
|
+
try {
|
|
70
|
+
const module = await import(pathToFileURL(resolved).href);
|
|
71
|
+
return { module: module, resolvedPath: resolved };
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
// Strategy B: CJS require (works for native addons like better-sqlite3)
|
|
75
|
+
try {
|
|
76
|
+
const module = appRequire(specifier);
|
|
77
|
+
return { module, resolvedPath: resolved };
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
errors.push(err);
|
|
83
|
+
}
|
|
38
84
|
}
|
|
39
|
-
|
|
40
|
-
|
|
85
|
+
// Strategy C: bare import
|
|
86
|
+
try {
|
|
41
87
|
const module = await import(specifier);
|
|
42
88
|
return { module: module, resolvedPath: `(bare import: ${specifier})` };
|
|
43
89
|
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
errors.push(err);
|
|
92
|
+
}
|
|
93
|
+
throw errors[0] ?? new Error(`Could not import '${specifier}'`);
|
|
44
94
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adonisjs-server-stats",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.2",
|
|
4
4
|
"description": "Real-time server monitoring for AdonisJS v6 applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adonisjs",
|
|
@@ -152,13 +152,13 @@
|
|
|
152
152
|
"vue": "^3.5.29"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
155
|
-
"@adonisjs/core": "^6.0.0 || ^7.0.0
|
|
156
|
-
"@adonisjs/lucid": "^21.0.0",
|
|
157
|
-
"@adonisjs/redis": "^9.0.0",
|
|
158
|
-
"@adonisjs/transmit": "^1.0.0",
|
|
155
|
+
"@adonisjs/core": "^6.0.0 || ^7.0.0",
|
|
156
|
+
"@adonisjs/lucid": "^21.0.0 || ^22.0.0",
|
|
157
|
+
"@adonisjs/redis": "^9.0.0 || ^10.0.0",
|
|
158
|
+
"@adonisjs/transmit": "^1.0.0 || ^3.0.0",
|
|
159
159
|
"@adonisjs/transmit-client": "^1.0.0",
|
|
160
|
-
"@julr/adonisjs-prometheus": "^1.4.0",
|
|
161
|
-
"better-sqlite3": "^7.0.0 || ^11.0.0",
|
|
160
|
+
"@julr/adonisjs-prometheus": "^1.4.0 || ^2.0.0",
|
|
161
|
+
"better-sqlite3": "^7.0.0 || ^11.0.0 || ^12.0.0",
|
|
162
162
|
"bullmq": "^5.0.0",
|
|
163
163
|
"edge.js": "^6.0.0",
|
|
164
164
|
"knex": "^3.0.0",
|