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.
@@ -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('test message', { key: 'value' });
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: 'info',
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', error);
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: 'error',
145
+ level: 50,
61
146
  service: 'test-service',
62
147
  msg: 'error occurred',
63
- error: 'test error',
64
148
  });
65
- expect(logOutput.stack).toBeDefined();
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('warning message', { reason: 'test' });
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: 'warn',
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('debug message', { detail: 'verbose' });
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: 'debug',
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('debug message', { detail: 'verbose' });
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.spyOn(trace, 'getActiveSpan').mockReturnValue(mockSpan as any);
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: 'info',
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', errorObject);
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: 'error',
307
+ level: 50,
155
308
  service: 'test-service',
156
309
  msg: 'error occurred',
157
- error: '[object Object]',
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', null as any);
164
- logger.info('message with undefined', undefined as any);
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('message', { a: 1, b: 2, c: 3 });
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: 'info',
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(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
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('pretty message', { key: 'value' });
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', error);
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 throw on circular references in context', () => {
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
- // JSON.stringify will throw on circular references
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('circular', circular);
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
- // Normal behavior: debug filtered out
272
- logger.debug('outside context');
273
- expect(consoleLogSpy).not.toHaveBeenCalled();
612
+ it('should not treat repeated shared references as circular', () => {
613
+ const logger = createEdgeLogger('test-service');
614
+ const shared = { nested: true };
274
615
 
275
- // Inside debug context: debug logged
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
- it('should support none level to temporarily disable logging', () => {
287
- const logger = createEdgeLogger('test-service', { level: 'info' });
288
-
289
- logger.info('before none');
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
- runWithLogLevel('none', () => {
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
- logger.info('after none');
299
- expect(consoleLogSpy.mock.calls.length).toBe(callsBeforeNone + 1);
300
- });
632
+ expect(consoleLogSpy).toHaveBeenCalledOnce();
633
+ const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
301
634
 
302
- it('should return value from callback', () => {
303
- const result = runWithLogLevel('debug', () => {
304
- return 42;
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 propagate async results', async () => {
310
- const result = await runWithLogLevel('debug', async () => {
311
- return 'async value';
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
- it('should allow raising log level temporarily', () => {
317
- const logger = createEdgeLogger('test-service', { level: 'error' });
649
+ grandchild.info('deep message');
318
650
 
319
- logger.info('normal info');
320
- logger.warn('normal warn');
321
- expect(consoleLogSpy).not.toHaveBeenCalled();
651
+ expect(consoleLogSpy).toHaveBeenCalledOnce();
652
+ const logOutput = JSON.parse(consoleLogSpy.mock.calls[0][0]);
322
653
 
323
- runWithLogLevel('info', () => {
324
- logger.info('temporary info');
325
- logger.warn('temporary warn');
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 isolate log level changes to context', () => {
331
- const logger = createEdgeLogger('test-service', { level: 'info' });
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
- runWithLogLevel('debug', () => {
334
- logger.debug('context 1 - debug');
335
- expect(consoleLogSpy).toHaveBeenCalledOnce();
336
- });
665
+ child.info({ env: 'production' }, 'override');
337
666
 
338
- runWithLogLevel('error', () => {
339
- logger.info('context 2 - info should be filtered');
340
- expect(consoleLogSpy).toHaveBeenCalledOnce(); // No new logs
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
- describe('getActiveLogLevel', () => {
346
- it('should return undefined when no active level', () => {
347
- expect(getActiveLogLevel()).toBeUndefined();
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 return active level inside runWithLogLevel', () => {
351
- runWithLogLevel('debug', () => {
352
- expect(getActiveLogLevel()).toBe('debug');
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
- runWithLogLevel('error', () => {
356
- expect(getActiveLogLevel()).toBe('error');
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 reset after runWithLogLevel completes', () => {
361
- runWithLogLevel('debug', () => {
362
- expect(getActiveLogLevel()).toBe('debug');
363
- });
364
- expect(getActiveLogLevel()).toBeUndefined();
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
  });