adonisjs-server-stats 1.18.0 → 1.18.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/README.md +1 -1
- package/dist/src/edge/plugin.d.ts +9 -0
- package/dist/src/edge/plugin.js +18 -0
- package/dist/src/log_stream/log_stream_service.d.ts +5 -0
- package/dist/src/log_stream/log_stream_service.js +35 -10
- package/dist/src/provider/provider_helpers_extra.d.ts +8 -2
- package/dist/src/provider/provider_helpers_extra.js +11 -5
- package/dist/src/provider/server_stats_provider.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -412,7 +412,7 @@ export default defineConfig({
|
|
|
412
412
|
|
|
413
413
|
## Production
|
|
414
414
|
|
|
415
|
-
By default this package does **nothing** when `NODE_ENV=production` -- no routes are registered and neither the debug nor the dashboard store is built. The metrics engine still runs, so `onStats`, Prometheus, and Transmit broadcasting keep working; there is simply no HTTP surface.
|
|
415
|
+
By default this package does **nothing** when `NODE_ENV=production` -- no routes are registered and neither the debug nor the dashboard store is built. The metrics engine still runs, so `onStats`, Prometheus, and Transmit broadcasting keep working; there is simply no HTTP surface. The `@serverStats()` Edge tag stays registered and renders nothing, so it can live in your layout unconditionally.
|
|
416
416
|
|
|
417
417
|
Set `production.enabled` to lift that:
|
|
418
418
|
|
|
@@ -31,6 +31,15 @@ interface EdgeToken {
|
|
|
31
31
|
};
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Edge plugin that registers `@serverStats()` as a tag that renders nothing.
|
|
36
|
+
*
|
|
37
|
+
* Edge prints an unregistered `@tag()` line into the page verbatim, so a layout
|
|
38
|
+
* that keeps `@serverStats()` in place must still find a tag under that name
|
|
39
|
+
* when the stats bar is off (production without `production.enabled`, routes
|
|
40
|
+
* refused by the fail-closed guard). This one reads no assets and emits nothing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function edgePluginServerStatsInert(): (edge: EdgeEngine) => void;
|
|
34
43
|
/**
|
|
35
44
|
* Edge plugin that registers the `@serverStats()` tag.
|
|
36
45
|
*/
|
package/dist/src/edge/plugin.js
CHANGED
|
@@ -59,6 +59,24 @@ function buildTemplateState(config, clientDir) {
|
|
|
59
59
|
}
|
|
60
60
|
return state;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Edge plugin that registers `@serverStats()` as a tag that renders nothing.
|
|
64
|
+
*
|
|
65
|
+
* Edge prints an unregistered `@tag()` line into the page verbatim, so a layout
|
|
66
|
+
* that keeps `@serverStats()` in place must still find a tag under that name
|
|
67
|
+
* when the stats bar is off (production without `production.enabled`, routes
|
|
68
|
+
* refused by the fail-closed guard). This one reads no assets and emits nothing.
|
|
69
|
+
*/
|
|
70
|
+
export function edgePluginServerStatsInert() {
|
|
71
|
+
return (edge) => {
|
|
72
|
+
edge.registerTag({
|
|
73
|
+
tagName: 'serverStats',
|
|
74
|
+
block: false,
|
|
75
|
+
seekable: true,
|
|
76
|
+
compile() { },
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
}
|
|
62
80
|
/**
|
|
63
81
|
* Edge plugin that registers the `@serverStats()` tag.
|
|
64
82
|
*/
|
|
@@ -3,6 +3,7 @@ export declare function parseAndEnrich(line: string): Record<string, unknown> |
|
|
|
3
3
|
export declare class LogStreamService {
|
|
4
4
|
private recentEntries;
|
|
5
5
|
private static readonly MAX_RECENT_ENTRIES;
|
|
6
|
+
private static readonly MAX_POLL_BYTES;
|
|
6
7
|
private lastSize;
|
|
7
8
|
private intervalId;
|
|
8
9
|
private logPath;
|
|
@@ -16,8 +17,12 @@ export declare class LogStreamService {
|
|
|
16
17
|
* in real-time without file polling.
|
|
17
18
|
*/
|
|
18
19
|
ingest(entry: Record<string, unknown>): void;
|
|
20
|
+
/** Record a timestamp, capping the array to prevent unbounded growth under high log volume. */
|
|
21
|
+
private pushRecent;
|
|
19
22
|
getLogStats(): LogStats;
|
|
20
23
|
start(): Promise<void>;
|
|
21
24
|
stop(): void;
|
|
25
|
+
/** Reset the once-per-streak failure flag after a fully successful poll. */
|
|
26
|
+
private markPollHealthy;
|
|
22
27
|
private pollNewEntries;
|
|
23
28
|
}
|
|
@@ -27,6 +27,7 @@ export function parseAndEnrich(line) {
|
|
|
27
27
|
export class LogStreamService {
|
|
28
28
|
recentEntries = [];
|
|
29
29
|
static MAX_RECENT_ENTRIES = 10_000;
|
|
30
|
+
static MAX_POLL_BYTES = 4 * 1024 * 1024;
|
|
30
31
|
lastSize = 0;
|
|
31
32
|
intervalId = null;
|
|
32
33
|
logPath;
|
|
@@ -44,12 +45,15 @@ export class LogStreamService {
|
|
|
44
45
|
*/
|
|
45
46
|
ingest(entry) {
|
|
46
47
|
const level = typeof entry.level === 'number' ? entry.level : 30;
|
|
47
|
-
|
|
48
|
+
this.pushRecent(Date.now(), level);
|
|
49
|
+
this.onEntry?.(entry);
|
|
50
|
+
}
|
|
51
|
+
/** Record a timestamp, capping the array to prevent unbounded growth under high log volume. */
|
|
52
|
+
pushRecent(time, level) {
|
|
48
53
|
if (this.recentEntries.length >= LogStreamService.MAX_RECENT_ENTRIES) {
|
|
49
54
|
this.recentEntries.splice(0, Math.floor(LogStreamService.MAX_RECENT_ENTRIES / 4));
|
|
50
55
|
}
|
|
51
|
-
this.recentEntries.push({ time
|
|
52
|
-
this.onEntry?.(entry);
|
|
56
|
+
this.recentEntries.push({ time, level });
|
|
53
57
|
}
|
|
54
58
|
getLogStats() {
|
|
55
59
|
const now = Date.now();
|
|
@@ -102,36 +106,57 @@ export class LogStreamService {
|
|
|
102
106
|
this.intervalId = null;
|
|
103
107
|
}
|
|
104
108
|
}
|
|
109
|
+
/** Reset the once-per-streak failure flag after a fully successful poll. */
|
|
110
|
+
markPollHealthy() {
|
|
111
|
+
if (this.warnedPollFailure) {
|
|
112
|
+
this.warnedPollFailure = false;
|
|
113
|
+
log.info('log stream: log file is readable again — resuming');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
105
116
|
async pollNewEntries() {
|
|
106
117
|
if (!this.logPath)
|
|
107
118
|
return;
|
|
108
119
|
try {
|
|
109
|
-
this.warnedPollFailure = false;
|
|
110
120
|
const stats = await stat(this.logPath);
|
|
111
121
|
// File was truncated/rotated — reset
|
|
112
122
|
if (stats.size < this.lastSize) {
|
|
113
123
|
this.lastSize = 0;
|
|
114
124
|
}
|
|
115
|
-
if (stats.size <= this.lastSize)
|
|
125
|
+
if (stats.size <= this.lastSize) {
|
|
126
|
+
this.markPollHealthy();
|
|
116
127
|
return;
|
|
117
|
-
|
|
128
|
+
}
|
|
129
|
+
// Cap each read so a rotation reset or burst can never allocate the
|
|
130
|
+
// whole backlog in one buffer; skip ahead and read only the tail.
|
|
131
|
+
// A partial first line after skipping fails JSON.parse and is dropped.
|
|
132
|
+
const readFrom = Math.max(this.lastSize, stats.size - LogStreamService.MAX_POLL_BYTES);
|
|
133
|
+
const newBytes = stats.size - readFrom;
|
|
118
134
|
const buffer = Buffer.alloc(newBytes);
|
|
119
135
|
const fd = await open(this.logPath, 'r');
|
|
120
|
-
await fd.read(buffer, 0, newBytes,
|
|
136
|
+
await fd.read(buffer, 0, newBytes, readFrom).finally(() => fd.close());
|
|
121
137
|
this.lastSize = stats.size;
|
|
138
|
+
this.markPollHealthy();
|
|
122
139
|
for (const line of buffer.toString('utf-8').trim().split('\n')) {
|
|
123
140
|
const entry = parseAndEnrich(line);
|
|
124
141
|
if (entry) {
|
|
125
142
|
const level = typeof entry.level === 'number' ? entry.level : 30;
|
|
126
143
|
const time = typeof entry.time === 'number' ? entry.time : Date.now();
|
|
127
|
-
this.
|
|
144
|
+
this.pushRecent(time, level);
|
|
128
145
|
this.onEntry?.(entry);
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
148
|
}
|
|
132
149
|
catch (err) {
|
|
133
|
-
if (
|
|
134
|
-
|
|
150
|
+
if (this.warnedPollFailure)
|
|
151
|
+
return;
|
|
152
|
+
this.warnedPollFailure = true;
|
|
153
|
+
// A missing file is a normal state for the fallback poller — the file
|
|
154
|
+
// appears once the app writes its first log line. Anything else
|
|
155
|
+
// (permissions, a directory in the way) deserves a real warning.
|
|
156
|
+
if (err?.code === 'ENOENT') {
|
|
157
|
+
log.info('log stream: log file not found (will keep watching) — ' + this.logPath);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
135
160
|
log.warn('log stream: cannot read log file — ' + err?.message);
|
|
136
161
|
}
|
|
137
162
|
}
|
|
@@ -33,8 +33,14 @@ export declare function setupLogStreamBroadcast(transmit: {
|
|
|
33
33
|
}, channelName: string, pinoHookActive: boolean, makePath: (...parts: string[]) => string): LogStreamService | null;
|
|
34
34
|
/** Check if dashboard dependencies are available. Returns true if available, false if missing. */
|
|
35
35
|
export declare function checkDashboardDepsHelper(config: ResolvedServerStatsConfig, app: ApplicationService): Promise<boolean>;
|
|
36
|
-
/**
|
|
37
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Register the `@serverStats()` Edge tag if Edge is available.
|
|
38
|
+
*
|
|
39
|
+
* The tag is always registered so the layout never prints it as text. With
|
|
40
|
+
* `renderBar` it renders the stats bar; without it the tag compiles to nothing.
|
|
41
|
+
* Returns true only when the bar will render.
|
|
42
|
+
*/
|
|
43
|
+
export declare function registerEdgePluginHelper(app: ApplicationService, config: ResolvedServerStatsConfig, renderBar: boolean): Promise<boolean>;
|
|
38
44
|
/** Set up the publisher-only email bridge for non-web environments. */
|
|
39
45
|
export declare function setupNonWebBridgeHelper(emitter: unknown, channel: string): Promise<void>;
|
|
40
46
|
interface StatsIntervalResult {
|
|
@@ -138,16 +138,22 @@ export async function checkDashboardDepsHelper(config, app) {
|
|
|
138
138
|
return false;
|
|
139
139
|
}
|
|
140
140
|
// ── registerEdgePluginHelper ────────────────────────────────────
|
|
141
|
-
/**
|
|
142
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Register the `@serverStats()` Edge tag if Edge is available.
|
|
143
|
+
*
|
|
144
|
+
* The tag is always registered so the layout never prints it as text. With
|
|
145
|
+
* `renderBar` it renders the stats bar; without it the tag compiles to nothing.
|
|
146
|
+
* Returns true only when the bar will render.
|
|
147
|
+
*/
|
|
148
|
+
export async function registerEdgePluginHelper(app, config, renderBar) {
|
|
143
149
|
if (!app.usingEdgeJS)
|
|
144
150
|
return false;
|
|
145
151
|
try {
|
|
146
152
|
const { appImport } = await import('../utils/app_import.js');
|
|
147
153
|
const edge = await appImport('edge.js');
|
|
148
|
-
const { edgePluginServerStats } = await import('../edge/plugin.js');
|
|
149
|
-
edge.default.use(edgePluginServerStats(config));
|
|
150
|
-
return
|
|
154
|
+
const { edgePluginServerStats, edgePluginServerStatsInert } = await import('../edge/plugin.js');
|
|
155
|
+
edge.default.use(renderBar ? edgePluginServerStats(config) : edgePluginServerStatsInert());
|
|
156
|
+
return renderBar;
|
|
151
157
|
}
|
|
152
158
|
catch (err) {
|
|
153
159
|
log.warn('could not register Edge plugin: ' + err?.message);
|
|
@@ -73,14 +73,14 @@ export default class ServerStatsProvider {
|
|
|
73
73
|
if (config.shouldShow)
|
|
74
74
|
setShouldShow(config.shouldShow);
|
|
75
75
|
const routesRegistered = await this.registerRoutes(config);
|
|
76
|
-
//
|
|
77
|
-
//
|
|
76
|
+
// The bar renders only when the routes it polls actually exist — not
|
|
77
|
+
// merely when the environment allows them. The fail-closed guard in
|
|
78
78
|
// registerAllRoutes can refuse registration (no authorize callback), and
|
|
79
79
|
// rendering the bar then would advertise endpoints that 404 — or, in
|
|
80
80
|
// production, hand every anonymous visitor a pointer at the stats API.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
// The tag itself is registered either way: Edge prints an unregistered
|
|
82
|
+
// `@serverStats()` into the page as literal text.
|
|
83
|
+
this.edgePluginActive = await registerEdgePluginHelper(this.app, config, routesRegistered);
|
|
84
84
|
}
|
|
85
85
|
/**
|
|
86
86
|
* Whether this package should do anything in the current environment.
|