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
|
-
|
|
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
|
}
|