adonisjs-server-stats 1.18.0 → 1.18.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.
@@ -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
  }
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.1",
4
4
  "description": "Real-time server monitoring for AdonisJS v6 applications",
5
5
  "keywords": [
6
6
  "adonisjs",