@logtape/logtape 1.2.2 → 1.2.4
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/package.json +4 -1
- package/deno.json +0 -36
- package/src/config.test.ts +0 -589
- package/src/config.ts +0 -409
- package/src/context.test.ts +0 -187
- package/src/context.ts +0 -55
- package/src/filter.test.ts +0 -84
- package/src/filter.ts +0 -64
- package/src/fixtures.ts +0 -35
- package/src/formatter.test.ts +0 -569
- package/src/formatter.ts +0 -961
- package/src/level.test.ts +0 -71
- package/src/level.ts +0 -86
- package/src/logger.test.ts +0 -1508
- package/src/logger.ts +0 -1788
- package/src/mod.ts +0 -59
- package/src/record.ts +0 -49
- package/src/sink.test.ts +0 -2590
- package/src/sink.ts +0 -976
- package/src/util.deno.ts +0 -19
- package/src/util.node.ts +0 -12
- package/src/util.ts +0 -11
- package/tsdown.config.ts +0 -24
package/src/sink.test.ts
DELETED
|
@@ -1,2590 +0,0 @@
|
|
|
1
|
-
import { suite } from "@alinea/suite";
|
|
2
|
-
import { assert } from "@std/assert/assert";
|
|
3
|
-
import { assertEquals } from "@std/assert/equals";
|
|
4
|
-
import { assertInstanceOf } from "@std/assert/instance-of";
|
|
5
|
-
import { assertThrows } from "@std/assert/throws";
|
|
6
|
-
import { delay } from "@std/async/delay";
|
|
7
|
-
import makeConsoleMock from "consolemock";
|
|
8
|
-
import { debug, error, fatal, info, trace, warning } from "./fixtures.ts";
|
|
9
|
-
import { defaultTextFormatter } from "./formatter.ts";
|
|
10
|
-
import type { LogLevel } from "./level.ts";
|
|
11
|
-
import type { LogRecord } from "./record.ts";
|
|
12
|
-
import {
|
|
13
|
-
type AsyncSink,
|
|
14
|
-
fingersCrossed,
|
|
15
|
-
fromAsyncSink,
|
|
16
|
-
getConsoleSink,
|
|
17
|
-
getStreamSink,
|
|
18
|
-
type Sink,
|
|
19
|
-
withFilter,
|
|
20
|
-
} from "./sink.ts";
|
|
21
|
-
|
|
22
|
-
const test = suite(import.meta);
|
|
23
|
-
|
|
24
|
-
test("withFilter()", () => {
|
|
25
|
-
const buffer: LogRecord[] = [];
|
|
26
|
-
const sink = withFilter(buffer.push.bind(buffer), "warning");
|
|
27
|
-
sink(trace);
|
|
28
|
-
sink(debug);
|
|
29
|
-
sink(info);
|
|
30
|
-
sink(warning);
|
|
31
|
-
sink(error);
|
|
32
|
-
sink(fatal);
|
|
33
|
-
assertEquals(buffer, [warning, error, fatal]);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
interface ConsoleMock extends Console {
|
|
37
|
-
history(): unknown[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
test("getStreamSink()", async () => {
|
|
41
|
-
let buffer: string = "";
|
|
42
|
-
const decoder = new TextDecoder();
|
|
43
|
-
const sink = getStreamSink(
|
|
44
|
-
new WritableStream({
|
|
45
|
-
write(chunk: Uint8Array) {
|
|
46
|
-
buffer += decoder.decode(chunk);
|
|
47
|
-
return Promise.resolve();
|
|
48
|
-
},
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
51
|
-
sink(trace);
|
|
52
|
-
sink(debug);
|
|
53
|
-
sink(info);
|
|
54
|
-
sink(warning);
|
|
55
|
-
sink(error);
|
|
56
|
-
sink(fatal);
|
|
57
|
-
await sink[Symbol.asyncDispose]();
|
|
58
|
-
assertEquals(
|
|
59
|
-
buffer,
|
|
60
|
-
`\
|
|
61
|
-
2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
62
|
-
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
63
|
-
2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
|
|
64
|
-
2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!
|
|
65
|
-
2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!
|
|
66
|
-
2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!
|
|
67
|
-
`,
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("getStreamSink() with nonBlocking - simple boolean", async () => {
|
|
72
|
-
let buffer: string = "";
|
|
73
|
-
const decoder = new TextDecoder();
|
|
74
|
-
const sink = getStreamSink(
|
|
75
|
-
new WritableStream({
|
|
76
|
-
write(chunk: Uint8Array) {
|
|
77
|
-
buffer += decoder.decode(chunk);
|
|
78
|
-
return Promise.resolve();
|
|
79
|
-
},
|
|
80
|
-
}),
|
|
81
|
-
{ nonBlocking: true },
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
// Check that it returns AsyncDisposable
|
|
85
|
-
assertInstanceOf(sink, Function);
|
|
86
|
-
assert(Symbol.asyncDispose in sink);
|
|
87
|
-
|
|
88
|
-
// Add records - they should not be written immediately
|
|
89
|
-
sink(trace);
|
|
90
|
-
sink(debug);
|
|
91
|
-
assertEquals(buffer, ""); // Not written yet
|
|
92
|
-
|
|
93
|
-
// Wait for flush interval (default 100ms)
|
|
94
|
-
await delay(150);
|
|
95
|
-
assertEquals(
|
|
96
|
-
buffer,
|
|
97
|
-
`2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
98
|
-
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
99
|
-
`,
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
await sink[Symbol.asyncDispose]();
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test("getStreamSink() with nonBlocking - custom buffer config", async () => {
|
|
106
|
-
let buffer: string = "";
|
|
107
|
-
const decoder = new TextDecoder();
|
|
108
|
-
const sink = getStreamSink(
|
|
109
|
-
new WritableStream({
|
|
110
|
-
write(chunk: Uint8Array) {
|
|
111
|
-
buffer += decoder.decode(chunk);
|
|
112
|
-
return Promise.resolve();
|
|
113
|
-
},
|
|
114
|
-
}),
|
|
115
|
-
{
|
|
116
|
-
nonBlocking: {
|
|
117
|
-
bufferSize: 2,
|
|
118
|
-
flushInterval: 50,
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
// Add records up to buffer size
|
|
124
|
-
sink(trace);
|
|
125
|
-
assertEquals(buffer, ""); // Not flushed yet
|
|
126
|
-
|
|
127
|
-
sink(debug); // This should trigger immediate flush (buffer size = 2)
|
|
128
|
-
await delay(10); // Small delay for async flush
|
|
129
|
-
assertEquals(
|
|
130
|
-
buffer,
|
|
131
|
-
`2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
132
|
-
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
133
|
-
`,
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// Add more records
|
|
137
|
-
const prevLength = buffer.length;
|
|
138
|
-
sink(info);
|
|
139
|
-
assertEquals(buffer.length, prevLength); // Not flushed yet
|
|
140
|
-
|
|
141
|
-
// Wait for flush interval
|
|
142
|
-
await delay(60);
|
|
143
|
-
assertEquals(
|
|
144
|
-
buffer.substring(prevLength),
|
|
145
|
-
`2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
|
|
146
|
-
`,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
await sink[Symbol.asyncDispose]();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("getStreamSink() with nonBlocking - no operations after dispose", async () => {
|
|
153
|
-
let buffer: string = "";
|
|
154
|
-
const decoder = new TextDecoder();
|
|
155
|
-
const sink = getStreamSink(
|
|
156
|
-
new WritableStream({
|
|
157
|
-
write(chunk: Uint8Array) {
|
|
158
|
-
buffer += decoder.decode(chunk);
|
|
159
|
-
return Promise.resolve();
|
|
160
|
-
},
|
|
161
|
-
}),
|
|
162
|
-
{ nonBlocking: true },
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
// Dispose immediately
|
|
166
|
-
await sink[Symbol.asyncDispose]();
|
|
167
|
-
|
|
168
|
-
// Try to add records after dispose
|
|
169
|
-
sink(trace);
|
|
170
|
-
sink(debug);
|
|
171
|
-
|
|
172
|
-
// No records should be written
|
|
173
|
-
assertEquals(buffer, "");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("getStreamSink() with nonBlocking - error handling", async () => {
|
|
177
|
-
const sink = getStreamSink(
|
|
178
|
-
new WritableStream({
|
|
179
|
-
write() {
|
|
180
|
-
return Promise.reject(new Error("Write error"));
|
|
181
|
-
},
|
|
182
|
-
}),
|
|
183
|
-
{ nonBlocking: true },
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// Should not throw when adding records
|
|
187
|
-
sink(trace);
|
|
188
|
-
sink(info);
|
|
189
|
-
sink(error);
|
|
190
|
-
|
|
191
|
-
// Wait for flush - errors should be silently ignored
|
|
192
|
-
await delay(150);
|
|
193
|
-
|
|
194
|
-
// Dispose - should not throw
|
|
195
|
-
await sink[Symbol.asyncDispose]();
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("getStreamSink() with nonBlocking - flush on dispose", async () => {
|
|
199
|
-
let buffer: string = "";
|
|
200
|
-
const decoder = new TextDecoder();
|
|
201
|
-
const sink = getStreamSink(
|
|
202
|
-
new WritableStream({
|
|
203
|
-
write(chunk: Uint8Array) {
|
|
204
|
-
buffer += decoder.decode(chunk);
|
|
205
|
-
return Promise.resolve();
|
|
206
|
-
},
|
|
207
|
-
}),
|
|
208
|
-
{
|
|
209
|
-
nonBlocking: {
|
|
210
|
-
bufferSize: 100,
|
|
211
|
-
flushInterval: 5000, // Very long interval
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
// Add records
|
|
217
|
-
sink(trace);
|
|
218
|
-
sink(debug);
|
|
219
|
-
sink(info);
|
|
220
|
-
assertEquals(buffer, ""); // Not flushed yet due to large buffer and long interval
|
|
221
|
-
|
|
222
|
-
// Dispose should flush all remaining records
|
|
223
|
-
await sink[Symbol.asyncDispose]();
|
|
224
|
-
assertEquals(
|
|
225
|
-
buffer,
|
|
226
|
-
`2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!
|
|
227
|
-
2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!
|
|
228
|
-
2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!
|
|
229
|
-
`,
|
|
230
|
-
);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test("getStreamSink() with nonBlocking - buffer overflow protection", async () => {
|
|
234
|
-
let buffer: string = "";
|
|
235
|
-
const decoder = new TextDecoder();
|
|
236
|
-
let recordsReceived = 0;
|
|
237
|
-
const sink = getStreamSink(
|
|
238
|
-
new WritableStream({
|
|
239
|
-
write(chunk: Uint8Array) {
|
|
240
|
-
const text = decoder.decode(chunk);
|
|
241
|
-
buffer += text;
|
|
242
|
-
// Count how many log records we actually receive
|
|
243
|
-
recordsReceived += text.split("\n").filter((line) =>
|
|
244
|
-
line.trim() !== ""
|
|
245
|
-
).length;
|
|
246
|
-
return Promise.resolve();
|
|
247
|
-
},
|
|
248
|
-
}),
|
|
249
|
-
{
|
|
250
|
-
nonBlocking: {
|
|
251
|
-
bufferSize: 3,
|
|
252
|
-
flushInterval: 50, // Short interval to ensure flushes happen
|
|
253
|
-
},
|
|
254
|
-
},
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
// Add many more records than maxBufferSize (6) very rapidly
|
|
258
|
-
// This should trigger multiple flushes and potentially overflow protection
|
|
259
|
-
for (let i = 0; i < 20; i++) {
|
|
260
|
-
sink(trace);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Wait for all flushes to complete
|
|
264
|
-
await delay(200);
|
|
265
|
-
|
|
266
|
-
// Force final flush
|
|
267
|
-
await sink[Symbol.asyncDispose]();
|
|
268
|
-
|
|
269
|
-
// Due to overflow protection, we should receive fewer than 20 records
|
|
270
|
-
// The exact number depends on timing, but some should be dropped
|
|
271
|
-
assert(
|
|
272
|
-
recordsReceived < 20,
|
|
273
|
-
`Expected < 20 records due to potential overflow, got ${recordsReceived}`,
|
|
274
|
-
);
|
|
275
|
-
assert(recordsReceived > 0, "Expected some records to be logged");
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test("getStreamSink() with nonBlocking - high volume non-blocking behavior", async () => {
|
|
279
|
-
let buffer: string = "";
|
|
280
|
-
const decoder = new TextDecoder();
|
|
281
|
-
const sink = getStreamSink(
|
|
282
|
-
new WritableStream({
|
|
283
|
-
write(chunk: Uint8Array) {
|
|
284
|
-
buffer += decoder.decode(chunk);
|
|
285
|
-
return Promise.resolve();
|
|
286
|
-
},
|
|
287
|
-
}),
|
|
288
|
-
{
|
|
289
|
-
nonBlocking: {
|
|
290
|
-
bufferSize: 3,
|
|
291
|
-
flushInterval: 50,
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
// Simulate rapid logging - this should not block
|
|
297
|
-
const startTime = performance.now();
|
|
298
|
-
for (let i = 0; i < 100; i++) {
|
|
299
|
-
sink(trace);
|
|
300
|
-
}
|
|
301
|
-
const endTime = performance.now();
|
|
302
|
-
|
|
303
|
-
// Adding logs should be very fast (non-blocking)
|
|
304
|
-
const duration = endTime - startTime;
|
|
305
|
-
assert(
|
|
306
|
-
duration < 100,
|
|
307
|
-
`Adding 100 logs took ${duration}ms, should be much faster`,
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
// Wait for flushes to complete
|
|
311
|
-
await delay(200);
|
|
312
|
-
|
|
313
|
-
// Should have logged some records
|
|
314
|
-
assert(buffer.length > 0, "Expected some records to be logged");
|
|
315
|
-
|
|
316
|
-
await sink[Symbol.asyncDispose]();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
test("getConsoleSink()", () => {
|
|
320
|
-
// @ts-ignore: consolemock is not typed
|
|
321
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
322
|
-
const sink = getConsoleSink({ console: mock });
|
|
323
|
-
sink(trace);
|
|
324
|
-
sink(debug);
|
|
325
|
-
sink(info);
|
|
326
|
-
sink(warning);
|
|
327
|
-
sink(error);
|
|
328
|
-
sink(fatal);
|
|
329
|
-
assertEquals(mock.history(), [
|
|
330
|
-
{
|
|
331
|
-
DEBUG: [
|
|
332
|
-
"%c22:13:20.000 %cTRC%c %cmy-app·junk %cHello, %o & %o!",
|
|
333
|
-
"color: gray;",
|
|
334
|
-
"background-color: gray; color: white;",
|
|
335
|
-
"background-color: default;",
|
|
336
|
-
"color: gray;",
|
|
337
|
-
"color: default;",
|
|
338
|
-
123,
|
|
339
|
-
456,
|
|
340
|
-
],
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
DEBUG: [
|
|
344
|
-
"%c22:13:20.000 %cDBG%c %cmy-app·junk %cHello, %o & %o!",
|
|
345
|
-
"color: gray;",
|
|
346
|
-
"background-color: gray; color: white;",
|
|
347
|
-
"background-color: default;",
|
|
348
|
-
"color: gray;",
|
|
349
|
-
"color: default;",
|
|
350
|
-
123,
|
|
351
|
-
456,
|
|
352
|
-
],
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
INFO: [
|
|
356
|
-
"%c22:13:20.000 %cINF%c %cmy-app·junk %cHello, %o & %o!",
|
|
357
|
-
"color: gray;",
|
|
358
|
-
"background-color: white; color: black;",
|
|
359
|
-
"background-color: default;",
|
|
360
|
-
"color: gray;",
|
|
361
|
-
"color: default;",
|
|
362
|
-
123,
|
|
363
|
-
456,
|
|
364
|
-
],
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
WARN: [
|
|
368
|
-
"%c22:13:20.000 %cWRN%c %cmy-app·junk %cHello, %o & %o!",
|
|
369
|
-
"color: gray;",
|
|
370
|
-
"background-color: orange; color: black;",
|
|
371
|
-
"background-color: default;",
|
|
372
|
-
"color: gray;",
|
|
373
|
-
"color: default;",
|
|
374
|
-
123,
|
|
375
|
-
456,
|
|
376
|
-
],
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
ERROR: [
|
|
380
|
-
"%c22:13:20.000 %cERR%c %cmy-app·junk %cHello, %o & %o!",
|
|
381
|
-
"color: gray;",
|
|
382
|
-
"background-color: red; color: white;",
|
|
383
|
-
"background-color: default;",
|
|
384
|
-
"color: gray;",
|
|
385
|
-
"color: default;",
|
|
386
|
-
123,
|
|
387
|
-
456,
|
|
388
|
-
],
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
ERROR: [
|
|
392
|
-
"%c22:13:20.000 %cFTL%c %cmy-app·junk %cHello, %o & %o!",
|
|
393
|
-
"color: gray;",
|
|
394
|
-
"background-color: maroon; color: white;",
|
|
395
|
-
"background-color: default;",
|
|
396
|
-
"color: gray;",
|
|
397
|
-
"color: default;",
|
|
398
|
-
123,
|
|
399
|
-
456,
|
|
400
|
-
],
|
|
401
|
-
},
|
|
402
|
-
]);
|
|
403
|
-
|
|
404
|
-
assertThrows(
|
|
405
|
-
() => sink({ ...info, level: "invalid" as LogLevel }),
|
|
406
|
-
TypeError,
|
|
407
|
-
"Invalid log level: invalid.",
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
// @ts-ignore: consolemock is not typed
|
|
411
|
-
const mock2: ConsoleMock = makeConsoleMock();
|
|
412
|
-
const sink2 = getConsoleSink({
|
|
413
|
-
console: mock2,
|
|
414
|
-
formatter: defaultTextFormatter,
|
|
415
|
-
});
|
|
416
|
-
sink2(trace);
|
|
417
|
-
sink2(debug);
|
|
418
|
-
sink2(info);
|
|
419
|
-
sink2(warning);
|
|
420
|
-
sink2(error);
|
|
421
|
-
sink2(fatal);
|
|
422
|
-
assertEquals(mock2.history(), [
|
|
423
|
-
{
|
|
424
|
-
DEBUG: [
|
|
425
|
-
"2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!",
|
|
426
|
-
],
|
|
427
|
-
},
|
|
428
|
-
{
|
|
429
|
-
DEBUG: [
|
|
430
|
-
"2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!",
|
|
431
|
-
],
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
INFO: [
|
|
435
|
-
"2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!",
|
|
436
|
-
],
|
|
437
|
-
},
|
|
438
|
-
{
|
|
439
|
-
WARN: [
|
|
440
|
-
"2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!",
|
|
441
|
-
],
|
|
442
|
-
},
|
|
443
|
-
{
|
|
444
|
-
ERROR: [
|
|
445
|
-
"2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!",
|
|
446
|
-
],
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
ERROR: [
|
|
450
|
-
"2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!",
|
|
451
|
-
],
|
|
452
|
-
},
|
|
453
|
-
]);
|
|
454
|
-
|
|
455
|
-
// @ts-ignore: consolemock is not typed
|
|
456
|
-
const mock3: ConsoleMock = makeConsoleMock();
|
|
457
|
-
const sink3 = getConsoleSink({
|
|
458
|
-
console: mock3,
|
|
459
|
-
levelMap: {
|
|
460
|
-
trace: "log",
|
|
461
|
-
debug: "log",
|
|
462
|
-
info: "log",
|
|
463
|
-
warning: "log",
|
|
464
|
-
error: "log",
|
|
465
|
-
fatal: "log",
|
|
466
|
-
},
|
|
467
|
-
formatter: defaultTextFormatter,
|
|
468
|
-
});
|
|
469
|
-
sink3(trace);
|
|
470
|
-
sink3(debug);
|
|
471
|
-
sink3(info);
|
|
472
|
-
sink3(warning);
|
|
473
|
-
sink3(error);
|
|
474
|
-
sink3(fatal);
|
|
475
|
-
assertEquals(mock3.history(), [
|
|
476
|
-
{
|
|
477
|
-
LOG: [
|
|
478
|
-
"2023-11-14 22:13:20.000 +00:00 [TRC] my-app·junk: Hello, 123 & 456!",
|
|
479
|
-
],
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
LOG: [
|
|
483
|
-
"2023-11-14 22:13:20.000 +00:00 [DBG] my-app·junk: Hello, 123 & 456!",
|
|
484
|
-
],
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
LOG: [
|
|
488
|
-
"2023-11-14 22:13:20.000 +00:00 [INF] my-app·junk: Hello, 123 & 456!",
|
|
489
|
-
],
|
|
490
|
-
},
|
|
491
|
-
{
|
|
492
|
-
LOG: [
|
|
493
|
-
"2023-11-14 22:13:20.000 +00:00 [WRN] my-app·junk: Hello, 123 & 456!",
|
|
494
|
-
],
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
LOG: [
|
|
498
|
-
"2023-11-14 22:13:20.000 +00:00 [ERR] my-app·junk: Hello, 123 & 456!",
|
|
499
|
-
],
|
|
500
|
-
},
|
|
501
|
-
{
|
|
502
|
-
LOG: [
|
|
503
|
-
"2023-11-14 22:13:20.000 +00:00 [FTL] my-app·junk: Hello, 123 & 456!",
|
|
504
|
-
],
|
|
505
|
-
},
|
|
506
|
-
]);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
test("getConsoleSink() with nonBlocking - simple boolean", async () => {
|
|
510
|
-
// @ts-ignore: consolemock is not typed
|
|
511
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
512
|
-
const sink = getConsoleSink({ console: mock, nonBlocking: true });
|
|
513
|
-
|
|
514
|
-
// Check that it returns a Disposable
|
|
515
|
-
assertInstanceOf(sink, Function);
|
|
516
|
-
assert(Symbol.dispose in sink);
|
|
517
|
-
|
|
518
|
-
// Add records - they should not be logged immediately
|
|
519
|
-
sink(trace);
|
|
520
|
-
sink(debug);
|
|
521
|
-
assertEquals(mock.history().length, 0); // Not logged yet
|
|
522
|
-
|
|
523
|
-
// Wait for flush interval (default 100ms)
|
|
524
|
-
await delay(150);
|
|
525
|
-
assertEquals(mock.history().length, 2); // Now they should be logged
|
|
526
|
-
|
|
527
|
-
// Dispose the sink
|
|
528
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
test("getConsoleSink() with nonBlocking - custom buffer config", async () => {
|
|
532
|
-
// @ts-ignore: consolemock is not typed
|
|
533
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
534
|
-
const sink = getConsoleSink({
|
|
535
|
-
console: mock,
|
|
536
|
-
nonBlocking: {
|
|
537
|
-
bufferSize: 3,
|
|
538
|
-
flushInterval: 50,
|
|
539
|
-
},
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
// Add records up to buffer size
|
|
543
|
-
sink(trace);
|
|
544
|
-
sink(debug);
|
|
545
|
-
assertEquals(mock.history().length, 0); // Not flushed yet
|
|
546
|
-
|
|
547
|
-
sink(info); // This should trigger scheduled flush (buffer size = 3)
|
|
548
|
-
await delay(10); // Wait for scheduled flush to execute
|
|
549
|
-
assertEquals(mock.history().length, 3); // Flushed due to buffer size
|
|
550
|
-
|
|
551
|
-
// Add more records
|
|
552
|
-
sink(warning);
|
|
553
|
-
assertEquals(mock.history().length, 3); // Not flushed yet
|
|
554
|
-
|
|
555
|
-
// Wait for flush interval
|
|
556
|
-
await delay(60);
|
|
557
|
-
assertEquals(mock.history().length, 4); // Flushed due to interval
|
|
558
|
-
|
|
559
|
-
// Dispose and check remaining records are flushed
|
|
560
|
-
sink(error);
|
|
561
|
-
sink(fatal);
|
|
562
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
563
|
-
assertEquals(mock.history().length, 6); // All records flushed on dispose
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
test("getConsoleSink() with nonBlocking - no operations after dispose", () => {
|
|
567
|
-
// @ts-ignore: consolemock is not typed
|
|
568
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
569
|
-
const sink = getConsoleSink({ console: mock, nonBlocking: true });
|
|
570
|
-
|
|
571
|
-
// Dispose immediately
|
|
572
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
573
|
-
|
|
574
|
-
// Try to add records after dispose
|
|
575
|
-
sink(trace);
|
|
576
|
-
sink(debug);
|
|
577
|
-
|
|
578
|
-
// No records should be logged
|
|
579
|
-
assertEquals(mock.history().length, 0);
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
test("getConsoleSink() with nonBlocking - error handling", async () => {
|
|
583
|
-
const errorConsole = {
|
|
584
|
-
...console,
|
|
585
|
-
debug: () => {
|
|
586
|
-
throw new Error("Console error");
|
|
587
|
-
},
|
|
588
|
-
info: () => {
|
|
589
|
-
throw new Error("Console error");
|
|
590
|
-
},
|
|
591
|
-
warn: () => {
|
|
592
|
-
throw new Error("Console error");
|
|
593
|
-
},
|
|
594
|
-
error: () => {
|
|
595
|
-
throw new Error("Console error");
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
|
|
599
|
-
const sink = getConsoleSink({
|
|
600
|
-
console: errorConsole,
|
|
601
|
-
nonBlocking: true,
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
// Should not throw when adding records
|
|
605
|
-
sink(trace);
|
|
606
|
-
sink(info);
|
|
607
|
-
sink(error);
|
|
608
|
-
|
|
609
|
-
// Wait for flush - errors should be silently ignored
|
|
610
|
-
await delay(150);
|
|
611
|
-
|
|
612
|
-
// Dispose - should not throw
|
|
613
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
test("getConsoleSink() with nonBlocking - buffer overflow protection", async () => {
|
|
617
|
-
// @ts-ignore: consolemock is not typed
|
|
618
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
619
|
-
const sink = getConsoleSink({
|
|
620
|
-
console: mock,
|
|
621
|
-
nonBlocking: {
|
|
622
|
-
bufferSize: 5,
|
|
623
|
-
flushInterval: 1000, // Long interval to prevent automatic flushing
|
|
624
|
-
},
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
// Add more records than 2x buffer size (which should trigger overflow protection)
|
|
628
|
-
for (let i = 0; i < 12; i++) {
|
|
629
|
-
sink(trace);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Should have dropped oldest records, keeping buffer size manageable
|
|
633
|
-
// Wait a bit for any scheduled flushes
|
|
634
|
-
await delay(10);
|
|
635
|
-
|
|
636
|
-
// Force flush by disposing
|
|
637
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
638
|
-
|
|
639
|
-
// Should have logged records, but not more than maxBufferSize (10)
|
|
640
|
-
const historyLength = mock.history().length;
|
|
641
|
-
assert(historyLength <= 10, `Expected <= 10 records, got ${historyLength}`);
|
|
642
|
-
assert(historyLength > 0, "Expected some records to be logged");
|
|
643
|
-
});
|
|
644
|
-
|
|
645
|
-
test("getConsoleSink() with nonBlocking - high volume non-blocking behavior", async () => {
|
|
646
|
-
// @ts-ignore: consolemock is not typed
|
|
647
|
-
const mock: ConsoleMock = makeConsoleMock();
|
|
648
|
-
const sink = getConsoleSink({
|
|
649
|
-
console: mock,
|
|
650
|
-
nonBlocking: {
|
|
651
|
-
bufferSize: 3,
|
|
652
|
-
flushInterval: 50,
|
|
653
|
-
},
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
// Simulate rapid logging - this should not block
|
|
657
|
-
const startTime = performance.now();
|
|
658
|
-
for (let i = 0; i < 100; i++) {
|
|
659
|
-
sink(trace);
|
|
660
|
-
}
|
|
661
|
-
const endTime = performance.now();
|
|
662
|
-
|
|
663
|
-
// Adding logs should be very fast (non-blocking)
|
|
664
|
-
const duration = endTime - startTime;
|
|
665
|
-
assert(
|
|
666
|
-
duration < 100,
|
|
667
|
-
`Adding 100 logs took ${duration}ms, should be much faster`,
|
|
668
|
-
);
|
|
669
|
-
|
|
670
|
-
// Wait for flushes to complete
|
|
671
|
-
await delay(200);
|
|
672
|
-
|
|
673
|
-
// Should have logged some records
|
|
674
|
-
assert(mock.history().length > 0, "Expected some records to be logged");
|
|
675
|
-
|
|
676
|
-
(sink as Sink & Disposable)[Symbol.dispose]();
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
test("fromAsyncSink() - basic functionality", async () => {
|
|
680
|
-
const buffer: LogRecord[] = [];
|
|
681
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
682
|
-
await delay(10);
|
|
683
|
-
buffer.push(record);
|
|
684
|
-
};
|
|
685
|
-
|
|
686
|
-
const sink = fromAsyncSink(asyncSink);
|
|
687
|
-
|
|
688
|
-
sink(trace);
|
|
689
|
-
sink(debug);
|
|
690
|
-
sink(info);
|
|
691
|
-
|
|
692
|
-
// Records should not be in buffer immediately
|
|
693
|
-
assertEquals(buffer.length, 0);
|
|
694
|
-
|
|
695
|
-
// Wait for async operations to complete
|
|
696
|
-
await sink[Symbol.asyncDispose]();
|
|
697
|
-
|
|
698
|
-
// All records should be in buffer in order
|
|
699
|
-
assertEquals(buffer.length, 3);
|
|
700
|
-
assertEquals(buffer, [trace, debug, info]);
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
test("fromAsyncSink() - promise chaining preserves order", async () => {
|
|
704
|
-
const buffer: LogRecord[] = [];
|
|
705
|
-
const delays = [50, 10, 30]; // Different delays for each call
|
|
706
|
-
let callIndex = 0;
|
|
707
|
-
|
|
708
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
709
|
-
const delayTime = delays[callIndex % delays.length];
|
|
710
|
-
callIndex++;
|
|
711
|
-
await delay(delayTime);
|
|
712
|
-
buffer.push(record);
|
|
713
|
-
};
|
|
714
|
-
|
|
715
|
-
const sink = fromAsyncSink(asyncSink);
|
|
716
|
-
|
|
717
|
-
sink(trace);
|
|
718
|
-
sink(debug);
|
|
719
|
-
sink(info);
|
|
720
|
-
|
|
721
|
-
await sink[Symbol.asyncDispose]();
|
|
722
|
-
|
|
723
|
-
// Despite different delays, order should be preserved
|
|
724
|
-
assertEquals(buffer.length, 3);
|
|
725
|
-
assertEquals(buffer, [trace, debug, info]);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
test("fromAsyncSink() - error handling", async () => {
|
|
729
|
-
const buffer: LogRecord[] = [];
|
|
730
|
-
let errorCount = 0;
|
|
731
|
-
|
|
732
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
733
|
-
if (record.level === "error") {
|
|
734
|
-
errorCount++;
|
|
735
|
-
throw new Error("Async sink error");
|
|
736
|
-
}
|
|
737
|
-
await Promise.resolve(); // Ensure it's async
|
|
738
|
-
buffer.push(record);
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
const sink = fromAsyncSink(asyncSink);
|
|
742
|
-
|
|
743
|
-
sink(trace);
|
|
744
|
-
sink(error); // This will throw in async sink
|
|
745
|
-
sink(info);
|
|
746
|
-
|
|
747
|
-
await sink[Symbol.asyncDispose]();
|
|
748
|
-
|
|
749
|
-
// Error should be caught and not break the chain
|
|
750
|
-
assertEquals(errorCount, 1);
|
|
751
|
-
assertEquals(buffer.length, 2);
|
|
752
|
-
assertEquals(buffer, [trace, info]);
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
test("fromAsyncSink() - multiple dispose calls", async () => {
|
|
756
|
-
const buffer: LogRecord[] = [];
|
|
757
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
758
|
-
await delay(10);
|
|
759
|
-
buffer.push(record);
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
const sink = fromAsyncSink(asyncSink);
|
|
763
|
-
|
|
764
|
-
sink(trace);
|
|
765
|
-
sink(debug);
|
|
766
|
-
|
|
767
|
-
// First dispose
|
|
768
|
-
await sink[Symbol.asyncDispose]();
|
|
769
|
-
assertEquals(buffer.length, 2);
|
|
770
|
-
|
|
771
|
-
// Second dispose should be safe
|
|
772
|
-
await sink[Symbol.asyncDispose]();
|
|
773
|
-
assertEquals(buffer.length, 2);
|
|
774
|
-
|
|
775
|
-
// Third dispose should be safe
|
|
776
|
-
await sink[Symbol.asyncDispose]();
|
|
777
|
-
assertEquals(buffer.length, 2);
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
test("fromAsyncSink() - concurrent calls", async () => {
|
|
781
|
-
const buffer: LogRecord[] = [];
|
|
782
|
-
let concurrentCalls = 0;
|
|
783
|
-
let maxConcurrentCalls = 0;
|
|
784
|
-
|
|
785
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
786
|
-
concurrentCalls++;
|
|
787
|
-
maxConcurrentCalls = Math.max(maxConcurrentCalls, concurrentCalls);
|
|
788
|
-
await delay(20);
|
|
789
|
-
buffer.push(record);
|
|
790
|
-
concurrentCalls--;
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
const sink = fromAsyncSink(asyncSink);
|
|
794
|
-
|
|
795
|
-
// Fire multiple calls rapidly
|
|
796
|
-
for (let i = 0; i < 5; i++) {
|
|
797
|
-
sink(trace);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
await sink[Symbol.asyncDispose]();
|
|
801
|
-
|
|
802
|
-
// Due to promise chaining, max concurrent calls should be 1
|
|
803
|
-
assertEquals(maxConcurrentCalls, 1);
|
|
804
|
-
assertEquals(buffer.length, 5);
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
test("fromAsyncSink() - works with synchronous exceptions", async () => {
|
|
808
|
-
const buffer: LogRecord[] = [];
|
|
809
|
-
let errorCount = 0;
|
|
810
|
-
|
|
811
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
812
|
-
if (record.level === "fatal") {
|
|
813
|
-
errorCount++;
|
|
814
|
-
// Synchronous throw before any await
|
|
815
|
-
throw new Error("Sync error in async sink");
|
|
816
|
-
}
|
|
817
|
-
await delay(10);
|
|
818
|
-
buffer.push(record);
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
const sink = fromAsyncSink(asyncSink);
|
|
822
|
-
|
|
823
|
-
sink(trace);
|
|
824
|
-
sink(fatal); // This will throw synchronously in async sink
|
|
825
|
-
sink(info);
|
|
826
|
-
|
|
827
|
-
await sink[Symbol.asyncDispose]();
|
|
828
|
-
|
|
829
|
-
// Error should still be caught
|
|
830
|
-
assertEquals(errorCount, 1);
|
|
831
|
-
assertEquals(buffer.length, 2);
|
|
832
|
-
assertEquals(buffer, [trace, info]);
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
test("fromAsyncSink() - very long async operations", async () => {
|
|
836
|
-
const buffer: LogRecord[] = [];
|
|
837
|
-
const asyncSink: AsyncSink = async (record) => {
|
|
838
|
-
await delay(100); // Longer delay
|
|
839
|
-
buffer.push(record);
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
const sink = fromAsyncSink(asyncSink);
|
|
843
|
-
|
|
844
|
-
sink(trace);
|
|
845
|
-
sink(debug);
|
|
846
|
-
|
|
847
|
-
// Don't wait, just dispose immediately
|
|
848
|
-
const disposePromise = sink[Symbol.asyncDispose]();
|
|
849
|
-
|
|
850
|
-
// Buffer should still be empty
|
|
851
|
-
assertEquals(buffer.length, 0);
|
|
852
|
-
|
|
853
|
-
// Wait for dispose to complete
|
|
854
|
-
await disposePromise;
|
|
855
|
-
|
|
856
|
-
// Now all records should be processed
|
|
857
|
-
assertEquals(buffer.length, 2);
|
|
858
|
-
assertEquals(buffer, [trace, debug]);
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
test("fromAsyncSink() - empty async sink", async () => {
|
|
862
|
-
const asyncSink: AsyncSink = async () => {
|
|
863
|
-
// Do nothing
|
|
864
|
-
};
|
|
865
|
-
|
|
866
|
-
const sink = fromAsyncSink(asyncSink);
|
|
867
|
-
|
|
868
|
-
// Should not throw
|
|
869
|
-
sink(trace);
|
|
870
|
-
sink(debug);
|
|
871
|
-
|
|
872
|
-
await sink[Symbol.asyncDispose]();
|
|
873
|
-
|
|
874
|
-
// Test passes if no errors thrown
|
|
875
|
-
assert(true);
|
|
876
|
-
});
|
|
877
|
-
|
|
878
|
-
test("fingersCrossed() - basic functionality", () => {
|
|
879
|
-
const buffer: LogRecord[] = [];
|
|
880
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
881
|
-
|
|
882
|
-
// Add debug and info logs - should be buffered
|
|
883
|
-
sink(trace);
|
|
884
|
-
sink(debug);
|
|
885
|
-
sink(info);
|
|
886
|
-
assertEquals(buffer.length, 0); // Not flushed yet
|
|
887
|
-
|
|
888
|
-
// Add warning - still buffered (default trigger is error)
|
|
889
|
-
sink(warning);
|
|
890
|
-
assertEquals(buffer.length, 0);
|
|
891
|
-
|
|
892
|
-
// Add error - should trigger flush
|
|
893
|
-
sink(error);
|
|
894
|
-
assertEquals(buffer, [trace, debug, info, warning, error]);
|
|
895
|
-
|
|
896
|
-
// After trigger, logs pass through directly
|
|
897
|
-
sink(fatal);
|
|
898
|
-
assertEquals(buffer, [trace, debug, info, warning, error, fatal]);
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
test("fingersCrossed() - custom trigger level", () => {
|
|
902
|
-
const buffer: LogRecord[] = [];
|
|
903
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
904
|
-
triggerLevel: "warning",
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
// Add logs below warning
|
|
908
|
-
sink(trace);
|
|
909
|
-
sink(debug);
|
|
910
|
-
sink(info);
|
|
911
|
-
assertEquals(buffer.length, 0);
|
|
912
|
-
|
|
913
|
-
// Warning should trigger flush
|
|
914
|
-
sink(warning);
|
|
915
|
-
assertEquals(buffer, [trace, debug, info, warning]);
|
|
916
|
-
|
|
917
|
-
// Subsequent logs pass through
|
|
918
|
-
sink(error);
|
|
919
|
-
assertEquals(buffer, [trace, debug, info, warning, error]);
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
test("fingersCrossed() - buffer overflow protection", () => {
|
|
923
|
-
const buffer: LogRecord[] = [];
|
|
924
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
925
|
-
maxBufferSize: 3,
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
// Add more logs than buffer size
|
|
929
|
-
sink(trace);
|
|
930
|
-
sink(debug);
|
|
931
|
-
sink(info);
|
|
932
|
-
sink(warning); // Should drop trace
|
|
933
|
-
assertEquals(buffer.length, 0); // Still buffered
|
|
934
|
-
|
|
935
|
-
// Trigger flush
|
|
936
|
-
sink(error);
|
|
937
|
-
// Should only have last 3 records + error
|
|
938
|
-
assertEquals(buffer, [debug, info, warning, error]);
|
|
939
|
-
});
|
|
940
|
-
|
|
941
|
-
test("fingersCrossed() - multiple trigger events", () => {
|
|
942
|
-
const buffer: LogRecord[] = [];
|
|
943
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
944
|
-
|
|
945
|
-
// First batch
|
|
946
|
-
sink(debug);
|
|
947
|
-
sink(info);
|
|
948
|
-
sink(error); // Trigger
|
|
949
|
-
assertEquals(buffer, [debug, info, error]);
|
|
950
|
-
|
|
951
|
-
// After trigger, everything passes through
|
|
952
|
-
sink(debug);
|
|
953
|
-
assertEquals(buffer, [debug, info, error, debug]);
|
|
954
|
-
|
|
955
|
-
sink(error); // Another error
|
|
956
|
-
assertEquals(buffer, [debug, info, error, debug, error]);
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
test("fingersCrossed() - trigger includes fatal", () => {
|
|
960
|
-
const buffer: LogRecord[] = [];
|
|
961
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
962
|
-
triggerLevel: "error",
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
sink(debug);
|
|
966
|
-
sink(info);
|
|
967
|
-
assertEquals(buffer.length, 0);
|
|
968
|
-
|
|
969
|
-
// Fatal should also trigger (since it's >= error)
|
|
970
|
-
sink(fatal);
|
|
971
|
-
assertEquals(buffer, [debug, info, fatal]);
|
|
972
|
-
});
|
|
973
|
-
|
|
974
|
-
test("fingersCrossed() - category isolation descendant mode", () => {
|
|
975
|
-
const buffer: LogRecord[] = [];
|
|
976
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
977
|
-
isolateByCategory: "descendant",
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
// Create test records with different categories
|
|
981
|
-
const appDebug: LogRecord = {
|
|
982
|
-
...debug,
|
|
983
|
-
category: ["app"],
|
|
984
|
-
};
|
|
985
|
-
const appModuleDebug: LogRecord = {
|
|
986
|
-
...debug,
|
|
987
|
-
category: ["app", "module"],
|
|
988
|
-
};
|
|
989
|
-
const appModuleSubDebug: LogRecord = {
|
|
990
|
-
...debug,
|
|
991
|
-
category: ["app", "module", "sub"],
|
|
992
|
-
};
|
|
993
|
-
const otherDebug: LogRecord = {
|
|
994
|
-
...debug,
|
|
995
|
-
category: ["other"],
|
|
996
|
-
};
|
|
997
|
-
const appError: LogRecord = {
|
|
998
|
-
...error,
|
|
999
|
-
category: ["app"],
|
|
1000
|
-
};
|
|
1001
|
-
|
|
1002
|
-
// Buffer logs in different categories
|
|
1003
|
-
sink(appDebug);
|
|
1004
|
-
sink(appModuleDebug);
|
|
1005
|
-
sink(appModuleSubDebug);
|
|
1006
|
-
sink(otherDebug);
|
|
1007
|
-
assertEquals(buffer.length, 0);
|
|
1008
|
-
|
|
1009
|
-
// Trigger in parent category
|
|
1010
|
-
sink(appError);
|
|
1011
|
-
|
|
1012
|
-
// Should flush parent and all descendants, but not other
|
|
1013
|
-
assertEquals(buffer.length, 4); // app, app.module, app.module.sub, and trigger
|
|
1014
|
-
assert(buffer.includes(appDebug));
|
|
1015
|
-
assert(buffer.includes(appModuleDebug));
|
|
1016
|
-
assert(buffer.includes(appModuleSubDebug));
|
|
1017
|
-
assert(buffer.includes(appError));
|
|
1018
|
-
assert(!buffer.includes(otherDebug));
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
test("fingersCrossed() - category isolation ancestor mode", () => {
|
|
1022
|
-
const buffer: LogRecord[] = [];
|
|
1023
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1024
|
-
isolateByCategory: "ancestor",
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
// Create test records
|
|
1028
|
-
const appDebug: LogRecord = {
|
|
1029
|
-
...debug,
|
|
1030
|
-
category: ["app"],
|
|
1031
|
-
};
|
|
1032
|
-
const appModuleDebug: LogRecord = {
|
|
1033
|
-
...debug,
|
|
1034
|
-
category: ["app", "module"],
|
|
1035
|
-
};
|
|
1036
|
-
const appModuleSubDebug: LogRecord = {
|
|
1037
|
-
...debug,
|
|
1038
|
-
category: ["app", "module", "sub"],
|
|
1039
|
-
};
|
|
1040
|
-
const appModuleSubError: LogRecord = {
|
|
1041
|
-
...error,
|
|
1042
|
-
category: ["app", "module", "sub"],
|
|
1043
|
-
};
|
|
1044
|
-
|
|
1045
|
-
// Buffer logs
|
|
1046
|
-
sink(appDebug);
|
|
1047
|
-
sink(appModuleDebug);
|
|
1048
|
-
sink(appModuleSubDebug);
|
|
1049
|
-
assertEquals(buffer.length, 0);
|
|
1050
|
-
|
|
1051
|
-
// Trigger in child category
|
|
1052
|
-
sink(appModuleSubError);
|
|
1053
|
-
|
|
1054
|
-
// Should flush child and all ancestors
|
|
1055
|
-
assertEquals(buffer.length, 4);
|
|
1056
|
-
assert(buffer.includes(appDebug));
|
|
1057
|
-
assert(buffer.includes(appModuleDebug));
|
|
1058
|
-
assert(buffer.includes(appModuleSubDebug));
|
|
1059
|
-
assert(buffer.includes(appModuleSubError));
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
test("fingersCrossed() - category isolation both mode", () => {
|
|
1063
|
-
const buffer: LogRecord[] = [];
|
|
1064
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1065
|
-
isolateByCategory: "both",
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
// Create test records
|
|
1069
|
-
const rootDebug: LogRecord = {
|
|
1070
|
-
...debug,
|
|
1071
|
-
category: ["app"],
|
|
1072
|
-
};
|
|
1073
|
-
const parentDebug: LogRecord = {
|
|
1074
|
-
...debug,
|
|
1075
|
-
category: ["app", "parent"],
|
|
1076
|
-
};
|
|
1077
|
-
const siblingDebug: LogRecord = {
|
|
1078
|
-
...debug,
|
|
1079
|
-
category: ["app", "sibling"],
|
|
1080
|
-
};
|
|
1081
|
-
const childDebug: LogRecord = {
|
|
1082
|
-
...debug,
|
|
1083
|
-
category: ["app", "parent", "child"],
|
|
1084
|
-
};
|
|
1085
|
-
const unrelatedDebug: LogRecord = {
|
|
1086
|
-
...debug,
|
|
1087
|
-
category: ["other"],
|
|
1088
|
-
};
|
|
1089
|
-
const parentError: LogRecord = {
|
|
1090
|
-
...error,
|
|
1091
|
-
category: ["app", "parent"],
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1094
|
-
// Buffer logs
|
|
1095
|
-
sink(rootDebug);
|
|
1096
|
-
sink(parentDebug);
|
|
1097
|
-
sink(siblingDebug);
|
|
1098
|
-
sink(childDebug);
|
|
1099
|
-
sink(unrelatedDebug);
|
|
1100
|
-
assertEquals(buffer.length, 0);
|
|
1101
|
-
|
|
1102
|
-
// Trigger in middle category
|
|
1103
|
-
sink(parentError);
|
|
1104
|
-
|
|
1105
|
-
// Should flush ancestors and descendants, but not siblings or unrelated
|
|
1106
|
-
assertEquals(buffer.length, 4);
|
|
1107
|
-
assert(buffer.includes(rootDebug)); // Ancestor
|
|
1108
|
-
assert(buffer.includes(parentDebug)); // Same
|
|
1109
|
-
assert(buffer.includes(childDebug)); // Descendant
|
|
1110
|
-
assert(buffer.includes(parentError)); // Trigger
|
|
1111
|
-
assert(!buffer.includes(siblingDebug)); // Sibling
|
|
1112
|
-
assert(!buffer.includes(unrelatedDebug)); // Unrelated
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
test("fingersCrossed() - custom category matcher", () => {
|
|
1116
|
-
const buffer: LogRecord[] = [];
|
|
1117
|
-
|
|
1118
|
-
// Custom matcher: only flush if categories share first element
|
|
1119
|
-
const customMatcher = (
|
|
1120
|
-
trigger: readonly string[],
|
|
1121
|
-
buffered: readonly string[],
|
|
1122
|
-
): boolean => {
|
|
1123
|
-
return trigger[0] === buffered[0];
|
|
1124
|
-
};
|
|
1125
|
-
|
|
1126
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1127
|
-
isolateByCategory: customMatcher,
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
// Create test records
|
|
1131
|
-
const app1Debug: LogRecord = {
|
|
1132
|
-
...debug,
|
|
1133
|
-
category: ["app", "module1"],
|
|
1134
|
-
};
|
|
1135
|
-
const app2Debug: LogRecord = {
|
|
1136
|
-
...debug,
|
|
1137
|
-
category: ["app", "module2"],
|
|
1138
|
-
};
|
|
1139
|
-
const otherDebug: LogRecord = {
|
|
1140
|
-
...debug,
|
|
1141
|
-
category: ["other", "module"],
|
|
1142
|
-
};
|
|
1143
|
-
const appError: LogRecord = {
|
|
1144
|
-
...error,
|
|
1145
|
-
category: ["app", "module3"],
|
|
1146
|
-
};
|
|
1147
|
-
|
|
1148
|
-
// Buffer logs
|
|
1149
|
-
sink(app1Debug);
|
|
1150
|
-
sink(app2Debug);
|
|
1151
|
-
sink(otherDebug);
|
|
1152
|
-
assertEquals(buffer.length, 0);
|
|
1153
|
-
|
|
1154
|
-
// Trigger
|
|
1155
|
-
sink(appError);
|
|
1156
|
-
|
|
1157
|
-
// Should flush all with same first category element
|
|
1158
|
-
assertEquals(buffer.length, 3);
|
|
1159
|
-
assert(buffer.includes(app1Debug));
|
|
1160
|
-
assert(buffer.includes(app2Debug));
|
|
1161
|
-
assert(buffer.includes(appError));
|
|
1162
|
-
assert(!buffer.includes(otherDebug));
|
|
1163
|
-
});
|
|
1164
|
-
|
|
1165
|
-
test("fingersCrossed() - isolated buffers maintain separate states", () => {
|
|
1166
|
-
const buffer: LogRecord[] = [];
|
|
1167
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1168
|
-
isolateByCategory: "descendant",
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
// Create test records
|
|
1172
|
-
const app1Debug: LogRecord = {
|
|
1173
|
-
...debug,
|
|
1174
|
-
category: ["app1"],
|
|
1175
|
-
};
|
|
1176
|
-
const app1Error: LogRecord = {
|
|
1177
|
-
...error,
|
|
1178
|
-
category: ["app1"],
|
|
1179
|
-
};
|
|
1180
|
-
const app2Debug: LogRecord = {
|
|
1181
|
-
...debug,
|
|
1182
|
-
category: ["app2"],
|
|
1183
|
-
};
|
|
1184
|
-
const app2Info: LogRecord = {
|
|
1185
|
-
...info,
|
|
1186
|
-
category: ["app2"],
|
|
1187
|
-
};
|
|
1188
|
-
|
|
1189
|
-
// Buffer in app1
|
|
1190
|
-
sink(app1Debug);
|
|
1191
|
-
|
|
1192
|
-
// Trigger app1
|
|
1193
|
-
sink(app1Error);
|
|
1194
|
-
assertEquals(buffer, [app1Debug, app1Error]);
|
|
1195
|
-
|
|
1196
|
-
// Buffer in app2 (should still be buffering)
|
|
1197
|
-
sink(app2Debug);
|
|
1198
|
-
assertEquals(buffer, [app1Debug, app1Error]); // app2 still buffered
|
|
1199
|
-
|
|
1200
|
-
// Add more to triggered app1 (should pass through)
|
|
1201
|
-
sink(app1Debug);
|
|
1202
|
-
assertEquals(buffer, [app1Debug, app1Error, app1Debug]);
|
|
1203
|
-
|
|
1204
|
-
// app2 still buffering
|
|
1205
|
-
sink(app2Info);
|
|
1206
|
-
assertEquals(buffer, [app1Debug, app1Error, app1Debug]); // app2 still buffered
|
|
1207
|
-
});
|
|
1208
|
-
|
|
1209
|
-
test("fingersCrossed() - chronological order in category isolation", () => {
|
|
1210
|
-
const buffer: LogRecord[] = [];
|
|
1211
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1212
|
-
isolateByCategory: "both",
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
// Create test records with different timestamps
|
|
1216
|
-
const app1: LogRecord = {
|
|
1217
|
-
...debug,
|
|
1218
|
-
category: ["app"],
|
|
1219
|
-
timestamp: 1000,
|
|
1220
|
-
};
|
|
1221
|
-
const app2: LogRecord = {
|
|
1222
|
-
...debug,
|
|
1223
|
-
category: ["app", "sub"],
|
|
1224
|
-
timestamp: 2000,
|
|
1225
|
-
};
|
|
1226
|
-
const app3: LogRecord = {
|
|
1227
|
-
...info,
|
|
1228
|
-
category: ["app"],
|
|
1229
|
-
timestamp: 3000,
|
|
1230
|
-
};
|
|
1231
|
-
const app4: LogRecord = {
|
|
1232
|
-
...debug,
|
|
1233
|
-
category: ["app", "sub"],
|
|
1234
|
-
timestamp: 4000,
|
|
1235
|
-
};
|
|
1236
|
-
const appError: LogRecord = {
|
|
1237
|
-
...error,
|
|
1238
|
-
category: ["app"],
|
|
1239
|
-
timestamp: 5000,
|
|
1240
|
-
};
|
|
1241
|
-
|
|
1242
|
-
// Add out of order
|
|
1243
|
-
sink(app3);
|
|
1244
|
-
sink(app1);
|
|
1245
|
-
sink(app4);
|
|
1246
|
-
sink(app2);
|
|
1247
|
-
|
|
1248
|
-
// Trigger
|
|
1249
|
-
sink(appError);
|
|
1250
|
-
|
|
1251
|
-
// Should be sorted by timestamp
|
|
1252
|
-
assertEquals(buffer, [app1, app2, app3, app4, appError]);
|
|
1253
|
-
});
|
|
1254
|
-
|
|
1255
|
-
test("fingersCrossed() - empty buffer trigger", () => {
|
|
1256
|
-
const buffer: LogRecord[] = [];
|
|
1257
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
1258
|
-
|
|
1259
|
-
// Trigger immediately without any buffered logs
|
|
1260
|
-
sink(error);
|
|
1261
|
-
assertEquals(buffer, [error]);
|
|
1262
|
-
|
|
1263
|
-
// Continue to pass through
|
|
1264
|
-
sink(debug);
|
|
1265
|
-
assertEquals(buffer, [error, debug]);
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
test("fingersCrossed() - buffer size per category in isolation mode", () => {
|
|
1269
|
-
const buffer: LogRecord[] = [];
|
|
1270
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1271
|
-
maxBufferSize: 2,
|
|
1272
|
-
isolateByCategory: "descendant",
|
|
1273
|
-
});
|
|
1274
|
-
|
|
1275
|
-
// Create records for different categories
|
|
1276
|
-
const app1Trace: LogRecord = { ...trace, category: ["app1"] };
|
|
1277
|
-
const app1Debug: LogRecord = { ...debug, category: ["app1"] };
|
|
1278
|
-
const app1Info: LogRecord = { ...info, category: ["app1"] };
|
|
1279
|
-
const app2Trace: LogRecord = { ...trace, category: ["app2"] };
|
|
1280
|
-
const app2Debug: LogRecord = { ...debug, category: ["app2"] };
|
|
1281
|
-
const app1Error: LogRecord = { ...error, category: ["app1"] };
|
|
1282
|
-
|
|
1283
|
-
// Fill app1 buffer beyond max
|
|
1284
|
-
sink(app1Trace);
|
|
1285
|
-
sink(app1Debug);
|
|
1286
|
-
sink(app1Info); // Should drop app1Trace
|
|
1287
|
-
|
|
1288
|
-
// Fill app2 buffer
|
|
1289
|
-
sink(app2Trace);
|
|
1290
|
-
sink(app2Debug);
|
|
1291
|
-
|
|
1292
|
-
// Trigger app1
|
|
1293
|
-
sink(app1Error);
|
|
1294
|
-
|
|
1295
|
-
// Should only have last 2 from app1 + error
|
|
1296
|
-
assertEquals(buffer.length, 3);
|
|
1297
|
-
assert(!buffer.some((r) => r === app1Trace)); // Dropped
|
|
1298
|
-
assert(buffer.includes(app1Debug));
|
|
1299
|
-
assert(buffer.includes(app1Info));
|
|
1300
|
-
assert(buffer.includes(app1Error));
|
|
1301
|
-
// app2 records should not be flushed
|
|
1302
|
-
assert(!buffer.includes(app2Trace));
|
|
1303
|
-
assert(!buffer.includes(app2Debug));
|
|
1304
|
-
});
|
|
1305
|
-
|
|
1306
|
-
test("fingersCrossed() - edge case: trigger level not in severity order", () => {
|
|
1307
|
-
const buffer: LogRecord[] = [];
|
|
1308
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1309
|
-
triggerLevel: "trace", // Lowest level triggers immediately
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
|
-
// Everything should pass through immediately
|
|
1313
|
-
sink(trace);
|
|
1314
|
-
assertEquals(buffer, [trace]);
|
|
1315
|
-
|
|
1316
|
-
sink(debug);
|
|
1317
|
-
assertEquals(buffer, [trace, debug]);
|
|
1318
|
-
});
|
|
1319
|
-
|
|
1320
|
-
test("fingersCrossed() - edge case: invalid trigger level", () => {
|
|
1321
|
-
const buffer: LogRecord[] = [];
|
|
1322
|
-
|
|
1323
|
-
// Should throw TypeError during sink creation
|
|
1324
|
-
assertThrows(
|
|
1325
|
-
() => {
|
|
1326
|
-
fingersCrossed(buffer.push.bind(buffer), {
|
|
1327
|
-
triggerLevel: "invalid" as LogLevel,
|
|
1328
|
-
});
|
|
1329
|
-
},
|
|
1330
|
-
TypeError,
|
|
1331
|
-
"Invalid triggerLevel",
|
|
1332
|
-
);
|
|
1333
|
-
});
|
|
1334
|
-
|
|
1335
|
-
test("fingersCrossed() - edge case: very large buffer size", () => {
|
|
1336
|
-
const buffer: LogRecord[] = [];
|
|
1337
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1338
|
-
maxBufferSize: Number.MAX_SAFE_INTEGER,
|
|
1339
|
-
});
|
|
1340
|
-
|
|
1341
|
-
// Add many records
|
|
1342
|
-
for (let i = 0; i < 1000; i++) {
|
|
1343
|
-
sink(debug);
|
|
1344
|
-
}
|
|
1345
|
-
assertEquals(buffer.length, 0); // Still buffered
|
|
1346
|
-
|
|
1347
|
-
sink(error);
|
|
1348
|
-
assertEquals(buffer.length, 1001); // All 1000 + error
|
|
1349
|
-
});
|
|
1350
|
-
|
|
1351
|
-
test("fingersCrossed() - edge case: zero buffer size", () => {
|
|
1352
|
-
const buffer: LogRecord[] = [];
|
|
1353
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1354
|
-
maxBufferSize: 0,
|
|
1355
|
-
});
|
|
1356
|
-
|
|
1357
|
-
// Nothing should be buffered
|
|
1358
|
-
sink(debug);
|
|
1359
|
-
sink(info);
|
|
1360
|
-
assertEquals(buffer.length, 0);
|
|
1361
|
-
|
|
1362
|
-
// Trigger should still work
|
|
1363
|
-
sink(error);
|
|
1364
|
-
assertEquals(buffer, [error]); // Only the trigger
|
|
1365
|
-
});
|
|
1366
|
-
|
|
1367
|
-
test("fingersCrossed() - edge case: negative buffer size", () => {
|
|
1368
|
-
const buffer: LogRecord[] = [];
|
|
1369
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1370
|
-
maxBufferSize: -1,
|
|
1371
|
-
});
|
|
1372
|
-
|
|
1373
|
-
// Should behave like zero
|
|
1374
|
-
sink(debug);
|
|
1375
|
-
sink(info);
|
|
1376
|
-
assertEquals(buffer.length, 0);
|
|
1377
|
-
|
|
1378
|
-
sink(error);
|
|
1379
|
-
assertEquals(buffer, [error]);
|
|
1380
|
-
});
|
|
1381
|
-
|
|
1382
|
-
test("fingersCrossed() - edge case: same record logged multiple times", () => {
|
|
1383
|
-
const buffer: LogRecord[] = [];
|
|
1384
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
1385
|
-
|
|
1386
|
-
// Log same record multiple times
|
|
1387
|
-
sink(debug);
|
|
1388
|
-
sink(debug);
|
|
1389
|
-
sink(debug);
|
|
1390
|
-
assertEquals(buffer.length, 0);
|
|
1391
|
-
|
|
1392
|
-
sink(error);
|
|
1393
|
-
// All instances should be preserved
|
|
1394
|
-
assertEquals(buffer.length, 4);
|
|
1395
|
-
assertEquals(buffer, [debug, debug, debug, error]);
|
|
1396
|
-
});
|
|
1397
|
-
|
|
1398
|
-
test("fingersCrossed() - edge case: empty category array", () => {
|
|
1399
|
-
const buffer: LogRecord[] = [];
|
|
1400
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1401
|
-
isolateByCategory: "both",
|
|
1402
|
-
});
|
|
1403
|
-
|
|
1404
|
-
const emptyCategory: LogRecord = {
|
|
1405
|
-
...debug,
|
|
1406
|
-
category: [],
|
|
1407
|
-
};
|
|
1408
|
-
|
|
1409
|
-
const normalCategory: LogRecord = {
|
|
1410
|
-
...info,
|
|
1411
|
-
category: ["app"],
|
|
1412
|
-
};
|
|
1413
|
-
|
|
1414
|
-
const emptyError: LogRecord = {
|
|
1415
|
-
...error,
|
|
1416
|
-
category: [],
|
|
1417
|
-
};
|
|
1418
|
-
|
|
1419
|
-
sink(emptyCategory);
|
|
1420
|
-
sink(normalCategory);
|
|
1421
|
-
assertEquals(buffer.length, 0);
|
|
1422
|
-
|
|
1423
|
-
// Trigger with empty category
|
|
1424
|
-
sink(emptyError);
|
|
1425
|
-
|
|
1426
|
-
// Only empty category should flush (no ancestors/descendants)
|
|
1427
|
-
assertEquals(buffer.length, 2);
|
|
1428
|
-
assert(buffer.includes(emptyCategory));
|
|
1429
|
-
assert(buffer.includes(emptyError));
|
|
1430
|
-
assert(!buffer.includes(normalCategory));
|
|
1431
|
-
});
|
|
1432
|
-
|
|
1433
|
-
test("fingersCrossed() - edge case: category with special characters", () => {
|
|
1434
|
-
const buffer: LogRecord[] = [];
|
|
1435
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1436
|
-
isolateByCategory: "descendant",
|
|
1437
|
-
});
|
|
1438
|
-
|
|
1439
|
-
// Category with null character (our separator)
|
|
1440
|
-
const specialCategory: LogRecord = {
|
|
1441
|
-
...debug,
|
|
1442
|
-
category: ["app\0special", "sub"],
|
|
1443
|
-
};
|
|
1444
|
-
|
|
1445
|
-
const normalCategory: LogRecord = {
|
|
1446
|
-
...info,
|
|
1447
|
-
category: ["app"],
|
|
1448
|
-
};
|
|
1449
|
-
|
|
1450
|
-
const specialError: LogRecord = {
|
|
1451
|
-
...error,
|
|
1452
|
-
category: ["app\0special"],
|
|
1453
|
-
};
|
|
1454
|
-
|
|
1455
|
-
sink(specialCategory);
|
|
1456
|
-
sink(normalCategory);
|
|
1457
|
-
assertEquals(buffer.length, 0);
|
|
1458
|
-
|
|
1459
|
-
// Should still work correctly despite special characters
|
|
1460
|
-
sink(specialError);
|
|
1461
|
-
|
|
1462
|
-
assertEquals(buffer.length, 2);
|
|
1463
|
-
assert(buffer.includes(specialCategory));
|
|
1464
|
-
assert(buffer.includes(specialError));
|
|
1465
|
-
assert(!buffer.includes(normalCategory));
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
test("fingersCrossed() - edge case: rapid alternating triggers", () => {
|
|
1469
|
-
const buffer: LogRecord[] = [];
|
|
1470
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1471
|
-
isolateByCategory: "both",
|
|
1472
|
-
});
|
|
1473
|
-
|
|
1474
|
-
const app1Debug: LogRecord = { ...debug, category: ["app1"] };
|
|
1475
|
-
const app2Debug: LogRecord = { ...debug, category: ["app2"] };
|
|
1476
|
-
const app1Error: LogRecord = { ...error, category: ["app1"] };
|
|
1477
|
-
const app2Error: LogRecord = { ...error, category: ["app2"] };
|
|
1478
|
-
|
|
1479
|
-
// Rapidly alternate between categories and triggers
|
|
1480
|
-
sink(app1Debug);
|
|
1481
|
-
sink(app2Debug);
|
|
1482
|
-
sink(app1Error); // Trigger app1
|
|
1483
|
-
assertEquals(buffer.length, 2); // app1Debug + app1Error
|
|
1484
|
-
|
|
1485
|
-
sink(app2Error); // Trigger app2
|
|
1486
|
-
assertEquals(buffer.length, 4); // Previous + app2Debug + app2Error
|
|
1487
|
-
|
|
1488
|
-
// After both triggered, everything passes through
|
|
1489
|
-
sink(app1Debug);
|
|
1490
|
-
sink(app2Debug);
|
|
1491
|
-
assertEquals(buffer.length, 6);
|
|
1492
|
-
});
|
|
1493
|
-
|
|
1494
|
-
test("fingersCrossed() - edge case: custom matcher throws error", () => {
|
|
1495
|
-
const buffer: LogRecord[] = [];
|
|
1496
|
-
|
|
1497
|
-
const errorMatcher = (): boolean => {
|
|
1498
|
-
throw new Error("Matcher error");
|
|
1499
|
-
};
|
|
1500
|
-
|
|
1501
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1502
|
-
isolateByCategory: errorMatcher,
|
|
1503
|
-
});
|
|
1504
|
-
|
|
1505
|
-
const app1Debug: LogRecord = { ...debug, category: ["app1"] };
|
|
1506
|
-
const app2Debug: LogRecord = { ...debug, category: ["app2"] };
|
|
1507
|
-
const app1Error: LogRecord = { ...error, category: ["app1"] };
|
|
1508
|
-
|
|
1509
|
-
sink(app1Debug);
|
|
1510
|
-
sink(app2Debug);
|
|
1511
|
-
|
|
1512
|
-
// Should handle error gracefully and still trigger
|
|
1513
|
-
try {
|
|
1514
|
-
sink(app1Error);
|
|
1515
|
-
} catch {
|
|
1516
|
-
// Should not throw to caller
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
// At minimum, trigger record should be sent
|
|
1520
|
-
assert(buffer.includes(app1Error));
|
|
1521
|
-
});
|
|
1522
|
-
|
|
1523
|
-
test("fingersCrossed() - edge case: circular category references", () => {
|
|
1524
|
-
const buffer: LogRecord[] = [];
|
|
1525
|
-
|
|
1526
|
-
// Custom matcher that creates circular logic
|
|
1527
|
-
const circularMatcher = (
|
|
1528
|
-
_trigger: readonly string[],
|
|
1529
|
-
_buffered: readonly string[],
|
|
1530
|
-
): boolean => {
|
|
1531
|
-
// Always return true, creating a circular flush
|
|
1532
|
-
return true;
|
|
1533
|
-
};
|
|
1534
|
-
|
|
1535
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1536
|
-
isolateByCategory: circularMatcher,
|
|
1537
|
-
});
|
|
1538
|
-
|
|
1539
|
-
const app1: LogRecord = { ...debug, category: ["app1"] };
|
|
1540
|
-
const app2: LogRecord = { ...debug, category: ["app2"] };
|
|
1541
|
-
const app3: LogRecord = { ...debug, category: ["app3"] };
|
|
1542
|
-
const trigger: LogRecord = { ...error, category: ["trigger"] };
|
|
1543
|
-
|
|
1544
|
-
sink(app1);
|
|
1545
|
-
sink(app2);
|
|
1546
|
-
sink(app3);
|
|
1547
|
-
assertEquals(buffer.length, 0);
|
|
1548
|
-
|
|
1549
|
-
// Should flush all despite circular logic
|
|
1550
|
-
sink(trigger);
|
|
1551
|
-
assertEquals(buffer.length, 4);
|
|
1552
|
-
|
|
1553
|
-
// All buffers should be cleared after flush
|
|
1554
|
-
const newDebug: LogRecord = { ...debug, category: ["new"] };
|
|
1555
|
-
sink(newDebug);
|
|
1556
|
-
assertEquals(buffer.length, 4); // New category should be buffered
|
|
1557
|
-
});
|
|
1558
|
-
|
|
1559
|
-
test("fingersCrossed() - edge case: timestamps in wrong order", () => {
|
|
1560
|
-
const buffer: LogRecord[] = [];
|
|
1561
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1562
|
-
isolateByCategory: "both",
|
|
1563
|
-
});
|
|
1564
|
-
|
|
1565
|
-
const future: LogRecord = {
|
|
1566
|
-
...debug,
|
|
1567
|
-
category: ["app"],
|
|
1568
|
-
timestamp: Date.now() + 10000, // Future
|
|
1569
|
-
};
|
|
1570
|
-
|
|
1571
|
-
const past: LogRecord = {
|
|
1572
|
-
...info,
|
|
1573
|
-
category: ["app", "sub"],
|
|
1574
|
-
timestamp: Date.now() - 10000, // Past
|
|
1575
|
-
};
|
|
1576
|
-
|
|
1577
|
-
const present: LogRecord = {
|
|
1578
|
-
...warning,
|
|
1579
|
-
category: ["app"],
|
|
1580
|
-
timestamp: Date.now(),
|
|
1581
|
-
};
|
|
1582
|
-
|
|
1583
|
-
const trigger: LogRecord = {
|
|
1584
|
-
...error,
|
|
1585
|
-
category: ["app"],
|
|
1586
|
-
timestamp: Date.now() + 5000,
|
|
1587
|
-
};
|
|
1588
|
-
|
|
1589
|
-
// Add in random order
|
|
1590
|
-
sink(future);
|
|
1591
|
-
sink(past);
|
|
1592
|
-
sink(present);
|
|
1593
|
-
|
|
1594
|
-
// Trigger
|
|
1595
|
-
sink(trigger);
|
|
1596
|
-
|
|
1597
|
-
// Should be sorted by timestamp
|
|
1598
|
-
assertEquals(buffer[0], past);
|
|
1599
|
-
assertEquals(buffer[1], present);
|
|
1600
|
-
assertEquals(buffer[2], future);
|
|
1601
|
-
assertEquals(buffer[3], trigger);
|
|
1602
|
-
});
|
|
1603
|
-
|
|
1604
|
-
test("fingersCrossed() - edge case: NaN and Infinity in timestamps", () => {
|
|
1605
|
-
const buffer: LogRecord[] = [];
|
|
1606
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1607
|
-
isolateByCategory: "both",
|
|
1608
|
-
});
|
|
1609
|
-
|
|
1610
|
-
const nanTime: LogRecord = {
|
|
1611
|
-
...debug,
|
|
1612
|
-
category: ["app"],
|
|
1613
|
-
timestamp: NaN,
|
|
1614
|
-
};
|
|
1615
|
-
|
|
1616
|
-
const infinityTime: LogRecord = {
|
|
1617
|
-
...info,
|
|
1618
|
-
category: ["app"],
|
|
1619
|
-
timestamp: Infinity,
|
|
1620
|
-
};
|
|
1621
|
-
|
|
1622
|
-
const negInfinityTime: LogRecord = {
|
|
1623
|
-
...warning,
|
|
1624
|
-
category: ["app"],
|
|
1625
|
-
timestamp: -Infinity,
|
|
1626
|
-
};
|
|
1627
|
-
|
|
1628
|
-
const normalTime: LogRecord = {
|
|
1629
|
-
...error,
|
|
1630
|
-
category: ["app"],
|
|
1631
|
-
timestamp: 1000,
|
|
1632
|
-
};
|
|
1633
|
-
|
|
1634
|
-
sink(nanTime);
|
|
1635
|
-
sink(infinityTime);
|
|
1636
|
-
sink(negInfinityTime);
|
|
1637
|
-
|
|
1638
|
-
// Should handle special values without crashing
|
|
1639
|
-
sink(normalTime);
|
|
1640
|
-
|
|
1641
|
-
// Check all records are present (order might vary with NaN)
|
|
1642
|
-
assertEquals(buffer.length, 4);
|
|
1643
|
-
assert(buffer.includes(nanTime));
|
|
1644
|
-
assert(buffer.includes(infinityTime));
|
|
1645
|
-
assert(buffer.includes(negInfinityTime));
|
|
1646
|
-
assert(buffer.includes(normalTime));
|
|
1647
|
-
});
|
|
1648
|
-
|
|
1649
|
-
test("fingersCrossed() - edge case: undefined properties in record", () => {
|
|
1650
|
-
const buffer: LogRecord[] = [];
|
|
1651
|
-
const sink = fingersCrossed(buffer.push.bind(buffer));
|
|
1652
|
-
|
|
1653
|
-
const weirdRecord: LogRecord = {
|
|
1654
|
-
...debug,
|
|
1655
|
-
properties: {
|
|
1656
|
-
normal: "value",
|
|
1657
|
-
undef: undefined,
|
|
1658
|
-
nullish: null,
|
|
1659
|
-
nan: NaN,
|
|
1660
|
-
inf: Infinity,
|
|
1661
|
-
},
|
|
1662
|
-
};
|
|
1663
|
-
|
|
1664
|
-
sink(weirdRecord);
|
|
1665
|
-
sink(error);
|
|
1666
|
-
|
|
1667
|
-
// Should preserve all properties as-is
|
|
1668
|
-
assertEquals(buffer[0].properties, weirdRecord.properties);
|
|
1669
|
-
});
|
|
1670
|
-
|
|
1671
|
-
test("fingersCrossed() - edge case: very deep category hierarchy", () => {
|
|
1672
|
-
const buffer: LogRecord[] = [];
|
|
1673
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1674
|
-
isolateByCategory: "both",
|
|
1675
|
-
});
|
|
1676
|
-
|
|
1677
|
-
// Create very deep hierarchy
|
|
1678
|
-
const deepCategory = Array.from({ length: 100 }, (_, i) => `level${i}`);
|
|
1679
|
-
const parentCategory = deepCategory.slice(0, 50);
|
|
1680
|
-
|
|
1681
|
-
const deepRecord: LogRecord = {
|
|
1682
|
-
...debug,
|
|
1683
|
-
category: deepCategory,
|
|
1684
|
-
};
|
|
1685
|
-
|
|
1686
|
-
const parentRecord: LogRecord = {
|
|
1687
|
-
...info,
|
|
1688
|
-
category: parentCategory,
|
|
1689
|
-
};
|
|
1690
|
-
|
|
1691
|
-
const deepError: LogRecord = {
|
|
1692
|
-
...error,
|
|
1693
|
-
category: deepCategory,
|
|
1694
|
-
};
|
|
1695
|
-
|
|
1696
|
-
sink(deepRecord);
|
|
1697
|
-
sink(parentRecord);
|
|
1698
|
-
assertEquals(buffer.length, 0);
|
|
1699
|
-
|
|
1700
|
-
// Should handle deep hierarchies
|
|
1701
|
-
sink(deepError);
|
|
1702
|
-
|
|
1703
|
-
// Both should flush (ancestor relationship)
|
|
1704
|
-
assertEquals(buffer.length, 3);
|
|
1705
|
-
assert(buffer.includes(deepRecord));
|
|
1706
|
-
assert(buffer.includes(parentRecord));
|
|
1707
|
-
assert(buffer.includes(deepError));
|
|
1708
|
-
});
|
|
1709
|
-
|
|
1710
|
-
test("fingersCrossed() - context isolation basic functionality", () => {
|
|
1711
|
-
const buffer: LogRecord[] = [];
|
|
1712
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1713
|
-
isolateByContext: { keys: ["requestId"] },
|
|
1714
|
-
});
|
|
1715
|
-
|
|
1716
|
-
// Create records with different request IDs
|
|
1717
|
-
const req1Debug: LogRecord = {
|
|
1718
|
-
...debug,
|
|
1719
|
-
properties: { requestId: "req-1", data: "debug1" },
|
|
1720
|
-
};
|
|
1721
|
-
const req1Info: LogRecord = {
|
|
1722
|
-
...info,
|
|
1723
|
-
properties: { requestId: "req-1", data: "info1" },
|
|
1724
|
-
};
|
|
1725
|
-
const req1Error: LogRecord = {
|
|
1726
|
-
...error,
|
|
1727
|
-
properties: { requestId: "req-1", data: "error1" },
|
|
1728
|
-
};
|
|
1729
|
-
|
|
1730
|
-
const req2Debug: LogRecord = {
|
|
1731
|
-
...debug,
|
|
1732
|
-
properties: { requestId: "req-2", data: "debug2" },
|
|
1733
|
-
};
|
|
1734
|
-
const req2Info: LogRecord = {
|
|
1735
|
-
...info,
|
|
1736
|
-
properties: { requestId: "req-2", data: "info2" },
|
|
1737
|
-
};
|
|
1738
|
-
|
|
1739
|
-
// Buffer logs for both requests
|
|
1740
|
-
sink(req1Debug);
|
|
1741
|
-
sink(req1Info);
|
|
1742
|
-
sink(req2Debug);
|
|
1743
|
-
sink(req2Info);
|
|
1744
|
-
assertEquals(buffer.length, 0); // All buffered
|
|
1745
|
-
|
|
1746
|
-
// Error in req-1 should only flush req-1 logs
|
|
1747
|
-
sink(req1Error);
|
|
1748
|
-
assertEquals(buffer.length, 3);
|
|
1749
|
-
assertEquals(buffer[0], req1Debug);
|
|
1750
|
-
assertEquals(buffer[1], req1Info);
|
|
1751
|
-
assertEquals(buffer[2], req1Error);
|
|
1752
|
-
|
|
1753
|
-
// req-2 logs should still be buffered
|
|
1754
|
-
buffer.length = 0;
|
|
1755
|
-
sink(req2Debug); // Add another req-2 log
|
|
1756
|
-
assertEquals(buffer.length, 0); // Still buffered
|
|
1757
|
-
|
|
1758
|
-
// Now trigger req-2
|
|
1759
|
-
const req2Error: LogRecord = {
|
|
1760
|
-
...error,
|
|
1761
|
-
properties: { requestId: "req-2", data: "error2" },
|
|
1762
|
-
};
|
|
1763
|
-
sink(req2Error);
|
|
1764
|
-
assertEquals(buffer.length, 4); // 2x req2Debug + req2Info + req2Error
|
|
1765
|
-
assertEquals(buffer[0], req2Debug);
|
|
1766
|
-
assertEquals(buffer[1], req2Info);
|
|
1767
|
-
assertEquals(buffer[2], req2Debug); // Second instance
|
|
1768
|
-
assertEquals(buffer[3], req2Error);
|
|
1769
|
-
});
|
|
1770
|
-
|
|
1771
|
-
test("fingersCrossed() - context isolation with multiple keys", () => {
|
|
1772
|
-
const buffer: LogRecord[] = [];
|
|
1773
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1774
|
-
isolateByContext: { keys: ["requestId", "sessionId"] },
|
|
1775
|
-
});
|
|
1776
|
-
|
|
1777
|
-
// Create records with different combinations
|
|
1778
|
-
const record1: LogRecord = {
|
|
1779
|
-
...debug,
|
|
1780
|
-
properties: { requestId: "req-1", sessionId: "sess-1" },
|
|
1781
|
-
};
|
|
1782
|
-
const record2: LogRecord = {
|
|
1783
|
-
...debug,
|
|
1784
|
-
properties: { requestId: "req-1", sessionId: "sess-2" },
|
|
1785
|
-
};
|
|
1786
|
-
const record3: LogRecord = {
|
|
1787
|
-
...debug,
|
|
1788
|
-
properties: { requestId: "req-2", sessionId: "sess-1" },
|
|
1789
|
-
};
|
|
1790
|
-
|
|
1791
|
-
sink(record1);
|
|
1792
|
-
sink(record2);
|
|
1793
|
-
sink(record3);
|
|
1794
|
-
assertEquals(buffer.length, 0); // All buffered
|
|
1795
|
-
|
|
1796
|
-
// Error with req-1/sess-1 should only flush that combination
|
|
1797
|
-
const trigger1: LogRecord = {
|
|
1798
|
-
...error,
|
|
1799
|
-
properties: { requestId: "req-1", sessionId: "sess-1" },
|
|
1800
|
-
};
|
|
1801
|
-
sink(trigger1);
|
|
1802
|
-
assertEquals(buffer.length, 2);
|
|
1803
|
-
assertEquals(buffer[0], record1);
|
|
1804
|
-
assertEquals(buffer[1], trigger1);
|
|
1805
|
-
|
|
1806
|
-
// Other combinations still buffered
|
|
1807
|
-
buffer.length = 0;
|
|
1808
|
-
const trigger2: LogRecord = {
|
|
1809
|
-
...error,
|
|
1810
|
-
properties: { requestId: "req-1", sessionId: "sess-2" },
|
|
1811
|
-
};
|
|
1812
|
-
sink(trigger2);
|
|
1813
|
-
assertEquals(buffer.length, 2);
|
|
1814
|
-
assertEquals(buffer[0], record2);
|
|
1815
|
-
assertEquals(buffer[1], trigger2);
|
|
1816
|
-
});
|
|
1817
|
-
|
|
1818
|
-
test("fingersCrossed() - context isolation with missing keys", () => {
|
|
1819
|
-
const buffer: LogRecord[] = [];
|
|
1820
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1821
|
-
isolateByContext: { keys: ["requestId"] },
|
|
1822
|
-
});
|
|
1823
|
-
|
|
1824
|
-
// Records with and without requestId
|
|
1825
|
-
const withId: LogRecord = {
|
|
1826
|
-
...debug,
|
|
1827
|
-
properties: { requestId: "req-1", other: "data" },
|
|
1828
|
-
};
|
|
1829
|
-
const withoutId: LogRecord = {
|
|
1830
|
-
...debug,
|
|
1831
|
-
properties: { other: "data" },
|
|
1832
|
-
};
|
|
1833
|
-
const withUndefinedId: LogRecord = {
|
|
1834
|
-
...debug,
|
|
1835
|
-
properties: { requestId: undefined, other: "data" },
|
|
1836
|
-
};
|
|
1837
|
-
|
|
1838
|
-
sink(withId);
|
|
1839
|
-
sink(withoutId);
|
|
1840
|
-
sink(withUndefinedId);
|
|
1841
|
-
assertEquals(buffer.length, 0); // All buffered
|
|
1842
|
-
|
|
1843
|
-
// Error without requestId should flush records without or with undefined requestId
|
|
1844
|
-
const triggerNoId: LogRecord = {
|
|
1845
|
-
...error,
|
|
1846
|
-
properties: { other: "data" },
|
|
1847
|
-
};
|
|
1848
|
-
sink(triggerNoId);
|
|
1849
|
-
assertEquals(buffer.length, 3); // withoutId + withUndefinedId + triggerNoId
|
|
1850
|
-
assertEquals(buffer[0], withoutId);
|
|
1851
|
-
assertEquals(buffer[1], withUndefinedId);
|
|
1852
|
-
assertEquals(buffer[2], triggerNoId);
|
|
1853
|
-
|
|
1854
|
-
// Records with requestId still buffered
|
|
1855
|
-
buffer.length = 0;
|
|
1856
|
-
const triggerWithId: LogRecord = {
|
|
1857
|
-
...error,
|
|
1858
|
-
properties: { requestId: "req-1", other: "data" },
|
|
1859
|
-
};
|
|
1860
|
-
sink(triggerWithId);
|
|
1861
|
-
assertEquals(buffer.length, 2);
|
|
1862
|
-
assertEquals(buffer[0], withId);
|
|
1863
|
-
assertEquals(buffer[1], triggerWithId);
|
|
1864
|
-
});
|
|
1865
|
-
|
|
1866
|
-
test("fingersCrossed() - combined category and context isolation", () => {
|
|
1867
|
-
const buffer: LogRecord[] = [];
|
|
1868
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1869
|
-
isolateByCategory: "descendant",
|
|
1870
|
-
isolateByContext: { keys: ["requestId"] },
|
|
1871
|
-
});
|
|
1872
|
-
|
|
1873
|
-
// Create records with different categories and contexts
|
|
1874
|
-
const appReq1: LogRecord = {
|
|
1875
|
-
...debug,
|
|
1876
|
-
category: ["app"],
|
|
1877
|
-
properties: { requestId: "req-1" },
|
|
1878
|
-
};
|
|
1879
|
-
const appModuleReq1: LogRecord = {
|
|
1880
|
-
...debug,
|
|
1881
|
-
category: ["app", "module"],
|
|
1882
|
-
properties: { requestId: "req-1" },
|
|
1883
|
-
};
|
|
1884
|
-
const appReq2: LogRecord = {
|
|
1885
|
-
...debug,
|
|
1886
|
-
category: ["app"],
|
|
1887
|
-
properties: { requestId: "req-2" },
|
|
1888
|
-
};
|
|
1889
|
-
const appModuleReq2: LogRecord = {
|
|
1890
|
-
...debug,
|
|
1891
|
-
category: ["app", "module"],
|
|
1892
|
-
properties: { requestId: "req-2" },
|
|
1893
|
-
};
|
|
1894
|
-
const otherReq1: LogRecord = {
|
|
1895
|
-
...debug,
|
|
1896
|
-
category: ["other"],
|
|
1897
|
-
properties: { requestId: "req-1" },
|
|
1898
|
-
};
|
|
1899
|
-
|
|
1900
|
-
sink(appReq1);
|
|
1901
|
-
sink(appModuleReq1);
|
|
1902
|
-
sink(appReq2);
|
|
1903
|
-
sink(appModuleReq2);
|
|
1904
|
-
sink(otherReq1);
|
|
1905
|
-
assertEquals(buffer.length, 0); // All buffered
|
|
1906
|
-
|
|
1907
|
-
// Error in ["app"] with req-1 should flush descendants with same requestId
|
|
1908
|
-
const triggerAppReq1: LogRecord = {
|
|
1909
|
-
...error,
|
|
1910
|
-
category: ["app"],
|
|
1911
|
-
properties: { requestId: "req-1" },
|
|
1912
|
-
};
|
|
1913
|
-
sink(triggerAppReq1);
|
|
1914
|
-
assertEquals(buffer.length, 3);
|
|
1915
|
-
assertEquals(buffer[0], appReq1);
|
|
1916
|
-
assertEquals(buffer[1], appModuleReq1);
|
|
1917
|
-
assertEquals(buffer[2], triggerAppReq1);
|
|
1918
|
-
|
|
1919
|
-
// Other combinations still buffered
|
|
1920
|
-
buffer.length = 0;
|
|
1921
|
-
const triggerAppReq2: LogRecord = {
|
|
1922
|
-
...error,
|
|
1923
|
-
category: ["app"],
|
|
1924
|
-
properties: { requestId: "req-2" },
|
|
1925
|
-
};
|
|
1926
|
-
sink(triggerAppReq2);
|
|
1927
|
-
assertEquals(buffer.length, 3);
|
|
1928
|
-
assertEquals(buffer[0], appReq2);
|
|
1929
|
-
assertEquals(buffer[1], appModuleReq2);
|
|
1930
|
-
assertEquals(buffer[2], triggerAppReq2);
|
|
1931
|
-
});
|
|
1932
|
-
|
|
1933
|
-
test("fingersCrossed() - context isolation buffer size limits", () => {
|
|
1934
|
-
const buffer: LogRecord[] = [];
|
|
1935
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
1936
|
-
maxBufferSize: 2,
|
|
1937
|
-
isolateByContext: { keys: ["requestId"] },
|
|
1938
|
-
});
|
|
1939
|
-
|
|
1940
|
-
// Create records for different contexts
|
|
1941
|
-
const req1Trace: LogRecord = {
|
|
1942
|
-
...trace,
|
|
1943
|
-
properties: { requestId: "req-1" },
|
|
1944
|
-
};
|
|
1945
|
-
const req1Debug: LogRecord = {
|
|
1946
|
-
...debug,
|
|
1947
|
-
properties: { requestId: "req-1" },
|
|
1948
|
-
};
|
|
1949
|
-
const req1Info: LogRecord = {
|
|
1950
|
-
...info,
|
|
1951
|
-
properties: { requestId: "req-1" },
|
|
1952
|
-
};
|
|
1953
|
-
const req2Trace: LogRecord = {
|
|
1954
|
-
...trace,
|
|
1955
|
-
properties: { requestId: "req-2" },
|
|
1956
|
-
};
|
|
1957
|
-
const req2Debug: LogRecord = {
|
|
1958
|
-
...debug,
|
|
1959
|
-
properties: { requestId: "req-2" },
|
|
1960
|
-
};
|
|
1961
|
-
|
|
1962
|
-
// Fill req-1 buffer beyond limit
|
|
1963
|
-
sink(req1Trace);
|
|
1964
|
-
sink(req1Debug);
|
|
1965
|
-
sink(req1Info); // Should drop req1Trace
|
|
1966
|
-
|
|
1967
|
-
// Fill req-2 buffer
|
|
1968
|
-
sink(req2Trace);
|
|
1969
|
-
sink(req2Debug);
|
|
1970
|
-
|
|
1971
|
-
// Trigger req-1
|
|
1972
|
-
const req1Error: LogRecord = {
|
|
1973
|
-
...error,
|
|
1974
|
-
properties: { requestId: "req-1" },
|
|
1975
|
-
};
|
|
1976
|
-
sink(req1Error);
|
|
1977
|
-
|
|
1978
|
-
// Should only have the last 2 records plus error
|
|
1979
|
-
assertEquals(buffer.length, 3);
|
|
1980
|
-
assertEquals(buffer[0], req1Debug);
|
|
1981
|
-
assertEquals(buffer[1], req1Info);
|
|
1982
|
-
assertEquals(buffer[2], req1Error);
|
|
1983
|
-
|
|
1984
|
-
// Trigger req-2
|
|
1985
|
-
buffer.length = 0;
|
|
1986
|
-
const req2Error: LogRecord = {
|
|
1987
|
-
...error,
|
|
1988
|
-
properties: { requestId: "req-2" },
|
|
1989
|
-
};
|
|
1990
|
-
sink(req2Error);
|
|
1991
|
-
|
|
1992
|
-
// req-2 buffer should still have both records
|
|
1993
|
-
assertEquals(buffer.length, 3);
|
|
1994
|
-
assertEquals(buffer[0], req2Trace);
|
|
1995
|
-
assertEquals(buffer[1], req2Debug);
|
|
1996
|
-
assertEquals(buffer[2], req2Error);
|
|
1997
|
-
});
|
|
1998
|
-
|
|
1999
|
-
test("fingersCrossed() - context isolation with special values", () => {
|
|
2000
|
-
const buffer: LogRecord[] = [];
|
|
2001
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2002
|
-
isolateByContext: { keys: ["value"] },
|
|
2003
|
-
});
|
|
2004
|
-
|
|
2005
|
-
// Records with special values
|
|
2006
|
-
const nullValue: LogRecord = {
|
|
2007
|
-
...debug,
|
|
2008
|
-
properties: { value: null },
|
|
2009
|
-
};
|
|
2010
|
-
const undefinedValue: LogRecord = {
|
|
2011
|
-
...debug,
|
|
2012
|
-
properties: { value: undefined },
|
|
2013
|
-
};
|
|
2014
|
-
const zeroValue: LogRecord = {
|
|
2015
|
-
...debug,
|
|
2016
|
-
properties: { value: 0 },
|
|
2017
|
-
};
|
|
2018
|
-
const emptyString: LogRecord = {
|
|
2019
|
-
...debug,
|
|
2020
|
-
properties: { value: "" },
|
|
2021
|
-
};
|
|
2022
|
-
const falseValue: LogRecord = {
|
|
2023
|
-
...debug,
|
|
2024
|
-
properties: { value: false },
|
|
2025
|
-
};
|
|
2026
|
-
|
|
2027
|
-
sink(nullValue);
|
|
2028
|
-
sink(undefinedValue);
|
|
2029
|
-
sink(zeroValue);
|
|
2030
|
-
sink(emptyString);
|
|
2031
|
-
sink(falseValue);
|
|
2032
|
-
assertEquals(buffer.length, 0); // All buffered
|
|
2033
|
-
|
|
2034
|
-
// Trigger with null value
|
|
2035
|
-
const triggerNull: LogRecord = {
|
|
2036
|
-
...error,
|
|
2037
|
-
properties: { value: null },
|
|
2038
|
-
};
|
|
2039
|
-
sink(triggerNull);
|
|
2040
|
-
assertEquals(buffer.length, 2);
|
|
2041
|
-
assertEquals(buffer[0], nullValue);
|
|
2042
|
-
assertEquals(buffer[1], triggerNull);
|
|
2043
|
-
|
|
2044
|
-
// Trigger with zero value
|
|
2045
|
-
buffer.length = 0;
|
|
2046
|
-
const triggerZero: LogRecord = {
|
|
2047
|
-
...error,
|
|
2048
|
-
properties: { value: 0 },
|
|
2049
|
-
};
|
|
2050
|
-
sink(triggerZero);
|
|
2051
|
-
assertEquals(buffer.length, 2);
|
|
2052
|
-
assertEquals(buffer[0], zeroValue);
|
|
2053
|
-
assertEquals(buffer[1], triggerZero);
|
|
2054
|
-
|
|
2055
|
-
// Trigger with false value
|
|
2056
|
-
buffer.length = 0;
|
|
2057
|
-
const triggerFalse: LogRecord = {
|
|
2058
|
-
...error,
|
|
2059
|
-
properties: { value: false },
|
|
2060
|
-
};
|
|
2061
|
-
sink(triggerFalse);
|
|
2062
|
-
assertEquals(buffer.length, 2);
|
|
2063
|
-
assertEquals(buffer[0], falseValue);
|
|
2064
|
-
assertEquals(buffer[1], triggerFalse);
|
|
2065
|
-
});
|
|
2066
|
-
|
|
2067
|
-
test("fingersCrossed() - context isolation only (no category isolation)", () => {
|
|
2068
|
-
const buffer: LogRecord[] = [];
|
|
2069
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2070
|
-
isolateByContext: { keys: ["requestId"] },
|
|
2071
|
-
});
|
|
2072
|
-
|
|
2073
|
-
// Different categories, same context
|
|
2074
|
-
const cat1Req1: LogRecord = {
|
|
2075
|
-
...debug,
|
|
2076
|
-
category: ["cat1"],
|
|
2077
|
-
properties: { requestId: "req-1" },
|
|
2078
|
-
};
|
|
2079
|
-
const cat2Req1: LogRecord = {
|
|
2080
|
-
...debug,
|
|
2081
|
-
category: ["cat2"],
|
|
2082
|
-
properties: { requestId: "req-1" },
|
|
2083
|
-
};
|
|
2084
|
-
const cat1Req2: LogRecord = {
|
|
2085
|
-
...debug,
|
|
2086
|
-
category: ["cat1"],
|
|
2087
|
-
properties: { requestId: "req-2" },
|
|
2088
|
-
};
|
|
2089
|
-
|
|
2090
|
-
sink(cat1Req1);
|
|
2091
|
-
sink(cat2Req1);
|
|
2092
|
-
sink(cat1Req2);
|
|
2093
|
-
assertEquals(buffer.length, 0); // All buffered
|
|
2094
|
-
|
|
2095
|
-
// Error in any category with req-1 should flush all req-1 logs
|
|
2096
|
-
const triggerReq1: LogRecord = {
|
|
2097
|
-
...error,
|
|
2098
|
-
category: ["cat3"],
|
|
2099
|
-
properties: { requestId: "req-1" },
|
|
2100
|
-
};
|
|
2101
|
-
sink(triggerReq1);
|
|
2102
|
-
assertEquals(buffer.length, 3);
|
|
2103
|
-
assertEquals(buffer[0], cat1Req1);
|
|
2104
|
-
assertEquals(buffer[1], cat2Req1);
|
|
2105
|
-
assertEquals(buffer[2], triggerReq1);
|
|
2106
|
-
|
|
2107
|
-
// req-2 still buffered
|
|
2108
|
-
buffer.length = 0;
|
|
2109
|
-
const triggerReq2: LogRecord = {
|
|
2110
|
-
...error,
|
|
2111
|
-
category: ["cat1"],
|
|
2112
|
-
properties: { requestId: "req-2" },
|
|
2113
|
-
};
|
|
2114
|
-
sink(triggerReq2);
|
|
2115
|
-
assertEquals(buffer.length, 2);
|
|
2116
|
-
assertEquals(buffer[0], cat1Req2);
|
|
2117
|
-
assertEquals(buffer[1], triggerReq2);
|
|
2118
|
-
});
|
|
2119
|
-
|
|
2120
|
-
test("fingersCrossed() - context isolation with nested objects", () => {
|
|
2121
|
-
const buffer: LogRecord[] = [];
|
|
2122
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2123
|
-
isolateByContext: { keys: ["user"] },
|
|
2124
|
-
});
|
|
2125
|
-
|
|
2126
|
-
// Records with nested object values
|
|
2127
|
-
const user1: LogRecord = {
|
|
2128
|
-
...debug,
|
|
2129
|
-
properties: { user: { id: 1, name: "Alice" } },
|
|
2130
|
-
};
|
|
2131
|
-
const user1Same: LogRecord = {
|
|
2132
|
-
...debug,
|
|
2133
|
-
properties: { user: { id: 1, name: "Alice" } },
|
|
2134
|
-
};
|
|
2135
|
-
const user2: LogRecord = {
|
|
2136
|
-
...debug,
|
|
2137
|
-
properties: { user: { id: 2, name: "Bob" } },
|
|
2138
|
-
};
|
|
2139
|
-
|
|
2140
|
-
sink(user1);
|
|
2141
|
-
sink(user1Same);
|
|
2142
|
-
sink(user2);
|
|
2143
|
-
assertEquals(buffer.length, 0); // All buffered
|
|
2144
|
-
|
|
2145
|
-
// Trigger with same user object
|
|
2146
|
-
const triggerUser1: LogRecord = {
|
|
2147
|
-
...error,
|
|
2148
|
-
properties: { user: { id: 1, name: "Alice" } },
|
|
2149
|
-
};
|
|
2150
|
-
sink(triggerUser1);
|
|
2151
|
-
assertEquals(buffer.length, 3);
|
|
2152
|
-
assertEquals(buffer[0], user1);
|
|
2153
|
-
assertEquals(buffer[1], user1Same);
|
|
2154
|
-
assertEquals(buffer[2], triggerUser1);
|
|
2155
|
-
|
|
2156
|
-
// user2 still buffered
|
|
2157
|
-
buffer.length = 0;
|
|
2158
|
-
const triggerUser2: LogRecord = {
|
|
2159
|
-
...error,
|
|
2160
|
-
properties: { user: { id: 2, name: "Bob" } },
|
|
2161
|
-
};
|
|
2162
|
-
sink(triggerUser2);
|
|
2163
|
-
assertEquals(buffer.length, 2);
|
|
2164
|
-
assertEquals(buffer[0], user2);
|
|
2165
|
-
assertEquals(buffer[1], triggerUser2);
|
|
2166
|
-
});
|
|
2167
|
-
|
|
2168
|
-
test("fingersCrossed() - context isolation after trigger", () => {
|
|
2169
|
-
const buffer: LogRecord[] = [];
|
|
2170
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2171
|
-
isolateByContext: { keys: ["requestId"] },
|
|
2172
|
-
});
|
|
2173
|
-
|
|
2174
|
-
// Trigger req-1 immediately
|
|
2175
|
-
const req1Error: LogRecord = {
|
|
2176
|
-
...error,
|
|
2177
|
-
properties: { requestId: "req-1" },
|
|
2178
|
-
};
|
|
2179
|
-
sink(req1Error);
|
|
2180
|
-
assertEquals(buffer.length, 1);
|
|
2181
|
-
assertEquals(buffer[0], req1Error);
|
|
2182
|
-
|
|
2183
|
-
// After trigger, req-1 logs pass through
|
|
2184
|
-
const req1Debug: LogRecord = {
|
|
2185
|
-
...debug,
|
|
2186
|
-
properties: { requestId: "req-1" },
|
|
2187
|
-
};
|
|
2188
|
-
sink(req1Debug);
|
|
2189
|
-
assertEquals(buffer.length, 2);
|
|
2190
|
-
assertEquals(buffer[1], req1Debug);
|
|
2191
|
-
|
|
2192
|
-
// But req-2 logs are still buffered
|
|
2193
|
-
const req2Debug: LogRecord = {
|
|
2194
|
-
...debug,
|
|
2195
|
-
properties: { requestId: "req-2" },
|
|
2196
|
-
};
|
|
2197
|
-
sink(req2Debug);
|
|
2198
|
-
assertEquals(buffer.length, 2); // No change
|
|
2199
|
-
|
|
2200
|
-
// Until req-2 triggers
|
|
2201
|
-
const req2Error: LogRecord = {
|
|
2202
|
-
...error,
|
|
2203
|
-
properties: { requestId: "req-2" },
|
|
2204
|
-
};
|
|
2205
|
-
sink(req2Error);
|
|
2206
|
-
assertEquals(buffer.length, 4);
|
|
2207
|
-
assertEquals(buffer[2], req2Debug);
|
|
2208
|
-
assertEquals(buffer[3], req2Error);
|
|
2209
|
-
});
|
|
2210
|
-
|
|
2211
|
-
test("fingersCrossed() - TTL-based buffer cleanup", async () => {
|
|
2212
|
-
const buffer: LogRecord[] = [];
|
|
2213
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2214
|
-
isolateByContext: {
|
|
2215
|
-
keys: ["requestId"],
|
|
2216
|
-
bufferTtlMs: 100, // 100ms TTL
|
|
2217
|
-
cleanupIntervalMs: 50, // cleanup every 50ms
|
|
2218
|
-
},
|
|
2219
|
-
}) as Sink & Disposable;
|
|
2220
|
-
|
|
2221
|
-
try {
|
|
2222
|
-
// Create records with different request IDs
|
|
2223
|
-
const req1Record: LogRecord = {
|
|
2224
|
-
...debug,
|
|
2225
|
-
properties: { requestId: "req-1" },
|
|
2226
|
-
timestamp: Date.now(),
|
|
2227
|
-
};
|
|
2228
|
-
const req2Record: LogRecord = {
|
|
2229
|
-
...debug,
|
|
2230
|
-
properties: { requestId: "req-2" },
|
|
2231
|
-
timestamp: Date.now(),
|
|
2232
|
-
};
|
|
2233
|
-
|
|
2234
|
-
// Add records to buffers
|
|
2235
|
-
sink(req1Record);
|
|
2236
|
-
sink(req2Record);
|
|
2237
|
-
|
|
2238
|
-
// Wait for TTL to expire and cleanup to run
|
|
2239
|
-
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2240
|
-
|
|
2241
|
-
// Add a new record after TTL expiry
|
|
2242
|
-
const req3Record: LogRecord = {
|
|
2243
|
-
...debug,
|
|
2244
|
-
properties: { requestId: "req-3" },
|
|
2245
|
-
timestamp: Date.now(),
|
|
2246
|
-
};
|
|
2247
|
-
sink(req3Record);
|
|
2248
|
-
|
|
2249
|
-
// Trigger an error for req-1 (should not flush expired req-1 buffer)
|
|
2250
|
-
const req1Error: LogRecord = {
|
|
2251
|
-
...error,
|
|
2252
|
-
properties: { requestId: "req-1" },
|
|
2253
|
-
timestamp: Date.now(),
|
|
2254
|
-
};
|
|
2255
|
-
sink(req1Error);
|
|
2256
|
-
|
|
2257
|
-
// Should only have req-1 error (req-1 debug was cleaned up by TTL)
|
|
2258
|
-
assertEquals(buffer.length, 1);
|
|
2259
|
-
assertEquals(buffer[0], req1Error);
|
|
2260
|
-
|
|
2261
|
-
// Trigger an error for req-3 (should flush req-3 buffer)
|
|
2262
|
-
buffer.length = 0; // Clear buffer
|
|
2263
|
-
const req3Error: LogRecord = {
|
|
2264
|
-
...error,
|
|
2265
|
-
properties: { requestId: "req-3" },
|
|
2266
|
-
timestamp: Date.now(),
|
|
2267
|
-
};
|
|
2268
|
-
sink(req3Error);
|
|
2269
|
-
|
|
2270
|
-
// Should have both req-3 debug and error
|
|
2271
|
-
assertEquals(buffer.length, 2);
|
|
2272
|
-
assertEquals(buffer[0], req3Record);
|
|
2273
|
-
assertEquals(buffer[1], req3Error);
|
|
2274
|
-
} finally {
|
|
2275
|
-
// Clean up timer
|
|
2276
|
-
sink[Symbol.dispose]();
|
|
2277
|
-
}
|
|
2278
|
-
});
|
|
2279
|
-
|
|
2280
|
-
test("fingersCrossed() - TTL disabled when bufferTtlMs is zero", () => {
|
|
2281
|
-
const buffer: LogRecord[] = [];
|
|
2282
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2283
|
-
isolateByContext: {
|
|
2284
|
-
keys: ["requestId"],
|
|
2285
|
-
bufferTtlMs: 0, // TTL disabled
|
|
2286
|
-
},
|
|
2287
|
-
});
|
|
2288
|
-
|
|
2289
|
-
// Should return a regular sink without disposal functionality
|
|
2290
|
-
assertEquals("dispose" in sink, false);
|
|
2291
|
-
|
|
2292
|
-
// Add a record
|
|
2293
|
-
const record: LogRecord = {
|
|
2294
|
-
...debug,
|
|
2295
|
-
properties: { requestId: "req-1" },
|
|
2296
|
-
};
|
|
2297
|
-
sink(record);
|
|
2298
|
-
|
|
2299
|
-
// Trigger should work normally
|
|
2300
|
-
const errorRecord: LogRecord = {
|
|
2301
|
-
...error,
|
|
2302
|
-
properties: { requestId: "req-1" },
|
|
2303
|
-
};
|
|
2304
|
-
sink(errorRecord);
|
|
2305
|
-
|
|
2306
|
-
assertEquals(buffer.length, 2);
|
|
2307
|
-
assertEquals(buffer[0], record);
|
|
2308
|
-
assertEquals(buffer[1], errorRecord);
|
|
2309
|
-
});
|
|
2310
|
-
|
|
2311
|
-
test("fingersCrossed() - TTL disabled when bufferTtlMs is undefined", () => {
|
|
2312
|
-
const buffer: LogRecord[] = [];
|
|
2313
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2314
|
-
isolateByContext: {
|
|
2315
|
-
keys: ["requestId"],
|
|
2316
|
-
// bufferTtlMs not specified
|
|
2317
|
-
},
|
|
2318
|
-
});
|
|
2319
|
-
|
|
2320
|
-
// Should return a regular sink without disposal functionality
|
|
2321
|
-
assertEquals("dispose" in sink, false);
|
|
2322
|
-
});
|
|
2323
|
-
|
|
2324
|
-
test("fingersCrossed() - LRU-based buffer eviction", () => {
|
|
2325
|
-
const buffer: LogRecord[] = [];
|
|
2326
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2327
|
-
isolateByContext: {
|
|
2328
|
-
keys: ["requestId"],
|
|
2329
|
-
maxContexts: 2, // Only keep 2 context buffers
|
|
2330
|
-
},
|
|
2331
|
-
});
|
|
2332
|
-
|
|
2333
|
-
// Step 1: Add req-1
|
|
2334
|
-
const req1Record: LogRecord = {
|
|
2335
|
-
...debug,
|
|
2336
|
-
properties: { requestId: "req-1" },
|
|
2337
|
-
};
|
|
2338
|
-
sink(req1Record);
|
|
2339
|
-
|
|
2340
|
-
// Step 2: Add req-2
|
|
2341
|
-
const req2Record: LogRecord = {
|
|
2342
|
-
...debug,
|
|
2343
|
-
properties: { requestId: "req-2" },
|
|
2344
|
-
};
|
|
2345
|
-
sink(req2Record);
|
|
2346
|
-
|
|
2347
|
-
// Step 3: Add req-3 (should evict req-1)
|
|
2348
|
-
const req3Record: LogRecord = {
|
|
2349
|
-
...debug,
|
|
2350
|
-
properties: { requestId: "req-3" },
|
|
2351
|
-
};
|
|
2352
|
-
sink(req3Record);
|
|
2353
|
-
|
|
2354
|
-
// Test req-1 was evicted by triggering error
|
|
2355
|
-
const req1Error: LogRecord = {
|
|
2356
|
-
...error,
|
|
2357
|
-
properties: { requestId: "req-1" },
|
|
2358
|
-
};
|
|
2359
|
-
sink(req1Error);
|
|
2360
|
-
|
|
2361
|
-
// If req-1 was evicted, should only have error (length=1)
|
|
2362
|
-
// If req-1 wasn't evicted, should have debug+error (length=2)
|
|
2363
|
-
assertEquals(buffer.length, 1, "req-1 should have been evicted by LRU");
|
|
2364
|
-
assertEquals(buffer[0], req1Error);
|
|
2365
|
-
});
|
|
2366
|
-
|
|
2367
|
-
test("fingersCrossed() - LRU eviction order with access updates", async () => {
|
|
2368
|
-
const buffer: LogRecord[] = [];
|
|
2369
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2370
|
-
isolateByContext: {
|
|
2371
|
-
keys: ["requestId"],
|
|
2372
|
-
maxContexts: 2,
|
|
2373
|
-
},
|
|
2374
|
-
});
|
|
2375
|
-
|
|
2376
|
-
// Add two contexts with time gap to ensure different timestamps
|
|
2377
|
-
const req1Record: LogRecord = {
|
|
2378
|
-
...debug,
|
|
2379
|
-
properties: { requestId: "req-1" },
|
|
2380
|
-
};
|
|
2381
|
-
sink(req1Record); // req-1 is oldest
|
|
2382
|
-
|
|
2383
|
-
// Small delay to ensure different lastAccess times
|
|
2384
|
-
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
2385
|
-
|
|
2386
|
-
const req2Record: LogRecord = {
|
|
2387
|
-
...debug,
|
|
2388
|
-
properties: { requestId: "req-2" },
|
|
2389
|
-
};
|
|
2390
|
-
sink(req2Record); // req-2 is newest
|
|
2391
|
-
|
|
2392
|
-
// Access req-1 again after another delay to make it more recent
|
|
2393
|
-
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
2394
|
-
|
|
2395
|
-
const req1Second: LogRecord = {
|
|
2396
|
-
...debug,
|
|
2397
|
-
properties: { requestId: "req-1" },
|
|
2398
|
-
};
|
|
2399
|
-
sink(req1Second); // Now req-2 is oldest, req-1 is newest
|
|
2400
|
-
|
|
2401
|
-
// Add third context - should evict req-2 (now the oldest)
|
|
2402
|
-
const req3Record: LogRecord = {
|
|
2403
|
-
...debug,
|
|
2404
|
-
properties: { requestId: "req-3" },
|
|
2405
|
-
};
|
|
2406
|
-
sink(req3Record);
|
|
2407
|
-
|
|
2408
|
-
// Verify req-2 was evicted
|
|
2409
|
-
const req2Error: LogRecord = {
|
|
2410
|
-
...error,
|
|
2411
|
-
properties: { requestId: "req-2" },
|
|
2412
|
-
};
|
|
2413
|
-
sink(req2Error);
|
|
2414
|
-
|
|
2415
|
-
// Should only have error record (no buffered records)
|
|
2416
|
-
assertEquals(buffer.length, 1, "req-2 should have been evicted");
|
|
2417
|
-
assertEquals(buffer[0], req2Error);
|
|
2418
|
-
});
|
|
2419
|
-
|
|
2420
|
-
test("fingersCrossed() - LRU disabled when maxContexts is zero", () => {
|
|
2421
|
-
const buffer: LogRecord[] = [];
|
|
2422
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2423
|
-
isolateByContext: {
|
|
2424
|
-
keys: ["requestId"],
|
|
2425
|
-
maxContexts: 0, // LRU disabled
|
|
2426
|
-
},
|
|
2427
|
-
});
|
|
2428
|
-
|
|
2429
|
-
// Create many contexts - should not be limited
|
|
2430
|
-
for (let i = 0; i < 100; i++) {
|
|
2431
|
-
const record: LogRecord = {
|
|
2432
|
-
...debug,
|
|
2433
|
-
properties: { requestId: `req-${i}` },
|
|
2434
|
-
};
|
|
2435
|
-
sink(record);
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
// Trigger the last context
|
|
2439
|
-
const errorRecord: LogRecord = {
|
|
2440
|
-
...error,
|
|
2441
|
-
properties: { requestId: "req-99" },
|
|
2442
|
-
};
|
|
2443
|
-
sink(errorRecord);
|
|
2444
|
-
|
|
2445
|
-
// Should have both debug and error records
|
|
2446
|
-
assertEquals(buffer.length, 2);
|
|
2447
|
-
assertEquals(buffer[0].properties?.requestId, "req-99");
|
|
2448
|
-
assertEquals(buffer[1], errorRecord);
|
|
2449
|
-
});
|
|
2450
|
-
|
|
2451
|
-
test("fingersCrossed() - LRU disabled when maxContexts is undefined", () => {
|
|
2452
|
-
const buffer: LogRecord[] = [];
|
|
2453
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2454
|
-
isolateByContext: {
|
|
2455
|
-
keys: ["requestId"],
|
|
2456
|
-
// maxContexts not specified
|
|
2457
|
-
},
|
|
2458
|
-
});
|
|
2459
|
-
|
|
2460
|
-
// Should work normally without LRU limits
|
|
2461
|
-
const record: LogRecord = {
|
|
2462
|
-
...debug,
|
|
2463
|
-
properties: { requestId: "req-1" },
|
|
2464
|
-
};
|
|
2465
|
-
sink(record);
|
|
2466
|
-
|
|
2467
|
-
const errorRecord: LogRecord = {
|
|
2468
|
-
...error,
|
|
2469
|
-
properties: { requestId: "req-1" },
|
|
2470
|
-
};
|
|
2471
|
-
sink(errorRecord);
|
|
2472
|
-
|
|
2473
|
-
assertEquals(buffer.length, 2);
|
|
2474
|
-
assertEquals(buffer[0], record);
|
|
2475
|
-
assertEquals(buffer[1], errorRecord);
|
|
2476
|
-
});
|
|
2477
|
-
|
|
2478
|
-
test("fingersCrossed() - Combined TTL and LRU functionality", async () => {
|
|
2479
|
-
const buffer: LogRecord[] = [];
|
|
2480
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2481
|
-
isolateByContext: {
|
|
2482
|
-
keys: ["requestId"],
|
|
2483
|
-
maxContexts: 2, // LRU limit
|
|
2484
|
-
bufferTtlMs: 100, // TTL limit
|
|
2485
|
-
cleanupIntervalMs: 50, // cleanup interval
|
|
2486
|
-
},
|
|
2487
|
-
}) as Sink & Disposable;
|
|
2488
|
-
|
|
2489
|
-
try {
|
|
2490
|
-
// Create records for multiple contexts
|
|
2491
|
-
const req1Record: LogRecord = {
|
|
2492
|
-
...debug,
|
|
2493
|
-
properties: { requestId: "req-1" },
|
|
2494
|
-
timestamp: Date.now(),
|
|
2495
|
-
};
|
|
2496
|
-
const req2Record: LogRecord = {
|
|
2497
|
-
...debug,
|
|
2498
|
-
properties: { requestId: "req-2" },
|
|
2499
|
-
timestamp: Date.now(),
|
|
2500
|
-
};
|
|
2501
|
-
|
|
2502
|
-
// Add two contexts (within LRU limit)
|
|
2503
|
-
sink(req1Record);
|
|
2504
|
-
sink(req2Record);
|
|
2505
|
-
|
|
2506
|
-
// Wait for TTL to expire
|
|
2507
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
2508
|
-
|
|
2509
|
-
// Add a third context (should work because TTL cleaned up old ones)
|
|
2510
|
-
const req3Record: LogRecord = {
|
|
2511
|
-
...debug,
|
|
2512
|
-
properties: { requestId: "req-3" },
|
|
2513
|
-
timestamp: Date.now(),
|
|
2514
|
-
};
|
|
2515
|
-
sink(req3Record);
|
|
2516
|
-
|
|
2517
|
-
// Trigger req-1 (should not find buffered records due to TTL expiry)
|
|
2518
|
-
const req1Error: LogRecord = {
|
|
2519
|
-
...error,
|
|
2520
|
-
properties: { requestId: "req-1" },
|
|
2521
|
-
timestamp: Date.now(),
|
|
2522
|
-
};
|
|
2523
|
-
sink(req1Error);
|
|
2524
|
-
|
|
2525
|
-
// Should only have the error record
|
|
2526
|
-
assertEquals(buffer.length, 1);
|
|
2527
|
-
assertEquals(buffer[0], req1Error);
|
|
2528
|
-
|
|
2529
|
-
// Clear buffer and trigger req-3 (should have recent record)
|
|
2530
|
-
buffer.length = 0;
|
|
2531
|
-
const req3Error: LogRecord = {
|
|
2532
|
-
...error,
|
|
2533
|
-
properties: { requestId: "req-3" },
|
|
2534
|
-
timestamp: Date.now(),
|
|
2535
|
-
};
|
|
2536
|
-
sink(req3Error);
|
|
2537
|
-
|
|
2538
|
-
// Should have both debug and error records
|
|
2539
|
-
assertEquals(buffer.length, 2);
|
|
2540
|
-
assertEquals(buffer[0], req3Record);
|
|
2541
|
-
assertEquals(buffer[1], req3Error);
|
|
2542
|
-
} finally {
|
|
2543
|
-
sink[Symbol.dispose]();
|
|
2544
|
-
}
|
|
2545
|
-
});
|
|
2546
|
-
|
|
2547
|
-
test("fingersCrossed() - LRU priority over TTL for active contexts", () => {
|
|
2548
|
-
const buffer: LogRecord[] = [];
|
|
2549
|
-
const sink = fingersCrossed(buffer.push.bind(buffer), {
|
|
2550
|
-
isolateByContext: {
|
|
2551
|
-
keys: ["requestId"],
|
|
2552
|
-
maxContexts: 2,
|
|
2553
|
-
bufferTtlMs: 10000, // Long TTL (10 seconds)
|
|
2554
|
-
},
|
|
2555
|
-
}) as Sink & Disposable;
|
|
2556
|
-
|
|
2557
|
-
try {
|
|
2558
|
-
// Create 3 contexts quickly (before TTL expires)
|
|
2559
|
-
const req1Record: LogRecord = {
|
|
2560
|
-
...debug,
|
|
2561
|
-
properties: { requestId: "req-1" },
|
|
2562
|
-
};
|
|
2563
|
-
const req2Record: LogRecord = {
|
|
2564
|
-
...debug,
|
|
2565
|
-
properties: { requestId: "req-2" },
|
|
2566
|
-
};
|
|
2567
|
-
const req3Record: LogRecord = {
|
|
2568
|
-
...debug,
|
|
2569
|
-
properties: { requestId: "req-3" },
|
|
2570
|
-
};
|
|
2571
|
-
|
|
2572
|
-
sink(req1Record); // LRU position: oldest
|
|
2573
|
-
sink(req2Record); // LRU position: middle
|
|
2574
|
-
sink(req3Record); // LRU position: newest, should evict req-1 due to LRU
|
|
2575
|
-
|
|
2576
|
-
// Now trigger req-2 (should have buffered record)
|
|
2577
|
-
const req2Error: LogRecord = {
|
|
2578
|
-
...error,
|
|
2579
|
-
properties: { requestId: "req-2" },
|
|
2580
|
-
};
|
|
2581
|
-
sink(req2Error);
|
|
2582
|
-
|
|
2583
|
-
// Should have both debug and error records
|
|
2584
|
-
assertEquals(buffer.length, 2);
|
|
2585
|
-
assertEquals(buffer[0], req2Record);
|
|
2586
|
-
assertEquals(buffer[1], req2Error);
|
|
2587
|
-
} finally {
|
|
2588
|
-
sink[Symbol.dispose]();
|
|
2589
|
-
}
|
|
2590
|
-
});
|