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 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
  */
@@ -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
- // Cap the array to prevent unbounded growth under high log volume
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: Date.now(), level });
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
- const newBytes = stats.size - this.lastSize;
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, this.lastSize).finally(() => fd.close());
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.recentEntries.push({ time, level });
144
+ this.pushRecent(time, level);
128
145
  this.onEntry?.(entry);
129
146
  }
130
147
  }
131
148
  }
132
149
  catch (err) {
133
- if (!this.warnedPollFailure) {
134
- this.warnedPollFailure = true;
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
- /** Register the Edge.js plugin if Edge is available. Returns true if registered. */
37
- export declare function registerEdgePluginHelper(app: ApplicationService, config: ResolvedServerStatsConfig): Promise<boolean>;
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
- /** Register the Edge.js plugin if Edge is available. Returns true if registered. */
142
- export async function registerEdgePluginHelper(app, config) {
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 true;
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
- // Only register the Edge tag when the routes it polls actually exist —
77
- // not merely when the environment allows them. The fail-closed guard in
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
- this.edgePluginActive = routesRegistered
82
- ? await registerEdgePluginHelper(this.app, config)
83
- : false;
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adonisjs-server-stats",
3
- "version": "1.18.0",
3
+ "version": "1.18.2",
4
4
  "description": "Real-time server monitoring for AdonisJS v6 applications",
5
5
  "keywords": [
6
6
  "adonisjs",