autotel-edge 3.16.0 → 3.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-YWMTEG63.js → chunk-QXL3GNVV.js} +3 -3
- package/dist/{chunk-YWMTEG63.js.map → chunk-QXL3GNVV.js.map} +1 -1
- package/dist/events.d.ts +2 -2
- package/dist/events.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/logger.d.ts +229 -20
- package/dist/logger.js +774 -33
- package/dist/logger.js.map +1 -1
- package/dist/sampling.d.ts +1 -1
- package/dist/{types-BOs00OpG.d.ts → types-CZFcIjJ4.d.ts} +1 -1
- package/package.json +1 -1
- package/src/api/logger.test.ts +1339 -99
- package/src/api/logger.ts +894 -62
- package/src/api/redact.test.ts +432 -0
- package/src/api/redact.ts +402 -0
- package/src/logger.ts +13 -0
package/src/api/logger.test.ts
CHANGED
|
@@ -30,17 +30,23 @@ describe('Edge Logger', () => {
|
|
|
30
30
|
expect(logger.error).toBeDefined();
|
|
31
31
|
expect(logger.warn).toBeDefined();
|
|
32
32
|
expect(logger.debug).toBeDefined();
|
|
33
|
+
expect(logger.trace).toBeDefined();
|
|
34
|
+
expect(logger.fatal).toBeDefined();
|
|
35
|
+
expect(logger.silent).toBeDefined();
|
|
36
|
+
expect(logger.isLevelEnabled).toBeDefined();
|
|
37
|
+
expect(logger.bindings).toBeDefined();
|
|
38
|
+
expect(logger.setBindings).toBeDefined();
|
|
33
39
|
});
|
|
34
40
|
|
|
35
41
|
it('should log info messages with service name', () => {
|
|
36
42
|
const logger = createEdgeLogger('test-service');
|
|
37
|
-
logger.info(
|
|
43
|
+
logger.info({ key: 'value' }, 'test message');
|
|
38
44
|
|
|
39
45
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
40
46
|
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
41
47
|
|
|
42
48
|
expect(logOutput).toMatchObject({
|
|
43
|
-
level:
|
|
49
|
+
level: 30,
|
|
44
50
|
service: 'test-service',
|
|
45
51
|
msg: 'test message',
|
|
46
52
|
key: 'value',
|
|
@@ -48,47 +54,192 @@ describe('Edge Logger', () => {
|
|
|
48
54
|
expect(logOutput.timestamp).toBeDefined();
|
|
49
55
|
});
|
|
50
56
|
|
|
57
|
+
it('should support pino-style object-first info calls', () => {
|
|
58
|
+
const logger = createEdgeLogger('test-service');
|
|
59
|
+
logger.info({ userId: '123' }, 'User created');
|
|
60
|
+
|
|
61
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
62
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
63
|
+
|
|
64
|
+
expect(logOutput).toMatchObject({
|
|
65
|
+
level: 30,
|
|
66
|
+
service: 'test-service',
|
|
67
|
+
msg: 'User created',
|
|
68
|
+
userId: '123',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should support pino-style object-only info calls', () => {
|
|
73
|
+
const logger = createEdgeLogger('test-service');
|
|
74
|
+
logger.info({ userId: '123' });
|
|
75
|
+
|
|
76
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
77
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
78
|
+
|
|
79
|
+
expect(logOutput).toMatchObject({
|
|
80
|
+
level: 30,
|
|
81
|
+
service: 'test-service',
|
|
82
|
+
userId: '123',
|
|
83
|
+
});
|
|
84
|
+
expect(logOutput).not.toHaveProperty('msg');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should omit msg for pino-style object-only info calls', () => {
|
|
88
|
+
const logger = createEdgeLogger('test-service');
|
|
89
|
+
logger.info({ userId: '123' });
|
|
90
|
+
|
|
91
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
92
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
93
|
+
|
|
94
|
+
expect(logOutput).not.toHaveProperty('msg');
|
|
95
|
+
expect(logOutput.userId).toBe('123');
|
|
96
|
+
});
|
|
97
|
+
|
|
51
98
|
it('should log error messages', () => {
|
|
52
99
|
const logger = createEdgeLogger('test-service');
|
|
53
100
|
const error = new Error('test error');
|
|
54
|
-
logger.error('error occurred'
|
|
101
|
+
logger.error(error, 'error occurred');
|
|
102
|
+
|
|
103
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
104
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
105
|
+
|
|
106
|
+
expect(logOutput).toMatchObject({
|
|
107
|
+
level: 50,
|
|
108
|
+
service: 'test-service',
|
|
109
|
+
msg: 'error occurred',
|
|
110
|
+
});
|
|
111
|
+
expect(logOutput.err).toMatchObject({
|
|
112
|
+
message: 'test error',
|
|
113
|
+
type: 'Error',
|
|
114
|
+
});
|
|
115
|
+
expect(typeof logOutput.err.stack).toBe('string');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should support pino-style error-first calls', () => {
|
|
119
|
+
const logger = createEdgeLogger('test-service');
|
|
120
|
+
logger.error(
|
|
121
|
+
{ requestId: 'req-1', error: 'test error' },
|
|
122
|
+
'error occurred',
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
126
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
127
|
+
|
|
128
|
+
expect(logOutput).toMatchObject({
|
|
129
|
+
level: 50,
|
|
130
|
+
service: 'test-service',
|
|
131
|
+
msg: 'error occurred',
|
|
132
|
+
requestId: 'req-1',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should support pino-style error object calls', () => {
|
|
137
|
+
const logger = createEdgeLogger('test-service');
|
|
138
|
+
const error = new Error('test error');
|
|
139
|
+
logger.error(error, 'error occurred');
|
|
55
140
|
|
|
56
141
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
57
142
|
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
58
143
|
|
|
59
144
|
expect(logOutput).toMatchObject({
|
|
60
|
-
level:
|
|
145
|
+
level: 50,
|
|
61
146
|
service: 'test-service',
|
|
62
147
|
msg: 'error occurred',
|
|
63
|
-
error: 'test error',
|
|
64
148
|
});
|
|
65
|
-
expect(logOutput.
|
|
149
|
+
expect(logOutput.err).toMatchObject({
|
|
150
|
+
message: 'test error',
|
|
151
|
+
type: 'Error',
|
|
152
|
+
});
|
|
153
|
+
expect(typeof logOutput.err.stack).toBe('string');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should use err as the default error key like pino', () => {
|
|
157
|
+
const logger = createEdgeLogger('test-service');
|
|
158
|
+
const error = new Error('test error');
|
|
159
|
+
|
|
160
|
+
logger.error(error);
|
|
161
|
+
|
|
162
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
163
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
164
|
+
|
|
165
|
+
expect(logOutput.msg).toBe('test error');
|
|
166
|
+
expect(logOutput).toHaveProperty('err');
|
|
167
|
+
expect(logOutput).not.toHaveProperty('error');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should nest default error details under err like pino', () => {
|
|
171
|
+
const logger = createEdgeLogger('test-service');
|
|
172
|
+
const error = new Error('test error');
|
|
173
|
+
|
|
174
|
+
logger.error(error);
|
|
175
|
+
|
|
176
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
177
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
178
|
+
|
|
179
|
+
expect(logOutput.err).toMatchObject({
|
|
180
|
+
message: 'test error',
|
|
181
|
+
type: 'Error',
|
|
182
|
+
});
|
|
183
|
+
expect(typeof logOutput.err.stack).toBe('string');
|
|
184
|
+
expect(logOutput).not.toHaveProperty('stack');
|
|
185
|
+
expect(logOutput).not.toHaveProperty('name');
|
|
66
186
|
});
|
|
67
187
|
|
|
68
188
|
it('should log warning messages', () => {
|
|
69
189
|
const logger = createEdgeLogger('test-service');
|
|
70
|
-
logger.warn(
|
|
190
|
+
logger.warn({ reason: 'test' }, 'warning message');
|
|
191
|
+
|
|
192
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
193
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
194
|
+
|
|
195
|
+
expect(logOutput).toMatchObject({
|
|
196
|
+
level: 40,
|
|
197
|
+
service: 'test-service',
|
|
198
|
+
msg: 'warning message',
|
|
199
|
+
reason: 'test',
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should support pino-style object-first warn calls', () => {
|
|
204
|
+
const logger = createEdgeLogger('test-service');
|
|
205
|
+
logger.warn({ reason: 'test' }, 'warning message');
|
|
71
206
|
|
|
72
207
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
73
208
|
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
74
209
|
|
|
75
210
|
expect(logOutput).toMatchObject({
|
|
76
|
-
level:
|
|
211
|
+
level: 40,
|
|
77
212
|
service: 'test-service',
|
|
78
213
|
msg: 'warning message',
|
|
79
214
|
reason: 'test',
|
|
80
215
|
});
|
|
81
216
|
});
|
|
82
217
|
|
|
218
|
+
it('should ignore extra non-format args after a string message like pino', () => {
|
|
219
|
+
const logger = createEdgeLogger('test-service');
|
|
220
|
+
|
|
221
|
+
logger.warn('warning message', { reason: 'test' } as any);
|
|
222
|
+
|
|
223
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
224
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
225
|
+
|
|
226
|
+
expect(logOutput).toMatchObject({
|
|
227
|
+
level: 40,
|
|
228
|
+
service: 'test-service',
|
|
229
|
+
msg: 'warning message',
|
|
230
|
+
});
|
|
231
|
+
expect(logOutput).not.toHaveProperty('reason');
|
|
232
|
+
});
|
|
233
|
+
|
|
83
234
|
it('should log debug messages when level is set to debug', () => {
|
|
84
235
|
const logger = createEdgeLogger('test-service', { level: 'debug' });
|
|
85
|
-
logger.debug(
|
|
236
|
+
logger.debug({ detail: 'verbose' }, 'debug message');
|
|
86
237
|
|
|
87
238
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
88
239
|
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
89
240
|
|
|
90
241
|
expect(logOutput).toMatchObject({
|
|
91
|
-
level:
|
|
242
|
+
level: 20,
|
|
92
243
|
service: 'test-service',
|
|
93
244
|
msg: 'debug message',
|
|
94
245
|
detail: 'verbose',
|
|
@@ -97,7 +248,7 @@ describe('Edge Logger', () => {
|
|
|
97
248
|
|
|
98
249
|
it('should not log debug messages when level is info (default)', () => {
|
|
99
250
|
const logger = createEdgeLogger('test-service');
|
|
100
|
-
logger.debug(
|
|
251
|
+
logger.debug({ detail: 'verbose' }, 'debug message');
|
|
101
252
|
|
|
102
253
|
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
103
254
|
});
|
|
@@ -123,7 +274,9 @@ describe('Edge Logger', () => {
|
|
|
123
274
|
};
|
|
124
275
|
|
|
125
276
|
// Mock trace.getActiveSpan to return our span
|
|
126
|
-
const getActiveSpanSpy = vi
|
|
277
|
+
const getActiveSpanSpy = vi
|
|
278
|
+
.spyOn(trace, 'getActiveSpan')
|
|
279
|
+
.mockReturnValue(mockSpan as any);
|
|
127
280
|
|
|
128
281
|
logger.info('message with trace');
|
|
129
282
|
|
|
@@ -131,7 +284,7 @@ describe('Edge Logger', () => {
|
|
|
131
284
|
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
132
285
|
|
|
133
286
|
expect(logOutput).toMatchObject({
|
|
134
|
-
level:
|
|
287
|
+
level: 30,
|
|
135
288
|
service: 'test-service',
|
|
136
289
|
msg: 'message with trace',
|
|
137
290
|
traceId: 'test-trace-id-16chars',
|
|
@@ -145,23 +298,23 @@ describe('Edge Logger', () => {
|
|
|
145
298
|
it('should handle non-Error objects', () => {
|
|
146
299
|
const logger = createEdgeLogger('test-service');
|
|
147
300
|
const errorObject = { message: 'simple error' };
|
|
148
|
-
logger.error('error occurred'
|
|
301
|
+
logger.error(errorObject, 'error occurred');
|
|
149
302
|
|
|
150
303
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
151
304
|
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
152
305
|
|
|
153
306
|
expect(logOutput).toMatchObject({
|
|
154
|
-
level:
|
|
307
|
+
level: 50,
|
|
155
308
|
service: 'test-service',
|
|
156
309
|
msg: 'error occurred',
|
|
157
|
-
|
|
310
|
+
message: 'simple error',
|
|
158
311
|
});
|
|
159
312
|
});
|
|
160
313
|
|
|
161
314
|
it('should handle null and undefined context gracefully', () => {
|
|
162
315
|
const logger = createEdgeLogger('test-service');
|
|
163
|
-
logger.info('message with null'
|
|
164
|
-
logger.info('message with undefined'
|
|
316
|
+
logger.info('message with null');
|
|
317
|
+
logger.info('message with undefined');
|
|
165
318
|
|
|
166
319
|
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
|
|
167
320
|
|
|
@@ -174,13 +327,13 @@ describe('Edge Logger', () => {
|
|
|
174
327
|
|
|
175
328
|
it('should accept a single context object', () => {
|
|
176
329
|
const logger = createEdgeLogger('test-service');
|
|
177
|
-
logger.info(
|
|
330
|
+
logger.info({ a: 1, b: 2, c: 3 }, 'message');
|
|
178
331
|
|
|
179
332
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
180
333
|
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
181
334
|
|
|
182
335
|
expect(logOutput).toMatchObject({
|
|
183
|
-
level:
|
|
336
|
+
level: 30,
|
|
184
337
|
msg: 'message',
|
|
185
338
|
a: 1,
|
|
186
339
|
b: 2,
|
|
@@ -195,14 +348,192 @@ describe('Edge Logger', () => {
|
|
|
195
348
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
196
349
|
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
197
350
|
|
|
198
|
-
expect(logOutput.timestamp).toMatch(
|
|
351
|
+
expect(logOutput.timestamp).toMatch(
|
|
352
|
+
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should expose a mutable level property', () => {
|
|
357
|
+
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
358
|
+
|
|
359
|
+
expect(logger.level).toBe('info');
|
|
360
|
+
expect(logger.levelVal).toBe(30);
|
|
361
|
+
expect(logger.isLevelEnabled('debug')).toBe(false);
|
|
362
|
+
|
|
363
|
+
logger.level = 'debug';
|
|
364
|
+
|
|
365
|
+
expect(logger.level).toBe('debug');
|
|
366
|
+
expect(logger.levelVal).toBe(20);
|
|
367
|
+
expect(logger.isLevelEnabled('debug')).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should expose pino-like instance metadata', () => {
|
|
371
|
+
const logger = createEdgeLogger('test-service', { msgPrefix: '[api] ' });
|
|
372
|
+
|
|
373
|
+
expect(logger.version).toBeDefined();
|
|
374
|
+
expect(logger.levels.values.info).toBe(30);
|
|
375
|
+
expect(logger.levels.labels[50]).toBe('error');
|
|
376
|
+
expect(logger.useLevelLabels).toBe(false);
|
|
377
|
+
expect(logger.msgPrefix).toBe('[api] ');
|
|
378
|
+
expect(typeof logger.on).toBe('function');
|
|
379
|
+
expect(typeof logger.flush).toBe('function');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should log trace and fatal levels', () => {
|
|
383
|
+
const logger = createEdgeLogger('test-service', { level: 'trace' });
|
|
384
|
+
|
|
385
|
+
logger.trace('trace message');
|
|
386
|
+
logger.fatal('fatal message');
|
|
387
|
+
|
|
388
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
|
|
389
|
+
|
|
390
|
+
const traceOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
391
|
+
const fatalOutput = JSON.parse(consoleLogSpy.mock.calls[1][0]);
|
|
392
|
+
|
|
393
|
+
expect(traceOutput.level).toBe(10);
|
|
394
|
+
expect(fatalOutput.level).toBe(60);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should apply msgPrefix to logged messages', () => {
|
|
398
|
+
const logger = createEdgeLogger('test-service', { msgPrefix: '[api] ' });
|
|
399
|
+
logger.info('hello');
|
|
400
|
+
|
|
401
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
402
|
+
expect(logOutput.msg).toBe('[api] hello');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should support enabled toggling', () => {
|
|
406
|
+
const logger = createEdgeLogger('test-service');
|
|
407
|
+
|
|
408
|
+
logger.enabled = false;
|
|
409
|
+
logger.info('hidden');
|
|
410
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
411
|
+
|
|
412
|
+
logger.enabled = true;
|
|
413
|
+
logger.info('visible');
|
|
414
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should support custom level methods', () => {
|
|
418
|
+
const logger = createEdgeLogger('test-service', {
|
|
419
|
+
level: 'audit',
|
|
420
|
+
customLevels: { audit: 35 },
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
(logger as any).audit({ actor: 'alice' }, 'audit message');
|
|
424
|
+
|
|
425
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
426
|
+
expect(logOutput.level).toBe(35);
|
|
427
|
+
expect(logOutput.msg).toBe('audit message');
|
|
428
|
+
expect(logOutput.actor).toBe('alice');
|
|
429
|
+
expect(logger.isLevelEnabled('audit')).toBe(true);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should expose customLevels and useOnlyCustomLevels on the logger like pino', () => {
|
|
433
|
+
const logger = createEdgeLogger('test-service', {
|
|
434
|
+
level: 'audit',
|
|
435
|
+
customLevels: { audit: 35 },
|
|
436
|
+
useOnlyCustomLevels: true,
|
|
437
|
+
}) as any;
|
|
438
|
+
|
|
439
|
+
expect(logger.customLevels).toEqual({ audit: 35 });
|
|
440
|
+
expect(logger.useOnlyCustomLevels).toBe(true);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should support useOnlyCustomLevels', () => {
|
|
444
|
+
const logger = createEdgeLogger('test-service', {
|
|
445
|
+
level: 'audit',
|
|
446
|
+
customLevels: { audit: 35 },
|
|
447
|
+
useOnlyCustomLevels: true,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
logger.info('not logged');
|
|
451
|
+
(logger as any).audit('logged');
|
|
452
|
+
|
|
453
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
454
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
455
|
+
expect(logOutput.msg).toBe('logged');
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it('should support messageKey, errorKey, and nestedKey', () => {
|
|
459
|
+
const logger = createEdgeLogger('test-service', {
|
|
460
|
+
messageKey: 'message',
|
|
461
|
+
errorKey: 'err',
|
|
462
|
+
nestedKey: 'payload',
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
logger.error({ error: 'boom', requestId: 'req-1' }, 'failed');
|
|
466
|
+
|
|
467
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
468
|
+
expect(logOutput.message).toBe('failed');
|
|
469
|
+
expect(logOutput.payload.error).toBe('boom');
|
|
470
|
+
expect(logOutput.payload.requestId).toBe('req-1');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('should support serializers and formatters', () => {
|
|
474
|
+
const logger = createEdgeLogger('test-service', {
|
|
475
|
+
bindings: { serviceVersion: '1.0.0' },
|
|
476
|
+
serializers: {
|
|
477
|
+
user: (value) => ({ id: (value as any).id }),
|
|
478
|
+
},
|
|
479
|
+
formatters: {
|
|
480
|
+
bindings: (bindings) => ({ ...bindings, bound: true }),
|
|
481
|
+
level: (label, value) => ({ severity: label, severityValue: value }),
|
|
482
|
+
log: (object) => ({ ...object, formatted: true }),
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
logger.info({ user: { id: 'u1', email: 'a@b.com' } }, 'hello');
|
|
487
|
+
|
|
488
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
489
|
+
expect(logOutput.severity).toBe('info');
|
|
490
|
+
expect(logOutput.severityValue).toBe(30);
|
|
491
|
+
expect(logOutput.bound).toBe(true);
|
|
492
|
+
expect(logOutput.formatted).toBe(true);
|
|
493
|
+
expect(logOutput.user).toEqual({ id: 'u1' });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('should support mixins', () => {
|
|
497
|
+
const logger = createEdgeLogger('test-service', {
|
|
498
|
+
mixin: () => ({ mixed: true }),
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
logger.info('hello');
|
|
502
|
+
|
|
503
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
504
|
+
expect(logOutput.mixed).toBe(true);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should call hooks.logMethod with the logger as this', () => {
|
|
508
|
+
let hookThis: unknown;
|
|
509
|
+
let hookLogger: EdgeLogger | undefined;
|
|
510
|
+
const logger = createEdgeLogger('test-service', {
|
|
511
|
+
hooks: {
|
|
512
|
+
logMethod(this: EdgeLogger, args, method, level) {
|
|
513
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias, unicorn/no-this-assignment
|
|
514
|
+
hookThis = this;
|
|
515
|
+
hookLogger = logger;
|
|
516
|
+
expect(args).toEqual([{ userId: '123' }, 'hello']);
|
|
517
|
+
expect(level).toBe(30);
|
|
518
|
+
method(...args);
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
logger.info({ userId: '123' }, 'hello');
|
|
524
|
+
|
|
525
|
+
expect(hookThis).toBe(hookLogger);
|
|
526
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
527
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
528
|
+
expect(logOutput.userId).toBe('123');
|
|
529
|
+
expect(logOutput.msg).toBe('hello');
|
|
199
530
|
});
|
|
200
531
|
});
|
|
201
532
|
|
|
202
533
|
describe('Pretty mode', () => {
|
|
203
534
|
it('should format logs in pretty mode', () => {
|
|
204
535
|
const logger = createEdgeLogger('test-service', { pretty: true });
|
|
205
|
-
logger.info(
|
|
536
|
+
logger.info({ key: 'value' }, 'pretty message');
|
|
206
537
|
|
|
207
538
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
208
539
|
const logOutput = consoleLogSpy.mock.calls[0][0];
|
|
@@ -217,7 +548,7 @@ describe('Edge Logger', () => {
|
|
|
217
548
|
it('should format errors in pretty mode', () => {
|
|
218
549
|
const logger = createEdgeLogger('test-service', { pretty: true });
|
|
219
550
|
const error = new Error('test error');
|
|
220
|
-
logger.error('error occurred'
|
|
551
|
+
logger.error(error, 'error occurred');
|
|
221
552
|
|
|
222
553
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
223
554
|
const logOutput = consoleLogSpy.mock.calls[0][0];
|
|
@@ -230,14 +561,29 @@ describe('Edge Logger', () => {
|
|
|
230
561
|
});
|
|
231
562
|
|
|
232
563
|
describe('Edge cases', () => {
|
|
233
|
-
it('should
|
|
564
|
+
it('should handle circular references safely by default', () => {
|
|
234
565
|
const logger = createEdgeLogger('test-service');
|
|
235
566
|
const circular: any = { a: 1 };
|
|
236
567
|
circular.self = circular;
|
|
237
568
|
|
|
238
|
-
//
|
|
569
|
+
// safe: true (default) handles circular refs with [Circular]
|
|
570
|
+
logger.info(circular, 'circular');
|
|
571
|
+
|
|
572
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
573
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
574
|
+
expect(logOutput.a).toBe(1);
|
|
575
|
+
// self points to the same circular object, which is serialized with [Circular] for its own self-ref
|
|
576
|
+
expect(logOutput.self.a).toBe(1);
|
|
577
|
+
expect(logOutput.self.self).toBe('[Circular]');
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('should throw on circular references when safe is false', () => {
|
|
581
|
+
const logger = createEdgeLogger('test-service', { safe: false });
|
|
582
|
+
const circular: any = { a: 1 };
|
|
583
|
+
circular.self = circular;
|
|
584
|
+
|
|
239
585
|
expect(() => {
|
|
240
|
-
logger.info(
|
|
586
|
+
logger.info(circular, 'circular');
|
|
241
587
|
}).toThrow(/circular/i);
|
|
242
588
|
});
|
|
243
589
|
|
|
@@ -262,106 +608,1000 @@ describe('Edge Logger', () => {
|
|
|
262
608
|
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
263
609
|
expect(logOutput.msg).toBe(specialMessage);
|
|
264
610
|
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
describe('Dynamic log level control', () => {
|
|
268
|
-
it('should override log level via runWithLogLevel', () => {
|
|
269
|
-
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
270
611
|
|
|
271
|
-
|
|
272
|
-
logger
|
|
273
|
-
|
|
612
|
+
it('should not treat repeated shared references as circular', () => {
|
|
613
|
+
const logger = createEdgeLogger('test-service');
|
|
614
|
+
const shared = { nested: true };
|
|
274
615
|
|
|
275
|
-
|
|
276
|
-
runWithLogLevel('debug', () => {
|
|
277
|
-
logger.debug('inside debug context');
|
|
278
|
-
});
|
|
279
|
-
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
616
|
+
logger.info({ a: shared, b: shared }, 'shared refs');
|
|
280
617
|
|
|
281
|
-
// Back to normal: debug filtered out again
|
|
282
|
-
logger.debug('outside context again');
|
|
283
618
|
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
619
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
620
|
+
expect(logOutput.a).toEqual({ nested: true });
|
|
621
|
+
expect(logOutput.b).toEqual({ nested: true });
|
|
284
622
|
});
|
|
623
|
+
});
|
|
285
624
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
logger.
|
|
290
|
-
const callsBeforeNone = consoleLogSpy.mock.calls.length;
|
|
625
|
+
describe('child()', () => {
|
|
626
|
+
it('should create a child logger with merged bindings', () => {
|
|
627
|
+
const logger = createEdgeLogger('test-service');
|
|
628
|
+
const child = logger.child({ requestId: 'req-123' });
|
|
291
629
|
|
|
292
|
-
|
|
293
|
-
logger.info('inside none context');
|
|
294
|
-
logger.error('even errors suppressed');
|
|
295
|
-
});
|
|
296
|
-
expect(consoleLogSpy.mock.calls.length).toBe(callsBeforeNone); // No new logs
|
|
630
|
+
child.info({ extra: 'data' }, 'child message');
|
|
297
631
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
});
|
|
632
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
633
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
301
634
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
635
|
+
expect(logOutput).toMatchObject({
|
|
636
|
+
level: 30,
|
|
637
|
+
service: 'test-service',
|
|
638
|
+
msg: 'child message',
|
|
639
|
+
requestId: 'req-123',
|
|
640
|
+
extra: 'data',
|
|
305
641
|
});
|
|
306
|
-
expect(result).toBe(42);
|
|
307
642
|
});
|
|
308
643
|
|
|
309
|
-
it('should
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
});
|
|
313
|
-
expect(result).toBe('async value');
|
|
314
|
-
});
|
|
644
|
+
it('should merge parent bindings with child bindings', () => {
|
|
645
|
+
const logger = createEdgeLogger('test-service');
|
|
646
|
+
const child = logger.child({ requestId: 'req-123' });
|
|
647
|
+
const grandchild = child.child({ userId: 'user-456' });
|
|
315
648
|
|
|
316
|
-
|
|
317
|
-
const logger = createEdgeLogger('test-service', { level: 'error' });
|
|
649
|
+
grandchild.info('deep message');
|
|
318
650
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
651
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
652
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
322
653
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
654
|
+
expect(logOutput).toMatchObject({
|
|
655
|
+
msg: 'deep message',
|
|
656
|
+
requestId: 'req-123',
|
|
657
|
+
userId: 'user-456',
|
|
326
658
|
});
|
|
327
|
-
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
|
|
328
659
|
});
|
|
329
660
|
|
|
330
|
-
it('should
|
|
331
|
-
const logger = createEdgeLogger('test-service'
|
|
661
|
+
it('should allow child attrs to override bindings', () => {
|
|
662
|
+
const logger = createEdgeLogger('test-service');
|
|
663
|
+
const child = logger.child({ env: 'staging' });
|
|
332
664
|
|
|
333
|
-
|
|
334
|
-
logger.debug('context 1 - debug');
|
|
335
|
-
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
336
|
-
});
|
|
665
|
+
child.info({ env: 'production' }, 'override');
|
|
337
666
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
667
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
668
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
669
|
+
|
|
670
|
+
// Call-time attrs override bindings
|
|
671
|
+
expect(logOutput.env).toBe('production');
|
|
342
672
|
});
|
|
343
|
-
});
|
|
344
673
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
674
|
+
it('should return an EdgeLogger from child()', () => {
|
|
675
|
+
const logger = createEdgeLogger('test-service');
|
|
676
|
+
const child = logger.child({ key: 'val' });
|
|
677
|
+
|
|
678
|
+
expect(child.info).toBeDefined();
|
|
679
|
+
expect(child.error).toBeDefined();
|
|
680
|
+
expect(child.warn).toBeDefined();
|
|
681
|
+
expect(child.debug).toBeDefined();
|
|
682
|
+
expect(child.trace).toBeDefined();
|
|
683
|
+
expect(child.fatal).toBeDefined();
|
|
684
|
+
expect(child.child).toBeDefined();
|
|
348
685
|
});
|
|
349
686
|
|
|
350
|
-
it('should
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
});
|
|
687
|
+
it('should call onChild when creating child loggers', () => {
|
|
688
|
+
const onChild = vi.fn();
|
|
689
|
+
const logger = createEdgeLogger('test-service', { onChild });
|
|
354
690
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
691
|
+
const child = logger.child({ key: 'val' });
|
|
692
|
+
|
|
693
|
+
expect(onChild).toHaveBeenCalledOnce();
|
|
694
|
+
expect(onChild).toHaveBeenCalledWith(child);
|
|
358
695
|
});
|
|
359
696
|
|
|
360
|
-
it('should
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
697
|
+
it('should chain child() calls', () => {
|
|
698
|
+
const logger = createEdgeLogger('test-service');
|
|
699
|
+
const child = logger.child({ a: 1 }).child({ b: 2 }).child({ c: 3 });
|
|
700
|
+
|
|
701
|
+
child.info('chained');
|
|
702
|
+
|
|
703
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
704
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
705
|
+
|
|
706
|
+
expect(logOutput).toMatchObject({ a: 1, b: 2, c: 3 });
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('should not affect parent logger', () => {
|
|
710
|
+
const logger = createEdgeLogger('test-service');
|
|
711
|
+
const child = logger.child({ childOnly: 'yes' });
|
|
712
|
+
|
|
713
|
+
logger.info('parent message');
|
|
714
|
+
|
|
715
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
716
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
717
|
+
|
|
718
|
+
expect(logOutput.childOnly).toBeUndefined();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it('should include child bindings in pretty mode', () => {
|
|
722
|
+
const logger = createEdgeLogger('test-service', { pretty: true });
|
|
723
|
+
const child = logger.child({ requestId: 'req-123' });
|
|
724
|
+
|
|
725
|
+
child.info({ extra: 'data' }, 'pretty child');
|
|
726
|
+
|
|
727
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
728
|
+
const output = consoleLogSpy.mock.calls[0][0];
|
|
729
|
+
expect(output).toContain('pretty child');
|
|
730
|
+
expect(output).toContain('requestId');
|
|
731
|
+
expect(output).toContain('req-123');
|
|
732
|
+
expect(output).toContain('extra');
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
it('should allow child logger level overrides', () => {
|
|
736
|
+
const logger = createEdgeLogger('test-service', { level: 'error' });
|
|
737
|
+
const child = logger.child({ requestId: 'req-123' }, { level: 'debug' });
|
|
738
|
+
|
|
739
|
+
child.debug('child debug');
|
|
740
|
+
|
|
741
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
742
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
743
|
+
expect(logOutput.requestId).toBe('req-123');
|
|
744
|
+
expect(logOutput.level).toBe(20);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('should return cloned bindings and allow additive setBindings', () => {
|
|
748
|
+
const logger = createEdgeLogger('test-service');
|
|
749
|
+
const child = logger.child({ requestId: 'req-123' });
|
|
750
|
+
|
|
751
|
+
expect(child.bindings()).toEqual({ requestId: 'req-123' });
|
|
752
|
+
|
|
753
|
+
const clonedBindings = child.bindings();
|
|
754
|
+
clonedBindings.requestId = 'mutated';
|
|
755
|
+
|
|
756
|
+
expect(child.bindings()).toEqual({ requestId: 'req-123' });
|
|
757
|
+
|
|
758
|
+
child.setBindings({ requestId: 'ignored', userId: 'user-456' });
|
|
759
|
+
expect(child.bindings()).toEqual({
|
|
760
|
+
requestId: 'req-123',
|
|
761
|
+
userId: 'user-456',
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it('should allow child msgPrefix additions', () => {
|
|
766
|
+
const logger = createEdgeLogger('test-service', { msgPrefix: '[parent] ' });
|
|
767
|
+
const child = logger.child(
|
|
768
|
+
{ requestId: 'req-123' },
|
|
769
|
+
{ msgPrefix: '[child] ' },
|
|
770
|
+
);
|
|
771
|
+
|
|
772
|
+
child.info('hello');
|
|
773
|
+
|
|
774
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
775
|
+
expect(logOutput.msg).toBe('[parent] [child] hello');
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('should append child msgPrefix to the parent prefix like pino', () => {
|
|
779
|
+
const logger = createEdgeLogger('test-service', { msgPrefix: '[parent] ' });
|
|
780
|
+
const child = logger.child(
|
|
781
|
+
{ requestId: 'req-123' },
|
|
782
|
+
{ msgPrefix: '[child] ' },
|
|
783
|
+
);
|
|
784
|
+
|
|
785
|
+
child.info('hello');
|
|
786
|
+
|
|
787
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
788
|
+
expect(logOutput.msg).toBe('[parent] [child] hello');
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
describe('Dynamic log level control', () => {
|
|
793
|
+
it('should override log level via runWithLogLevel', () => {
|
|
794
|
+
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
795
|
+
|
|
796
|
+
// Normal behavior: debug filtered out
|
|
797
|
+
logger.debug('outside context');
|
|
798
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
799
|
+
|
|
800
|
+
// Inside debug context: debug logged
|
|
801
|
+
runWithLogLevel('debug', () => {
|
|
802
|
+
logger.debug('inside debug context');
|
|
803
|
+
});
|
|
804
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
805
|
+
|
|
806
|
+
// Back to normal: debug filtered out again
|
|
807
|
+
logger.debug('outside context again');
|
|
808
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('should support silent level to temporarily disable logging', () => {
|
|
812
|
+
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
813
|
+
|
|
814
|
+
logger.info('before silent');
|
|
815
|
+
const callsBeforeSilent = consoleLogSpy.mock.calls.length;
|
|
816
|
+
|
|
817
|
+
runWithLogLevel('silent', () => {
|
|
818
|
+
logger.info('inside silent context');
|
|
819
|
+
logger.error('even errors suppressed');
|
|
820
|
+
});
|
|
821
|
+
expect(consoleLogSpy.mock.calls.length).toBe(callsBeforeSilent); // No new logs
|
|
822
|
+
|
|
823
|
+
logger.info('after silent');
|
|
824
|
+
expect(consoleLogSpy.mock.calls.length).toBe(callsBeforeSilent + 1);
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('should return value from callback', () => {
|
|
828
|
+
const result = runWithLogLevel('debug', () => {
|
|
829
|
+
return 42;
|
|
830
|
+
});
|
|
831
|
+
expect(result).toBe(42);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('should propagate async results', async () => {
|
|
835
|
+
const result = await runWithLogLevel('debug', async () => {
|
|
836
|
+
return 'async value';
|
|
837
|
+
});
|
|
838
|
+
expect(result).toBe('async value');
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should allow raising log level temporarily', () => {
|
|
842
|
+
const logger = createEdgeLogger('test-service', { level: 'error' });
|
|
843
|
+
|
|
844
|
+
logger.info('normal info');
|
|
845
|
+
logger.warn('normal warn');
|
|
846
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
847
|
+
|
|
848
|
+
runWithLogLevel('info', () => {
|
|
849
|
+
logger.info('temporary info');
|
|
850
|
+
logger.warn('temporary warn');
|
|
851
|
+
});
|
|
852
|
+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
it('should isolate log level changes to context', () => {
|
|
856
|
+
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
857
|
+
|
|
858
|
+
runWithLogLevel('debug', () => {
|
|
859
|
+
logger.debug('context 1 - debug');
|
|
860
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
runWithLogLevel('error', () => {
|
|
864
|
+
logger.info('context 2 - info should be filtered');
|
|
865
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce(); // No new logs
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
describe('getActiveLogLevel', () => {
|
|
871
|
+
it('should return undefined when no active level', () => {
|
|
872
|
+
expect(getActiveLogLevel()).toBeUndefined();
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('should return active level inside runWithLogLevel', () => {
|
|
876
|
+
runWithLogLevel('debug', () => {
|
|
877
|
+
expect(getActiveLogLevel()).toBe('debug');
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
runWithLogLevel('error', () => {
|
|
881
|
+
expect(getActiveLogLevel()).toBe('error');
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it('should reset after runWithLogLevel completes', () => {
|
|
886
|
+
runWithLogLevel('debug', () => {
|
|
887
|
+
expect(getActiveLogLevel()).toBe('debug');
|
|
888
|
+
});
|
|
889
|
+
expect(getActiveLogLevel()).toBeUndefined();
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it('should emit level-change events', () => {
|
|
893
|
+
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
894
|
+
const listener = vi.fn();
|
|
895
|
+
|
|
896
|
+
logger.on('level-change', listener);
|
|
897
|
+
logger.level = 'debug';
|
|
898
|
+
|
|
899
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
900
|
+
expect(listener).toHaveBeenCalledWith('debug', 20, 'info', 30, logger);
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it('should support once listeners and flush callbacks', () => {
|
|
904
|
+
const logger = createEdgeLogger('test-service', { level: 'info' });
|
|
905
|
+
const listener = vi.fn();
|
|
906
|
+
const flushCallback = vi.fn();
|
|
907
|
+
|
|
908
|
+
logger.once('level-change', listener);
|
|
909
|
+
logger.level = 'debug';
|
|
910
|
+
logger.level = 'trace';
|
|
911
|
+
logger.flush(flushCallback);
|
|
912
|
+
|
|
913
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
914
|
+
expect(flushCallback).toHaveBeenCalledOnce();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('should honor the provided event name for event emitter methods', () => {
|
|
918
|
+
const logger = createEdgeLogger('test-service') as any;
|
|
919
|
+
const listener = vi.fn();
|
|
920
|
+
|
|
921
|
+
logger.on('custom-event', listener);
|
|
922
|
+
logger.emit('custom-event', 'payload');
|
|
923
|
+
|
|
924
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
925
|
+
expect(listener).toHaveBeenCalledWith('payload');
|
|
926
|
+
expect(logger.eventNames()).toContain('custom-event');
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
describe('redact option', () => {
|
|
931
|
+
it('should redact top-level fields in JSON output', () => {
|
|
932
|
+
const logger = createEdgeLogger('test-service', {
|
|
933
|
+
redact: { paths: ['password', 'token'] },
|
|
934
|
+
});
|
|
935
|
+
logger.info(
|
|
936
|
+
{
|
|
937
|
+
user: 'alice',
|
|
938
|
+
password: 'hunter2',
|
|
939
|
+
token: 'jwt-abc',
|
|
940
|
+
},
|
|
941
|
+
'login',
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
945
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
946
|
+
|
|
947
|
+
expect(logOutput.user).toBe('alice');
|
|
948
|
+
expect(logOutput.password).toBe('[Redacted]');
|
|
949
|
+
expect(logOutput.token).toBe('[Redacted]');
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it('should redact nested paths', () => {
|
|
953
|
+
const logger = createEdgeLogger('test-service', {
|
|
954
|
+
redact: { paths: ['user.email', 'req.headers.authorization'] },
|
|
955
|
+
});
|
|
956
|
+
logger.info(
|
|
957
|
+
{
|
|
958
|
+
user: { email: 'a@b.com', name: 'Alice' },
|
|
959
|
+
req: {
|
|
960
|
+
headers: {
|
|
961
|
+
authorization: 'Bearer token123',
|
|
962
|
+
'content-type': 'application/json',
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
'request',
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
970
|
+
expect(logOutput.user.email).toBe('[Redacted]');
|
|
971
|
+
expect(logOutput.user.name).toBe('Alice');
|
|
972
|
+
expect(logOutput.req.headers.authorization).toBe('[Redacted]');
|
|
973
|
+
expect(logOutput.req.headers['content-type']).toBe('application/json');
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
it('should redact with custom censor', () => {
|
|
977
|
+
const logger = createEdgeLogger('test-service', {
|
|
978
|
+
redact: { paths: ['ssn'], censor: '***' },
|
|
979
|
+
});
|
|
980
|
+
logger.info({ ssn: '123-45-6789', name: 'Alice' }, 'record');
|
|
981
|
+
|
|
982
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
983
|
+
expect(logOutput.ssn).toBe('***');
|
|
984
|
+
expect(logOutput.name).toBe('Alice');
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('should redact with censor function', () => {
|
|
988
|
+
const logger = createEdgeLogger('test-service', {
|
|
989
|
+
redact: {
|
|
990
|
+
paths: ['ccn'],
|
|
991
|
+
censor: (val: unknown) => '****' + String(val).slice(-4),
|
|
992
|
+
},
|
|
993
|
+
});
|
|
994
|
+
logger.info({ ccn: '4111111111111234' }, 'payment');
|
|
995
|
+
|
|
996
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
997
|
+
expect(logOutput.ccn).toBe('****1234');
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('should not mutate original attrs', () => {
|
|
1001
|
+
const logger = createEdgeLogger('test-service', {
|
|
1002
|
+
redact: { paths: ['password'] },
|
|
1003
|
+
});
|
|
1004
|
+
const attrs = { password: 'secret', name: 'Alice' };
|
|
1005
|
+
logger.info(attrs, 'test');
|
|
1006
|
+
|
|
1007
|
+
expect(attrs.password).toBe('secret');
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('should apply redaction in child loggers', () => {
|
|
1011
|
+
const logger = createEdgeLogger('test-service', {
|
|
1012
|
+
redact: { paths: ['password'] },
|
|
1013
|
+
});
|
|
1014
|
+
const child = logger.child({ requestId: 'req-1' });
|
|
1015
|
+
child.info({ password: 'secret', user: 'alice' }, 'child log');
|
|
1016
|
+
|
|
1017
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1018
|
+
expect(logOutput.password).toBe('[Redacted]');
|
|
1019
|
+
expect(logOutput.user).toBe('alice');
|
|
1020
|
+
expect(logOutput.requestId).toBe('req-1');
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it('should redact error attributes', () => {
|
|
1024
|
+
const logger = createEdgeLogger('test-service', {
|
|
1025
|
+
redact: { paths: ['password'] },
|
|
1026
|
+
});
|
|
1027
|
+
logger.error(
|
|
1028
|
+
{
|
|
1029
|
+
password: 'secret',
|
|
1030
|
+
user: 'alice',
|
|
1031
|
+
error: 'bad creds',
|
|
1032
|
+
},
|
|
1033
|
+
'login failed',
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1037
|
+
expect(logOutput.password).toBe('[Redacted]');
|
|
1038
|
+
expect(logOutput.user).toBe('alice');
|
|
1039
|
+
// 'error' field from attrs gets remapped to 'err' (default errorKey)
|
|
1040
|
+
expect(logOutput.err).toBe('bad creds');
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
it('should redact in pretty mode', () => {
|
|
1044
|
+
const logger = createEdgeLogger('test-service', {
|
|
1045
|
+
pretty: true,
|
|
1046
|
+
redact: { paths: ['password'] },
|
|
1047
|
+
});
|
|
1048
|
+
logger.info({ user: 'alice', password: 'secret' }, 'login');
|
|
1049
|
+
|
|
1050
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
1051
|
+
const output = consoleLogSpy.mock.calls[0][0];
|
|
1052
|
+
expect(output).toContain('[Redacted]');
|
|
1053
|
+
expect(output).toContain('alice');
|
|
1054
|
+
expect(output).not.toContain('secret');
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it('should redact binding fields from child loggers', () => {
|
|
1058
|
+
const logger = createEdgeLogger('test-service', {
|
|
1059
|
+
bindings: { secret: 'top-level-secret' },
|
|
1060
|
+
redact: { paths: ['secret'] },
|
|
1061
|
+
});
|
|
1062
|
+
logger.info('test');
|
|
1063
|
+
|
|
1064
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1065
|
+
expect(logOutput.secret).toBe('[Redacted]');
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it('should handle wildcard paths', () => {
|
|
1069
|
+
const logger = createEdgeLogger('test-service', {
|
|
1070
|
+
redact: { paths: ['users[*].password'] },
|
|
1071
|
+
});
|
|
1072
|
+
logger.info(
|
|
1073
|
+
{
|
|
1074
|
+
users: [
|
|
1075
|
+
{ name: 'A', password: 'p1' },
|
|
1076
|
+
{ name: 'B', password: 'p2' },
|
|
1077
|
+
],
|
|
1078
|
+
},
|
|
1079
|
+
'batch',
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1083
|
+
expect(logOutput.users[0].password).toBe('[Redacted]');
|
|
1084
|
+
expect(logOutput.users[1].password).toBe('[Redacted]');
|
|
1085
|
+
expect(logOutput.users[0].name).toBe('A');
|
|
1086
|
+
expect(logOutput.users[1].name).toBe('B');
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
it('should not redact when redact option is not set', () => {
|
|
1090
|
+
const logger = createEdgeLogger('test-service');
|
|
1091
|
+
logger.info({ password: 'secret' }, 'test');
|
|
1092
|
+
|
|
1093
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1094
|
+
expect(logOutput.password).toBe('secret');
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it('should accept preset name as redact option', () => {
|
|
1098
|
+
const logger = createEdgeLogger('test-service', {
|
|
1099
|
+
redact: 'default',
|
|
1100
|
+
});
|
|
1101
|
+
logger.info(
|
|
1102
|
+
{
|
|
1103
|
+
password: 'hunter2',
|
|
1104
|
+
token: 'jwt-abc',
|
|
1105
|
+
safe: 'visible',
|
|
1106
|
+
},
|
|
1107
|
+
'login',
|
|
1108
|
+
);
|
|
1109
|
+
|
|
1110
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1111
|
+
expect(logOutput.password).toBe('[Redacted]');
|
|
1112
|
+
expect(logOutput.token).toBe('[Redacted]');
|
|
1113
|
+
expect(logOutput.safe).toBe('visible');
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
it('should accept string[] as redact shorthand', () => {
|
|
1117
|
+
const logger = createEdgeLogger('test-service', {
|
|
1118
|
+
redact: ['password', 'secret'],
|
|
1119
|
+
});
|
|
1120
|
+
logger.info({ password: 'hunter2', secret: 'abc', safe: 'visible' }, 'test');
|
|
1121
|
+
|
|
1122
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1123
|
+
expect(logOutput.password).toBe('[Redacted]');
|
|
1124
|
+
expect(logOutput.secret).toBe('[Redacted]');
|
|
1125
|
+
expect(logOutput.safe).toBe('visible');
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it('should redact transmitted payloads', () => {
|
|
1129
|
+
const send = vi.fn();
|
|
1130
|
+
const logger = createEdgeLogger('test-service', {
|
|
1131
|
+
redact: { paths: ['password'] },
|
|
1132
|
+
transmit: { send },
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
logger.info({ user: 'alice', password: 'secret' }, 'login');
|
|
1136
|
+
|
|
1137
|
+
expect(send).toHaveBeenCalledOnce();
|
|
1138
|
+
const [, event] = send.mock.calls[0];
|
|
1139
|
+
expect(event).not.toEqual(
|
|
1140
|
+
expect.objectContaining({
|
|
1141
|
+
messages: expect.arrayContaining([
|
|
1142
|
+
expect.objectContaining({ password: 'secret' }),
|
|
1143
|
+
]),
|
|
1144
|
+
}),
|
|
1145
|
+
);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it('should transmit the original logger arguments without duplicating the message', () => {
|
|
1149
|
+
const send = vi.fn();
|
|
1150
|
+
const logger = createEdgeLogger('test-service', {
|
|
1151
|
+
transmit: { send },
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
logger.info({ user: 'alice' }, 'login');
|
|
1155
|
+
|
|
1156
|
+
expect(send).toHaveBeenCalledOnce();
|
|
1157
|
+
const [, event] = send.mock.calls[0];
|
|
1158
|
+
expect(event.messages).toEqual([{ user: 'alice' }, 'login']);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
it('should transmit no bindings for a root logger with no child bindings', () => {
|
|
1162
|
+
const send = vi.fn();
|
|
1163
|
+
const logger = createEdgeLogger('test-service', {
|
|
1164
|
+
transmit: { send },
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
logger.info('login');
|
|
1168
|
+
|
|
1169
|
+
expect(send).toHaveBeenCalledOnce();
|
|
1170
|
+
const [, event] = send.mock.calls[0];
|
|
1171
|
+
expect(event.bindings).toEqual([]);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
it('should transmit child bindings as a hierarchy instead of one merged object', () => {
|
|
1175
|
+
const send = vi.fn();
|
|
1176
|
+
const logger = createEdgeLogger('test-service', {
|
|
1177
|
+
transmit: { send },
|
|
1178
|
+
});
|
|
1179
|
+
const child = logger.child({ requestId: 'req-1' });
|
|
1180
|
+
const grandchild = child.child({ userId: 'user-1' });
|
|
1181
|
+
|
|
1182
|
+
grandchild.info({ action: 'login' }, 'login');
|
|
1183
|
+
|
|
1184
|
+
expect(send).toHaveBeenCalledOnce();
|
|
1185
|
+
const [, event] = send.mock.calls[0];
|
|
1186
|
+
expect(event.bindings).toEqual([
|
|
1187
|
+
{ requestId: 'req-1' },
|
|
1188
|
+
{ userId: 'user-1' },
|
|
1189
|
+
]);
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
it('should apply serializers to transmitted messages and bindings like pino browser logEvent', () => {
|
|
1193
|
+
const send = vi.fn();
|
|
1194
|
+
const logger = createEdgeLogger('test-service', {
|
|
1195
|
+
bindings: {
|
|
1196
|
+
user: { id: 'bound-user', email: 'bound@example.com' },
|
|
1197
|
+
},
|
|
1198
|
+
serializers: {
|
|
1199
|
+
user: (value) => ({ id: (value as { id: string }).id }),
|
|
1200
|
+
},
|
|
1201
|
+
transmit: { send },
|
|
1202
|
+
});
|
|
1203
|
+
const child = logger.child({
|
|
1204
|
+
user: { id: 'child-user', email: 'child@example.com' },
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
child.info(
|
|
1208
|
+
{ user: { id: 'msg-user', email: 'msg@example.com' } },
|
|
1209
|
+
'login',
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
expect(send).toHaveBeenCalledOnce();
|
|
1213
|
+
const [, event] = send.mock.calls[0];
|
|
1214
|
+
expect(event.messages).toEqual([
|
|
1215
|
+
{ user: { id: 'msg-user' } },
|
|
1216
|
+
'login',
|
|
1217
|
+
]);
|
|
1218
|
+
expect(event.bindings).toEqual([
|
|
1219
|
+
{ user: { id: 'child-user' } },
|
|
1220
|
+
]);
|
|
1221
|
+
});
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
describe('name option', () => {
|
|
1225
|
+
it('should include name in log output', () => {
|
|
1226
|
+
const logger = createEdgeLogger('test-service', { name: 'my-logger' });
|
|
1227
|
+
logger.info('hello');
|
|
1228
|
+
|
|
1229
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1230
|
+
expect(logOutput.name).toBe('my-logger');
|
|
1231
|
+
expect(logOutput.service).toBe('test-service');
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
it('should expose name on the logger instance', () => {
|
|
1235
|
+
const logger = createEdgeLogger('test-service', { name: 'my-logger' });
|
|
1236
|
+
expect(logger.name).toBe('my-logger');
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
it('should not include name field when not set', () => {
|
|
1240
|
+
const logger = createEdgeLogger('test-service');
|
|
1241
|
+
logger.info('hello');
|
|
1242
|
+
|
|
1243
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1244
|
+
expect(logOutput.name).toBeUndefined();
|
|
1245
|
+
expect(logger.name).toBeUndefined();
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
it('should propagate name to child loggers', () => {
|
|
1249
|
+
const logger = createEdgeLogger('test-service', { name: 'my-logger' });
|
|
1250
|
+
const child = logger.child({ requestId: 'req-1' });
|
|
1251
|
+
|
|
1252
|
+
child.info('child msg');
|
|
1253
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1254
|
+
expect(logOutput.name).toBe('my-logger');
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
describe('base option', () => {
|
|
1259
|
+
it('should merge base bindings into every log', () => {
|
|
1260
|
+
const logger = createEdgeLogger('test-service', {
|
|
1261
|
+
base: { pid: 123, hostname: 'edge-1' },
|
|
1262
|
+
});
|
|
1263
|
+
logger.info('hello');
|
|
1264
|
+
|
|
1265
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1266
|
+
expect(logOutput.pid).toBe(123);
|
|
1267
|
+
expect(logOutput.hostname).toBe('edge-1');
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
it('should disable base with null', () => {
|
|
1271
|
+
const logger = createEdgeLogger('test-service', { base: null });
|
|
1272
|
+
logger.info('hello');
|
|
1273
|
+
|
|
1274
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1275
|
+
expect(logOutput.pid).toBeUndefined();
|
|
1276
|
+
expect(logOutput.hostname).toBeUndefined();
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
it('should propagate base to child loggers', () => {
|
|
1280
|
+
const logger = createEdgeLogger('test-service', {
|
|
1281
|
+
base: { region: 'us-east-1' },
|
|
1282
|
+
});
|
|
1283
|
+
const child = logger.child({ requestId: 'req-1' });
|
|
1284
|
+
child.info('child msg');
|
|
1285
|
+
|
|
1286
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1287
|
+
expect(logOutput.region).toBe('us-east-1');
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
describe('timestamp option', () => {
|
|
1292
|
+
it('should include ISO timestamp by default', () => {
|
|
1293
|
+
const logger = createEdgeLogger('test-service');
|
|
1294
|
+
logger.info('hello');
|
|
1295
|
+
|
|
1296
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1297
|
+
expect(logOutput.timestamp).toBeDefined();
|
|
1298
|
+
expect(logOutput.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
it('should omit timestamp when false', () => {
|
|
1302
|
+
const logger = createEdgeLogger('test-service', { timestamp: false });
|
|
1303
|
+
logger.info('hello');
|
|
1304
|
+
|
|
1305
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1306
|
+
expect(logOutput.timestamp).toBeUndefined();
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
it('should use custom timestamp function', () => {
|
|
1310
|
+
const logger = createEdgeLogger('test-service', {
|
|
1311
|
+
timestamp: () => '2026-01-01T00:00:00.000Z',
|
|
1312
|
+
});
|
|
1313
|
+
logger.info('hello');
|
|
1314
|
+
|
|
1315
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1316
|
+
expect(logOutput.timestamp).toBe('2026-01-01T00:00:00.000Z');
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
it('should propagate timestamp option to children', () => {
|
|
1320
|
+
const logger = createEdgeLogger('test-service', { timestamp: false });
|
|
1321
|
+
const child = logger.child({ requestId: 'req-1' });
|
|
1322
|
+
child.info('child msg');
|
|
1323
|
+
|
|
1324
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1325
|
+
expect(logOutput.timestamp).toBeUndefined();
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
describe('safe option', () => {
|
|
1330
|
+
it('should handle circular references when safe is true (default)', () => {
|
|
1331
|
+
const logger = createEdgeLogger('test-service');
|
|
1332
|
+
const circular: any = { a: 1 };
|
|
1333
|
+
circular.self = circular;
|
|
1334
|
+
|
|
1335
|
+
expect(() => logger.info(circular, 'msg')).not.toThrow();
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it('should throw on circular references when safe is false', () => {
|
|
1339
|
+
const logger = createEdgeLogger('test-service', { safe: false });
|
|
1340
|
+
const circular: any = { a: 1 };
|
|
1341
|
+
circular.self = circular;
|
|
1342
|
+
|
|
1343
|
+
expect(() => logger.info(circular, 'msg')).toThrow();
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
it('should not corrupt shared (non-circular) object references', () => {
|
|
1347
|
+
const logger = createEdgeLogger('test-service');
|
|
1348
|
+
const shared = { x: 1 };
|
|
1349
|
+
const payload = { a: shared, b: shared };
|
|
1350
|
+
|
|
1351
|
+
logger.info(payload, 'shared refs');
|
|
1352
|
+
|
|
1353
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
1354
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1355
|
+
// Both a and b should serialize normally — shared is not circular
|
|
1356
|
+
expect(logOutput.a).toEqual({ x: 1 });
|
|
1357
|
+
expect(logOutput.b).toEqual({ x: 1 });
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
describe('crlf option', () => {
|
|
1362
|
+
it(String.raw`should append \r\n when crlf is true`, () => {
|
|
1363
|
+
const logger = createEdgeLogger('test-service', { crlf: true });
|
|
1364
|
+
logger.info('hello');
|
|
1365
|
+
|
|
1366
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
1367
|
+
const rawOutput = consoleLogSpy.mock.calls[0][0];
|
|
1368
|
+
expect(rawOutput.endsWith('\r\n')).toBe(true);
|
|
1369
|
+
// Should still be valid JSON before the \r\n
|
|
1370
|
+
const logOutput = JSON.parse(rawOutput.trimEnd());
|
|
1371
|
+
expect(logOutput.msg).toBe('hello');
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
it(String.raw`should not append \r\n by default`, () => {
|
|
1375
|
+
const logger = createEdgeLogger('test-service');
|
|
1376
|
+
logger.info('hello');
|
|
1377
|
+
|
|
1378
|
+
const rawOutput = consoleLogSpy.mock.calls[0][0];
|
|
1379
|
+
expect(rawOutput.endsWith('\r\n')).toBe(false);
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
describe('write option', () => {
|
|
1384
|
+
it('should call write function instead of console.log', () => {
|
|
1385
|
+
const writeSpy = vi.fn();
|
|
1386
|
+
const logger = createEdgeLogger('test-service', { write: writeSpy });
|
|
1387
|
+
logger.info('hello');
|
|
1388
|
+
|
|
1389
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
1390
|
+
expect(writeSpy).toHaveBeenCalledOnce();
|
|
1391
|
+
expect(writeSpy.mock.calls[0][0]).toMatchObject({
|
|
1392
|
+
msg: 'hello',
|
|
1393
|
+
service: 'test-service',
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
it('should dispatch to level-specific write functions', () => {
|
|
1398
|
+
const infoSpy = vi.fn();
|
|
1399
|
+
const errorSpy = vi.fn();
|
|
1400
|
+
const logger = createEdgeLogger('test-service', {
|
|
1401
|
+
write: { info: infoSpy, error: errorSpy },
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
logger.info('info msg');
|
|
1405
|
+
logger.error('error msg');
|
|
1406
|
+
|
|
1407
|
+
expect(infoSpy).toHaveBeenCalledOnce();
|
|
1408
|
+
expect(errorSpy).toHaveBeenCalledOnce();
|
|
1409
|
+
expect(infoSpy.mock.calls[0][0].msg).toBe('info msg');
|
|
1410
|
+
expect(errorSpy.mock.calls[0][0].msg).toBe('error msg');
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
it('should fall back to console.log for unmatched levels', () => {
|
|
1414
|
+
const infoSpy = vi.fn();
|
|
1415
|
+
const logger = createEdgeLogger('test-service', {
|
|
1416
|
+
write: { info: infoSpy },
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
logger.warn('warn msg');
|
|
1420
|
+
|
|
1421
|
+
// Falls back to console.log when no matching level writer
|
|
1422
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
1423
|
+
expect(infoSpy).not.toHaveBeenCalled();
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
it('should propagate write to child loggers', () => {
|
|
1427
|
+
const writeSpy = vi.fn();
|
|
1428
|
+
const logger = createEdgeLogger('test-service', { write: writeSpy });
|
|
1429
|
+
const child = logger.child({ requestId: 'req-1' });
|
|
1430
|
+
|
|
1431
|
+
child.info('child msg');
|
|
1432
|
+
|
|
1433
|
+
expect(consoleLogSpy).not.toHaveBeenCalled();
|
|
1434
|
+
expect(writeSpy).toHaveBeenCalledOnce();
|
|
1435
|
+
expect(writeSpy.mock.calls[0][0].requestId).toBe('req-1');
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
describe('transmit option', () => {
|
|
1440
|
+
it('should call transmit.send after logging', () => {
|
|
1441
|
+
const sendSpy = vi.fn();
|
|
1442
|
+
const logger = createEdgeLogger('test-service', {
|
|
1443
|
+
transmit: { send: sendSpy },
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
logger.info({ userId: '123' }, 'hello');
|
|
1447
|
+
|
|
1448
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
1449
|
+
expect(sendSpy).toHaveBeenCalledOnce();
|
|
1450
|
+
|
|
1451
|
+
const [level, logEvent] = sendSpy.mock.calls[0];
|
|
1452
|
+
expect(level).toBe('info');
|
|
1453
|
+
expect(logEvent.ts).toBeTypeOf('number');
|
|
1454
|
+
expect(logEvent.level.label).toBe('info');
|
|
1455
|
+
expect(logEvent.level.value).toBe(30);
|
|
1456
|
+
expect(logEvent.bindings).toEqual([]);
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
it('should respect transmit.level threshold', () => {
|
|
1460
|
+
const sendSpy = vi.fn();
|
|
1461
|
+
const logger = createEdgeLogger('test-service', {
|
|
1462
|
+
transmit: { level: 'error', send: sendSpy },
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
logger.info('info msg');
|
|
1466
|
+
expect(sendSpy).not.toHaveBeenCalled();
|
|
1467
|
+
|
|
1468
|
+
logger.error('error msg');
|
|
1469
|
+
expect(sendSpy).toHaveBeenCalledOnce();
|
|
1470
|
+
expect(sendSpy.mock.calls[0][0]).toBe('error');
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
it('should propagate transmit to child loggers', () => {
|
|
1474
|
+
const sendSpy = vi.fn();
|
|
1475
|
+
const logger = createEdgeLogger('test-service', {
|
|
1476
|
+
transmit: { send: sendSpy },
|
|
1477
|
+
});
|
|
1478
|
+
const child = logger.child({ requestId: 'req-1' });
|
|
1479
|
+
|
|
1480
|
+
child.info('child msg');
|
|
1481
|
+
|
|
1482
|
+
expect(sendSpy).toHaveBeenCalledOnce();
|
|
1483
|
+
expect(sendSpy.mock.calls[0][1].bindings).toEqual([{ requestId: 'req-1' }]);
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
it('should redact secrets in transmit payload', () => {
|
|
1487
|
+
const sendSpy = vi.fn();
|
|
1488
|
+
const logger = createEdgeLogger('test-service', {
|
|
1489
|
+
redact: { paths: ['password'] },
|
|
1490
|
+
transmit: { send: sendSpy },
|
|
1491
|
+
});
|
|
1492
|
+
const child = logger.child({ password: 'bound-secret', user: 'alice' });
|
|
1493
|
+
|
|
1494
|
+
child.info({ password: 'secret', safe: 'visible' }, 'login');
|
|
1495
|
+
|
|
1496
|
+
expect(sendSpy).toHaveBeenCalledOnce();
|
|
1497
|
+
const logEvent = sendSpy.mock.calls[0][1];
|
|
1498
|
+
// Child bindings should have password redacted
|
|
1499
|
+
expect(logEvent.bindings[0].password).toBe('[Redacted]');
|
|
1500
|
+
expect(logEvent.bindings[0].user).toBe('alice');
|
|
1501
|
+
});
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
describe('hooks option', () => {
|
|
1505
|
+
it('should call logMethod hook before logging', () => {
|
|
1506
|
+
const logger = createEdgeLogger('test-service', {
|
|
1507
|
+
hooks: {
|
|
1508
|
+
logMethod(args, method, _level) {
|
|
1509
|
+
// Prepend a field to every log call
|
|
1510
|
+
const [first, ...rest] = args;
|
|
1511
|
+
if (typeof first === 'object' && first !== null) {
|
|
1512
|
+
method({ ...first, hooked: true }, ...rest);
|
|
1513
|
+
} else {
|
|
1514
|
+
method({ hooked: true }, ...args);
|
|
1515
|
+
}
|
|
1516
|
+
},
|
|
1517
|
+
},
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
logger.info('hello');
|
|
1521
|
+
|
|
1522
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
1523
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1524
|
+
expect(logOutput.hooked).toBe(true);
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
it('should propagate hooks to child loggers', () => {
|
|
1528
|
+
const hookSpy = vi.fn((args, method, _level) => {
|
|
1529
|
+
method(...args);
|
|
1530
|
+
});
|
|
1531
|
+
const logger = createEdgeLogger('test-service', {
|
|
1532
|
+
hooks: { logMethod: hookSpy },
|
|
1533
|
+
});
|
|
1534
|
+
const child = logger.child({ requestId: 'req-1' });
|
|
1535
|
+
|
|
1536
|
+
child.info('child msg');
|
|
1537
|
+
|
|
1538
|
+
expect(hookSpy).toHaveBeenCalledOnce();
|
|
1539
|
+
});
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
describe('edgeLimit option', () => {
|
|
1543
|
+
it('should truncate objects with too many keys in safe stringify', () => {
|
|
1544
|
+
const writeSpy = vi.fn();
|
|
1545
|
+
const logger = createEdgeLogger('test-service', {
|
|
1546
|
+
edgeLimit: 5,
|
|
1547
|
+
write: writeSpy,
|
|
1548
|
+
});
|
|
1549
|
+
const nested: Record<string, number> = {};
|
|
1550
|
+
for (let i = 0; i < 10; i++) {
|
|
1551
|
+
nested[`key${i}`] = i;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
// write fn receives the raw object, which gets stringified by the
|
|
1555
|
+
// outer log path. To test edgeLimit directly, use safeStringify via console.log path.
|
|
1556
|
+
const logger2 = createEdgeLogger('test-service', {
|
|
1557
|
+
edgeLimit: 5,
|
|
1558
|
+
nestedKey: 'payload',
|
|
1559
|
+
});
|
|
1560
|
+
logger2.info(nested, 'many keys');
|
|
1561
|
+
|
|
1562
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
1563
|
+
const raw = consoleLogSpy.mock.calls[0][0];
|
|
1564
|
+
const logOutput = JSON.parse(raw);
|
|
1565
|
+
// payload has 10 keys, edgeLimit=5 means first 5 + '...'
|
|
1566
|
+
expect(logOutput.payload['...']).toContain('more properties');
|
|
1567
|
+
});
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
describe('depthLimit option', () => {
|
|
1571
|
+
it('should truncate deeply nested objects', () => {
|
|
1572
|
+
const logger = createEdgeLogger('test-service', { depthLimit: 3 });
|
|
1573
|
+
const deep = {
|
|
1574
|
+
l1: { l2: { l3: { l4: { l5: 'very deep' } } } },
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
logger.info({ nested: deep }, 'deep nesting');
|
|
1578
|
+
|
|
1579
|
+
expect(consoleLogSpy).toHaveBeenCalledOnce();
|
|
1580
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1581
|
+
// Objects beyond depthLimit become '[Object]'
|
|
1582
|
+
// Find the first [Object] to confirm truncation happens
|
|
1583
|
+
const walk = (obj: any, depth = 0): number => {
|
|
1584
|
+
if (obj === '[Object]') return depth;
|
|
1585
|
+
if (typeof obj !== 'object' || obj === null) return -1;
|
|
1586
|
+
for (const v of Object.values(obj)) {
|
|
1587
|
+
const d = walk(v, depth + 1);
|
|
1588
|
+
if (d > 0) return d;
|
|
1589
|
+
}
|
|
1590
|
+
return -1;
|
|
1591
|
+
};
|
|
1592
|
+
const truncatedAt = walk(logOutput);
|
|
1593
|
+
expect(truncatedAt).toBeGreaterThan(0);
|
|
1594
|
+
expect(truncatedAt).toBeLessThanOrEqual(5); // truncation happens within depth limit range
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
it('should allow full depth by default (depthLimit: 5)', () => {
|
|
1598
|
+
const logger = createEdgeLogger('test-service');
|
|
1599
|
+
const deep = { l1: { l2: { l3: { l4: 'deep' } } } };
|
|
1600
|
+
|
|
1601
|
+
logger.info({ data: deep }, 'deep nesting');
|
|
1602
|
+
|
|
1603
|
+
const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
|
|
1604
|
+
expect(logOutput.data.l1.l2.l3.l4).toBe('deep');
|
|
365
1605
|
});
|
|
366
1606
|
});
|
|
367
1607
|
});
|