mohen 1.0.0
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/.github/workflows/ci.yml +40 -0
- package/.github/workflows/publish.yml +38 -0
- package/README.md +198 -0
- package/dist/logger.d.ts +72 -0
- package/dist/logger.js +389 -0
- package/example/usage.ts +161 -0
- package/logo.png +0 -0
- package/package.json +40 -0
- package/src/logger.ts +454 -0
- package/test/logger.test.ts +499 -0
- package/test/test-server.ts +52 -0
- package/tsconfig.json +29 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import express, { Express } from 'express';
|
|
3
|
+
import request from 'supertest';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { createLogger, attachMetadata, attachTrpcMetadata } from '../src/logger';
|
|
7
|
+
import { initTRPC } from '@trpc/server';
|
|
8
|
+
import * as trpcExpress from '@trpc/server/adapters/express';
|
|
9
|
+
|
|
10
|
+
const TEST_LOG_FILE = path.join(__dirname, 'test.log');
|
|
11
|
+
|
|
12
|
+
// Mock date for deterministic timestamp
|
|
13
|
+
const MOCK_DATE = new Date('2024-01-15T10:30:00.000Z');
|
|
14
|
+
|
|
15
|
+
// Helper to read log entries
|
|
16
|
+
function readLogEntries(): any[] {
|
|
17
|
+
if (!fs.existsSync(TEST_LOG_FILE)) return [];
|
|
18
|
+
const content = fs.readFileSync(TEST_LOG_FILE, 'utf-8').trim();
|
|
19
|
+
if (!content) return [];
|
|
20
|
+
return content.split('\n').map((line) => JSON.parse(line));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper to clear log file
|
|
24
|
+
function clearLogFile(): void {
|
|
25
|
+
if (fs.existsSync(TEST_LOG_FILE)) {
|
|
26
|
+
fs.unlinkSync(TEST_LOG_FILE);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('mohen logger', () => {
|
|
31
|
+
let app: Express;
|
|
32
|
+
let logger: ReturnType<typeof createLogger>;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
clearLogFile();
|
|
36
|
+
|
|
37
|
+
// Mock Date for deterministic timestamp
|
|
38
|
+
vi.useFakeTimers();
|
|
39
|
+
vi.setSystemTime(MOCK_DATE);
|
|
40
|
+
|
|
41
|
+
logger = createLogger(TEST_LOG_FILE, {
|
|
42
|
+
redact: ['password', 'token', 'secret'],
|
|
43
|
+
});
|
|
44
|
+
app = express();
|
|
45
|
+
app.use(express.json());
|
|
46
|
+
app.use(logger.express());
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
vi.useRealTimers();
|
|
51
|
+
vi.restoreAllMocks();
|
|
52
|
+
clearLogFile();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('Express - Normal Requests', () => {
|
|
56
|
+
it('should log GET requests with correct fields', async () => {
|
|
57
|
+
app.get('/api/test', (req, res) => {
|
|
58
|
+
res.json({ message: 'hello', value: 42 });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await request(app).get('/api/test').expect(200);
|
|
62
|
+
|
|
63
|
+
const entries = readLogEntries();
|
|
64
|
+
expect(entries).toHaveLength(1);
|
|
65
|
+
expect(entries[0]).toMatchObject({
|
|
66
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
67
|
+
type: 'http',
|
|
68
|
+
method: 'GET',
|
|
69
|
+
path: '/api/test',
|
|
70
|
+
statusCode: 200,
|
|
71
|
+
duration: 0,
|
|
72
|
+
request: {
|
|
73
|
+
body: {},
|
|
74
|
+
query: {},
|
|
75
|
+
},
|
|
76
|
+
response: {
|
|
77
|
+
body: { message: 'hello', value: 42 },
|
|
78
|
+
streaming: false,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
expect(entries[0].requestId).toMatch(/^[a-z0-9]+-[a-z0-9]+$/);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should log POST requests with request body', async () => {
|
|
85
|
+
app.post('/api/users', (req, res) => {
|
|
86
|
+
res.json({ id: 1, name: req.body.name });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await request(app)
|
|
90
|
+
.post('/api/users')
|
|
91
|
+
.send({ name: 'John', email: 'john@example.com' })
|
|
92
|
+
.expect(200);
|
|
93
|
+
|
|
94
|
+
const entries = readLogEntries();
|
|
95
|
+
expect(entries).toHaveLength(1);
|
|
96
|
+
expect(entries[0]).toMatchObject({
|
|
97
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
98
|
+
type: 'http',
|
|
99
|
+
method: 'POST',
|
|
100
|
+
path: '/api/users',
|
|
101
|
+
statusCode: 200,
|
|
102
|
+
duration: 0,
|
|
103
|
+
request: {
|
|
104
|
+
body: { name: 'John', email: 'john@example.com' },
|
|
105
|
+
query: {},
|
|
106
|
+
},
|
|
107
|
+
response: {
|
|
108
|
+
body: { id: 1, name: 'John' },
|
|
109
|
+
streaming: false,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should redact sensitive fields', async () => {
|
|
115
|
+
app.post('/api/login', (req, res) => {
|
|
116
|
+
res.json({ success: true, token: 'abc123' });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await request(app)
|
|
120
|
+
.post('/api/login')
|
|
121
|
+
.send({ username: 'john', password: 'secret123' })
|
|
122
|
+
.expect(200);
|
|
123
|
+
|
|
124
|
+
const entries = readLogEntries();
|
|
125
|
+
expect(entries).toHaveLength(1);
|
|
126
|
+
expect(entries[0]).toMatchObject({
|
|
127
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
128
|
+
type: 'http',
|
|
129
|
+
method: 'POST',
|
|
130
|
+
path: '/api/login',
|
|
131
|
+
statusCode: 200,
|
|
132
|
+
request: {
|
|
133
|
+
body: { username: 'john', password: '[REDACTED]' },
|
|
134
|
+
},
|
|
135
|
+
response: {
|
|
136
|
+
body: { success: true, token: '[REDACTED]' },
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should log query parameters', async () => {
|
|
142
|
+
app.get('/api/search', (req, res) => {
|
|
143
|
+
res.json({ query: req.query.q });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await request(app).get('/api/search?q=hello&limit=10').expect(200);
|
|
147
|
+
|
|
148
|
+
const entries = readLogEntries();
|
|
149
|
+
expect(entries).toHaveLength(1);
|
|
150
|
+
expect(entries[0]).toMatchObject({
|
|
151
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
152
|
+
type: 'http',
|
|
153
|
+
method: 'GET',
|
|
154
|
+
path: '/api/search?q=hello&limit=10',
|
|
155
|
+
statusCode: 200,
|
|
156
|
+
request: {
|
|
157
|
+
query: { q: 'hello', limit: '10' },
|
|
158
|
+
},
|
|
159
|
+
response: {
|
|
160
|
+
body: { query: 'hello' },
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('Express - SSE Streaming', () => {
|
|
167
|
+
it('should aggregate SSE chunks into single log entry', async () => {
|
|
168
|
+
app.get('/api/stream', (req, res) => {
|
|
169
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
170
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
171
|
+
res.setHeader('Connection', 'keep-alive');
|
|
172
|
+
|
|
173
|
+
res.write(`data: ${JSON.stringify({ count: 1 })}\n\n`);
|
|
174
|
+
res.write(`data: ${JSON.stringify({ count: 2 })}\n\n`);
|
|
175
|
+
res.write(`data: ${JSON.stringify({ count: 3 })}\n\n`);
|
|
176
|
+
res.write('data: [DONE]\n\n');
|
|
177
|
+
res.end();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await request(app)
|
|
181
|
+
.get('/api/stream')
|
|
182
|
+
.set('Accept', 'text/event-stream')
|
|
183
|
+
.expect(200);
|
|
184
|
+
|
|
185
|
+
const entries = readLogEntries();
|
|
186
|
+
expect(entries).toHaveLength(1);
|
|
187
|
+
expect(entries[0]).toMatchObject({
|
|
188
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
189
|
+
type: 'http',
|
|
190
|
+
method: 'GET',
|
|
191
|
+
path: '/api/stream',
|
|
192
|
+
statusCode: 200,
|
|
193
|
+
response: {
|
|
194
|
+
streaming: true,
|
|
195
|
+
chunks: [
|
|
196
|
+
{ count: 1 },
|
|
197
|
+
{ count: 2 },
|
|
198
|
+
{ count: 3 },
|
|
199
|
+
{ done: true },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should handle SSE with metadata', async () => {
|
|
206
|
+
app.get('/api/stream-with-meta', (req, res) => {
|
|
207
|
+
attachMetadata(req, { streamId: 'abc123', userId: '42' });
|
|
208
|
+
|
|
209
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
210
|
+
res.write(`data: ${JSON.stringify({ msg: 'hello' })}\n\n`);
|
|
211
|
+
res.end();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await request(app)
|
|
215
|
+
.get('/api/stream-with-meta')
|
|
216
|
+
.set('Accept', 'text/event-stream')
|
|
217
|
+
.expect(200);
|
|
218
|
+
|
|
219
|
+
const entries = readLogEntries();
|
|
220
|
+
expect(entries).toHaveLength(1);
|
|
221
|
+
expect(entries[0]).toMatchObject({
|
|
222
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
223
|
+
type: 'http',
|
|
224
|
+
method: 'GET',
|
|
225
|
+
path: '/api/stream-with-meta',
|
|
226
|
+
statusCode: 200,
|
|
227
|
+
response: {
|
|
228
|
+
streaming: true,
|
|
229
|
+
chunks: [{ msg: 'hello' }],
|
|
230
|
+
},
|
|
231
|
+
metadata: { streamId: 'abc123', userId: '42' },
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('Express - attachMetadata', () => {
|
|
237
|
+
it('should attach custom metadata to log entry', async () => {
|
|
238
|
+
app.get('/api/users/:id', (req, res) => {
|
|
239
|
+
attachMetadata(req, {
|
|
240
|
+
userId: req.params.id,
|
|
241
|
+
source: 'user-service',
|
|
242
|
+
cacheHit: false,
|
|
243
|
+
});
|
|
244
|
+
res.json({ id: req.params.id, name: 'John' });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await request(app).get('/api/users/123').expect(200);
|
|
248
|
+
|
|
249
|
+
const entries = readLogEntries();
|
|
250
|
+
expect(entries).toHaveLength(1);
|
|
251
|
+
expect(entries[0]).toMatchObject({
|
|
252
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
253
|
+
type: 'http',
|
|
254
|
+
method: 'GET',
|
|
255
|
+
path: '/api/users/123',
|
|
256
|
+
statusCode: 200,
|
|
257
|
+
response: {
|
|
258
|
+
body: { id: '123', name: 'John' },
|
|
259
|
+
streaming: false,
|
|
260
|
+
},
|
|
261
|
+
metadata: {
|
|
262
|
+
userId: '123',
|
|
263
|
+
source: 'user-service',
|
|
264
|
+
cacheHit: false,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should merge multiple metadata calls', async () => {
|
|
270
|
+
app.get('/api/order', (req, res) => {
|
|
271
|
+
attachMetadata(req, { orderId: 'order-1' });
|
|
272
|
+
attachMetadata(req, { region: 'us-east-1' });
|
|
273
|
+
attachMetadata(req, { priority: true });
|
|
274
|
+
res.json({ success: true });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await request(app).get('/api/order').expect(200);
|
|
278
|
+
|
|
279
|
+
const entries = readLogEntries();
|
|
280
|
+
expect(entries).toHaveLength(1);
|
|
281
|
+
expect(entries[0]).toMatchObject({
|
|
282
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
283
|
+
type: 'http',
|
|
284
|
+
method: 'GET',
|
|
285
|
+
path: '/api/order',
|
|
286
|
+
statusCode: 200,
|
|
287
|
+
response: {
|
|
288
|
+
body: { success: true },
|
|
289
|
+
},
|
|
290
|
+
metadata: {
|
|
291
|
+
orderId: 'order-1',
|
|
292
|
+
region: 'us-east-1',
|
|
293
|
+
priority: true,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should not include metadata field if none attached', async () => {
|
|
299
|
+
app.get('/api/simple', (req, res) => {
|
|
300
|
+
res.json({ ok: true });
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
await request(app).get('/api/simple').expect(200);
|
|
304
|
+
|
|
305
|
+
const entries = readLogEntries();
|
|
306
|
+
expect(entries).toHaveLength(1);
|
|
307
|
+
expect(entries[0].metadata).toBeUndefined();
|
|
308
|
+
expect(entries[0]).toMatchObject({
|
|
309
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
310
|
+
type: 'http',
|
|
311
|
+
method: 'GET',
|
|
312
|
+
path: '/api/simple',
|
|
313
|
+
statusCode: 200,
|
|
314
|
+
response: {
|
|
315
|
+
body: { ok: true },
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('tRPC - Normal Procedures', () => {
|
|
322
|
+
it('should log tRPC query', async () => {
|
|
323
|
+
interface Context {
|
|
324
|
+
logMetadata?: Record<string, unknown>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const t = initTRPC.context<Context>().create();
|
|
328
|
+
const loggedProcedure = t.procedure.use(logger.trpc<Context>());
|
|
329
|
+
|
|
330
|
+
const appRouter = t.router({
|
|
331
|
+
hello: loggedProcedure
|
|
332
|
+
.input((val: unknown) => val as { name: string })
|
|
333
|
+
.query(({ input }) => {
|
|
334
|
+
return { greeting: `Hello, ${input.name}!` };
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
app.use(
|
|
339
|
+
'/trpc',
|
|
340
|
+
trpcExpress.createExpressMiddleware({
|
|
341
|
+
router: appRouter,
|
|
342
|
+
createContext: (): Context => ({ logMetadata: {} }),
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
await request(app)
|
|
347
|
+
.get('/trpc/hello?input=%7B%22name%22%3A%22World%22%7D')
|
|
348
|
+
.expect(200);
|
|
349
|
+
|
|
350
|
+
const entries = readLogEntries();
|
|
351
|
+
// Find the tRPC entry (there will also be an Express entry)
|
|
352
|
+
const trpcEntry = entries.find((e) => e.type === 'trpc');
|
|
353
|
+
expect(trpcEntry).toBeDefined();
|
|
354
|
+
expect(trpcEntry).toMatchObject({
|
|
355
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
356
|
+
type: 'trpc',
|
|
357
|
+
method: 'QUERY',
|
|
358
|
+
path: 'hello',
|
|
359
|
+
statusCode: 200,
|
|
360
|
+
duration: 0,
|
|
361
|
+
response: {
|
|
362
|
+
body: { greeting: 'Hello, World!' },
|
|
363
|
+
streaming: false,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
expect(trpcEntry.requestId).toMatch(/^[a-z0-9]+-[a-z0-9]+$/);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should log tRPC mutation', async () => {
|
|
370
|
+
interface Context {
|
|
371
|
+
logMetadata?: Record<string, unknown>;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const t = initTRPC.context<Context>().create();
|
|
375
|
+
const loggedProcedure = t.procedure.use(logger.trpc<Context>());
|
|
376
|
+
|
|
377
|
+
const appRouter = t.router({
|
|
378
|
+
createUser: loggedProcedure
|
|
379
|
+
.input((val: unknown) => val as { email: string })
|
|
380
|
+
.mutation(({ input }) => {
|
|
381
|
+
return { id: 'user-1', email: input.email };
|
|
382
|
+
}),
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
app.use(
|
|
386
|
+
'/trpc',
|
|
387
|
+
trpcExpress.createExpressMiddleware({
|
|
388
|
+
router: appRouter,
|
|
389
|
+
createContext: (): Context => ({ logMetadata: {} }),
|
|
390
|
+
})
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
await request(app)
|
|
394
|
+
.post('/trpc/createUser')
|
|
395
|
+
.send({ email: 'test@example.com' })
|
|
396
|
+
.expect(200);
|
|
397
|
+
|
|
398
|
+
const entries = readLogEntries();
|
|
399
|
+
const trpcEntry = entries.find((e) => e.type === 'trpc');
|
|
400
|
+
expect(trpcEntry).toBeDefined();
|
|
401
|
+
expect(trpcEntry).toMatchObject({
|
|
402
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
403
|
+
type: 'trpc',
|
|
404
|
+
method: 'MUTATION',
|
|
405
|
+
path: 'createUser',
|
|
406
|
+
statusCode: 200,
|
|
407
|
+
response: {
|
|
408
|
+
body: { id: 'user-1', email: 'test@example.com' },
|
|
409
|
+
streaming: false,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('tRPC - attachTrpcMetadata', () => {
|
|
416
|
+
it('should attach custom metadata to tRPC log entry', async () => {
|
|
417
|
+
interface Context {
|
|
418
|
+
logMetadata?: Record<string, unknown>;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const t = initTRPC.context<Context>().create();
|
|
422
|
+
const loggedProcedure = t.procedure.use(logger.trpc<Context>());
|
|
423
|
+
|
|
424
|
+
const appRouter = t.router({
|
|
425
|
+
getUser: loggedProcedure
|
|
426
|
+
.input((val: unknown) => val as { id: string })
|
|
427
|
+
.query(({ input, ctx }) => {
|
|
428
|
+
attachTrpcMetadata(ctx, {
|
|
429
|
+
userId: input.id,
|
|
430
|
+
source: 'user-service',
|
|
431
|
+
cached: true,
|
|
432
|
+
});
|
|
433
|
+
return { id: input.id, name: 'John' };
|
|
434
|
+
}),
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
app.use(
|
|
438
|
+
'/trpc',
|
|
439
|
+
trpcExpress.createExpressMiddleware({
|
|
440
|
+
router: appRouter,
|
|
441
|
+
createContext: (): Context => ({ logMetadata: {} }),
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
await request(app)
|
|
446
|
+
.get('/trpc/getUser?input=%7B%22id%22%3A%22user-123%22%7D')
|
|
447
|
+
.expect(200);
|
|
448
|
+
|
|
449
|
+
const entries = readLogEntries();
|
|
450
|
+
const trpcEntry = entries.find((e) => e.type === 'trpc');
|
|
451
|
+
expect(trpcEntry).toBeDefined();
|
|
452
|
+
expect(trpcEntry).toMatchObject({
|
|
453
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
454
|
+
type: 'trpc',
|
|
455
|
+
method: 'QUERY',
|
|
456
|
+
path: 'getUser',
|
|
457
|
+
statusCode: 200,
|
|
458
|
+
response: {
|
|
459
|
+
body: { id: 'user-123', name: 'John' },
|
|
460
|
+
streaming: false,
|
|
461
|
+
},
|
|
462
|
+
metadata: {
|
|
463
|
+
userId: 'user-123',
|
|
464
|
+
source: 'user-service',
|
|
465
|
+
cached: true,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe('File Size Management', () => {
|
|
472
|
+
it('should truncate log file when exceeding max size', async () => {
|
|
473
|
+
vi.useRealTimers(); // Use real timers for this test
|
|
474
|
+
|
|
475
|
+
// Create logger with very small max size
|
|
476
|
+
clearLogFile();
|
|
477
|
+
const smallLogger = createLogger(TEST_LOG_FILE, {
|
|
478
|
+
maxSizeBytes: 500, // Very small for testing
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const smallApp = express();
|
|
482
|
+
smallApp.use(express.json());
|
|
483
|
+
smallApp.use(smallLogger.express());
|
|
484
|
+
smallApp.get('/api/test', (req, res) => {
|
|
485
|
+
res.json({ message: 'a'.repeat(100) });
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Make multiple requests to exceed the limit
|
|
489
|
+
for (let i = 0; i < 10; i++) {
|
|
490
|
+
await request(smallApp).get('/api/test').expect(200);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const entries = readLogEntries();
|
|
494
|
+
// Should have fewer entries due to truncation (keeping 25%)
|
|
495
|
+
expect(entries.length).toBeLessThan(10);
|
|
496
|
+
expect(entries.length).toBeGreaterThan(0);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { createLogger } from '../src/logger';
|
|
3
|
+
|
|
4
|
+
const logger = createLogger('./logs/test.log', {
|
|
5
|
+
maxSizeBytes: 1024 * 1024, // 1MB for testing
|
|
6
|
+
redact: ['password', 'secret'],
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const app = express();
|
|
10
|
+
app.use(express.json());
|
|
11
|
+
app.use(logger.express());
|
|
12
|
+
|
|
13
|
+
// Regular JSON endpoint
|
|
14
|
+
app.get('/api/test', (req, res) => {
|
|
15
|
+
res.json({ message: 'Hello World', timestamp: Date.now() });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// POST with body
|
|
19
|
+
app.post('/api/data', (req, res) => {
|
|
20
|
+
res.json({ received: req.body, success: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// SSE streaming endpoint
|
|
24
|
+
app.get('/api/stream', (req, res) => {
|
|
25
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
26
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
27
|
+
res.setHeader('Connection', 'keep-alive');
|
|
28
|
+
|
|
29
|
+
let count = 0;
|
|
30
|
+
const interval = setInterval(() => {
|
|
31
|
+
count++;
|
|
32
|
+
res.write(`data: ${JSON.stringify({ count, msg: `Event ${count}` })}\n\n`);
|
|
33
|
+
|
|
34
|
+
if (count >= 3) {
|
|
35
|
+
res.write('data: [DONE]\n\n');
|
|
36
|
+
clearInterval(interval);
|
|
37
|
+
res.end();
|
|
38
|
+
}
|
|
39
|
+
}, 50);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const PORT = 3456;
|
|
43
|
+
const server = app.listen(PORT, () => {
|
|
44
|
+
console.log(`Test server running on http://localhost:${PORT}`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Auto-shutdown after tests
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
console.log('Shutting down test server...');
|
|
50
|
+
server.close();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}, 5000);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noImplicitAny": true,
|
|
9
|
+
"strictNullChecks": true,
|
|
10
|
+
"noImplicitThis": true,
|
|
11
|
+
"alwaysStrict": true,
|
|
12
|
+
"noUnusedLocals": false,
|
|
13
|
+
"noUnusedParameters": false,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": false,
|
|
16
|
+
"inlineSourceMap": true,
|
|
17
|
+
"inlineSources": true,
|
|
18
|
+
"experimentalDecorators": true,
|
|
19
|
+
"strictPropertyInitialization": false,
|
|
20
|
+
"outDir": "./dist",
|
|
21
|
+
"rootDir": "./src",
|
|
22
|
+
"skipLibCheck": true,
|
|
23
|
+
"esModuleInterop": true,
|
|
24
|
+
"resolveJsonModule": true,
|
|
25
|
+
"moduleResolution": "node"
|
|
26
|
+
},
|
|
27
|
+
"include": ["src/**/*"],
|
|
28
|
+
"exclude": ["node_modules", "dist", "example"]
|
|
29
|
+
}
|
package/vitest.config.ts
ADDED