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