@icd-iot-aicf/nestjs-logger 4.0.8 → 4.0.10

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.
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Log Snapshot — v3 Baseline Capture Script
3
+ *
4
+ * ╔══════════════════════════════════════════════════════════════════╗
5
+ * ║ COPY THIS FILE INTO YOUR PROJECT'S test/ FOLDER BEFORE ║
6
+ * ║ UPGRADING TO v4, THEN RUN: ║
7
+ * ║ ║
8
+ * ║ npx jest --testPathPatterns=log-snapshot --updateSnapshot ║
9
+ * ║ ║
10
+ * ║ This saves a baseline snapshot of your v3 log format. ║
11
+ * ║ After upgrading to v4, run the same file from the v4 package ║
12
+ * ║ (test/log-snapshot.e2e-spec.ts) to compare what changed. ║
13
+ * ╚══════════════════════════════════════════════════════════════════╝
14
+ *
15
+ * REQUIREMENTS (add to your project if missing):
16
+ * npm install --save-dev @nestjs/testing supertest
17
+ *
18
+ * ADD THIS JEST CONFIG to your jest-e2e.json (or equivalent):
19
+ * "transformIgnorePatterns": ["node_modules/(?!(uuid)/)"]
20
+ */
21
+
22
+ import { Test, TestingModule } from '@nestjs/testing';
23
+ import {
24
+ Controller,
25
+ Get,
26
+ INestApplication,
27
+ MiddlewareConsumer,
28
+ Module,
29
+ NestModule,
30
+ } from '@nestjs/common';
31
+ import request from 'supertest';
32
+
33
+ // ── v3 imports (these names change in v4 — do NOT update before snapshotting) ──
34
+ import {
35
+ AppLogConfigModule,
36
+ LoggerModule,
37
+ HTTPLogsMiddleware, // renamed to HttpLogsMiddleware in v4
38
+ FORMAT_TYPE,
39
+ CustomLoggerService,
40
+ RecordName,
41
+ HttpAction,
42
+ DBAction,
43
+ LogicAction,
44
+ ExceptionAction,
45
+ } from '@icd-iot-aicf/nestjs-logger';
46
+
47
+ // ── Normalisation ──────────────────────────────────────────────────────────────
48
+
49
+ const DYNAMIC_FIELDS = new Set([
50
+ 'timestamp',
51
+ 'systemTimestamp',
52
+ 'reqTimestamp',
53
+ 'resTimestamp',
54
+ 'transactionId',
55
+ 'sessionId',
56
+ 'tid',
57
+ 'subSessionId',
58
+ 'serviceStartTime',
59
+ 'serviceEndTime',
60
+ 'serviceTime',
61
+ 'usageTime',
62
+ 'startTime',
63
+ 'endTime',
64
+ 'processTime',
65
+ 'instance',
66
+ 'containerId',
67
+ 'message',
68
+ ]);
69
+
70
+ function normalizeLog(log: any): any {
71
+ if (typeof log !== 'object' || log === null) return log;
72
+ if (Array.isArray(log)) return log.map(normalizeLog);
73
+
74
+ const result: Record<string, any> = {};
75
+ for (const [key, value] of Object.entries(log)) {
76
+ if (DYNAMIC_FIELDS.has(key)) {
77
+ result[key] = '[NORMALIZED]';
78
+ } else if (typeof value === 'string') {
79
+ result[key] = normalizeString(value);
80
+ } else {
81
+ result[key] = normalizeLog(value);
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+
87
+ function normalizeString(value: string): string {
88
+ try {
89
+ const parsed = JSON.parse(value);
90
+ if (typeof parsed === 'object' && parsed !== null) {
91
+ return JSON.stringify(normalizeLog(parsed));
92
+ }
93
+ } catch {
94
+ /* not JSON */
95
+ }
96
+
97
+ return value
98
+ .replace(/\b(\d{1,3}\.){3}\d{1,3}:\d+\b/g, '[HOST]')
99
+ .replace(/\blocalhost:\d+\b/g, '[HOST]')
100
+ .replace(
101
+ /[a-z0-9_-]+_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi,
102
+ '[AUTO-TXN]',
103
+ );
104
+ }
105
+
106
+ function captureSnapshot(calls: jest.MockContext<any, any[]>['calls']) {
107
+ return {
108
+ count: calls.length,
109
+ logs: calls.map(([arg]) => normalizeLog(arg)),
110
+ };
111
+ }
112
+
113
+ // ── Shared test controller ─────────────────────────────────────────────────────
114
+
115
+ @Controller('snapshot')
116
+ class SnapshotController {
117
+ @Get('success')
118
+ @RecordName('SuccessEndpoint')
119
+ success() {
120
+ return { resultCode: '20000', resultDescription: 'SUCCESS', httpCode: 200 };
121
+ }
122
+
123
+ @Get('not-found')
124
+ @RecordName('NotFoundEndpoint')
125
+ notFound() {
126
+ return {
127
+ resultCode: '40400',
128
+ resultDescription: 'DATA_NOT_FOUND',
129
+ httpCode: 404,
130
+ };
131
+ }
132
+
133
+ @Get('no-headers')
134
+ noHeaders() {
135
+ return { ok: true };
136
+ }
137
+ }
138
+
139
+ const waitForFinish = (ms = 80) => new Promise((r) => setTimeout(r, ms));
140
+
141
+ // ─────────────────────────────────────────────────────────────────────────────
142
+ // IMPORTANT: describe() and it() names below MUST match the v4 snapshot file
143
+ // exactly — Jest uses these names as snapshot keys for comparison.
144
+ // ─────────────────────────────────────────────────────────────────────────────
145
+
146
+ describe('Log Snapshot — AICF format', () => {
147
+ let app: INestApplication;
148
+ let logSpy: jest.SpyInstance;
149
+
150
+ beforeAll(async () => {
151
+ @Module({
152
+ imports: [
153
+ AppLogConfigModule.forRoot({
154
+ appName: 'snapshot-app',
155
+ componentName: 'snapshot-comp',
156
+ format: FORMAT_TYPE.AICF,
157
+ }),
158
+ LoggerModule,
159
+ ],
160
+ controllers: [SnapshotController],
161
+ })
162
+ class AicfSnapshotModule implements NestModule {
163
+ configure(consumer: MiddlewareConsumer) {
164
+ // v3 name — this is what changes in v4 (HttpLogsMiddleware)
165
+ consumer.apply(HTTPLogsMiddleware).forRoutes('*');
166
+ }
167
+ }
168
+
169
+ const moduleFixture: TestingModule = await Test.createTestingModule({
170
+ imports: [AicfSnapshotModule],
171
+ }).compile();
172
+
173
+ app = moduleFixture.createNestApplication();
174
+ await app.init();
175
+
176
+ logSpy = jest.spyOn(app.get(CustomLoggerService), 'log');
177
+ });
178
+
179
+ afterAll(async () => await app.close());
180
+ beforeEach(() => logSpy.mockClear());
181
+
182
+ it('GET /snapshot/success — log count + structure', async () => {
183
+ await request(app.getHttpServer())
184
+ .get('/snapshot/success')
185
+ .set('x-ais-orderref', 'SNAP-AICF-001');
186
+ await waitForFinish();
187
+ const snapshot = captureSnapshot(logSpy.mock.calls);
188
+ expect(snapshot.count).toMatchSnapshot('log count');
189
+ expect(snapshot.logs).toMatchSnapshot('log structure');
190
+ });
191
+
192
+ it('GET /snapshot/not-found — log count + structure', async () => {
193
+ await request(app.getHttpServer())
194
+ .get('/snapshot/not-found')
195
+ .set('x-ais-orderref', 'SNAP-AICF-002');
196
+ await waitForFinish();
197
+ const snapshot = captureSnapshot(logSpy.mock.calls);
198
+ expect(snapshot.count).toMatchSnapshot('log count');
199
+ expect(snapshot.logs).toMatchSnapshot('log structure');
200
+ });
201
+
202
+ it('GET /snapshot/no-headers — log count + structure (missing correlation headers)', async () => {
203
+ await request(app.getHttpServer()).get('/snapshot/no-headers');
204
+ await waitForFinish();
205
+ const snapshot = captureSnapshot(logSpy.mock.calls);
206
+ expect(snapshot.count).toMatchSnapshot('log count');
207
+ expect(snapshot.logs).toMatchSnapshot('log structure');
208
+ });
209
+
210
+ it('HttpAction.GET — DTO field structure', () => {
211
+ const dto = HttpAction.GET({
212
+ requestObject: {
213
+ url: '/api/users/1',
214
+ headers: { 'x-ais-orderref': 'SNAP-ACT-001' },
215
+ queryString: {},
216
+ routeParameters: {},
217
+ payload: {},
218
+ },
219
+ responseObject: { httpStatusCode: 200, headers: {}, payload: { id: 1 } },
220
+ });
221
+ expect(normalizeLog(dto)).toMatchSnapshot();
222
+ });
223
+
224
+ it('HttpAction.POST — DTO field structure', () => {
225
+ const dto = HttpAction.POST({
226
+ requestObject: {
227
+ url: '/api/users',
228
+ headers: {},
229
+ queryString: {},
230
+ routeParameters: {},
231
+ payload: { name: 'test-user' },
232
+ },
233
+ responseObject: { httpStatusCode: 201, headers: {}, payload: { id: 99 } },
234
+ });
235
+ expect(normalizeLog(dto)).toMatchSnapshot();
236
+ });
237
+
238
+ it('DBAction.READ — DTO field structure', () => {
239
+ const dto = DBAction.READ({
240
+ requestObject: { method: 'findOne', payload: { id: 1 } },
241
+ responseObject: {
242
+ httpStatusCode: 200,
243
+ headers: {},
244
+ payload: { name: 'item' },
245
+ },
246
+ });
247
+ expect(normalizeLog(dto)).toMatchSnapshot();
248
+ });
249
+
250
+ it('DBAction.CREATE — DTO field structure', () => {
251
+ const dto = DBAction.CREATE({
252
+ requestObject: { method: 'insertOne', payload: { name: 'new-item' } },
253
+ });
254
+ expect(normalizeLog(dto)).toMatchSnapshot();
255
+ });
256
+
257
+ it('LogicAction.FUNCTION — DTO field structure', () => {
258
+ const dto = LogicAction.FUNCTION({ message: 'Processing data' });
259
+ expect(normalizeLog(dto)).toMatchSnapshot();
260
+ });
261
+
262
+ it('LogicAction.CHECKPOINT — DTO field structure', () => {
263
+ const dto = LogicAction.CHECKPOINT({ message: 'Step completed' });
264
+ expect(normalizeLog(dto)).toMatchSnapshot();
265
+ });
266
+
267
+ it('ExceptionAction.LOG — DTO field structure', () => {
268
+ const dto = ExceptionAction.LOG({ message: 'Unexpected error occurred' });
269
+ expect(normalizeLog(dto)).toMatchSnapshot();
270
+ });
271
+ });
272
+
273
+ describe('Log Snapshot — Cloudron format', () => {
274
+ let app: INestApplication;
275
+ let logSpy: jest.SpyInstance;
276
+
277
+ beforeAll(async () => {
278
+ @Module({
279
+ imports: [
280
+ AppLogConfigModule.forRoot({
281
+ appName: 'snapshot-app',
282
+ componentName: 'snapshot-comp',
283
+ format: FORMAT_TYPE.CLOUDRON,
284
+ }),
285
+ LoggerModule,
286
+ ],
287
+ controllers: [SnapshotController],
288
+ })
289
+ class CloudronSnapshotModule implements NestModule {
290
+ configure(consumer: MiddlewareConsumer) {
291
+ consumer.apply(HTTPLogsMiddleware).forRoutes('*');
292
+ }
293
+ }
294
+
295
+ const moduleFixture: TestingModule = await Test.createTestingModule({
296
+ imports: [CloudronSnapshotModule],
297
+ }).compile();
298
+
299
+ app = moduleFixture.createNestApplication();
300
+ await app.init();
301
+
302
+ logSpy = jest.spyOn(app.get(CustomLoggerService), 'log');
303
+ });
304
+
305
+ afterAll(async () => await app.close());
306
+ beforeEach(() => logSpy.mockClear());
307
+
308
+ it('GET /snapshot/success — log count + structure', async () => {
309
+ await request(app.getHttpServer())
310
+ .get('/snapshot/success')
311
+ .set('x-ais-orderref', 'SNAP-CLDRON-001');
312
+ await waitForFinish();
313
+ const snapshot = captureSnapshot(logSpy.mock.calls);
314
+ expect(snapshot.count).toMatchSnapshot('log count');
315
+ expect(snapshot.logs).toMatchSnapshot('log structure');
316
+ });
317
+
318
+ it('GET /snapshot/not-found — log count + structure', async () => {
319
+ await request(app.getHttpServer())
320
+ .get('/snapshot/not-found')
321
+ .set('x-ais-orderref', 'SNAP-CLDRON-002');
322
+ await waitForFinish();
323
+ const snapshot = captureSnapshot(logSpy.mock.calls);
324
+ expect(snapshot.count).toMatchSnapshot('log count');
325
+ expect(snapshot.logs).toMatchSnapshot('log structure');
326
+ });
327
+
328
+ it('GET /snapshot/no-headers — log count + structure (missing correlation headers)', async () => {
329
+ await request(app.getHttpServer()).get('/snapshot/no-headers');
330
+ await waitForFinish();
331
+ const snapshot = captureSnapshot(logSpy.mock.calls);
332
+ expect(snapshot.count).toMatchSnapshot('log count');
333
+ expect(snapshot.logs).toMatchSnapshot('log structure');
334
+ });
335
+
336
+ it('HttpAction.GET — DTO field structure', () => {
337
+ const dto = HttpAction.GET({
338
+ requestObject: {
339
+ url: '/api/resource/1',
340
+ headers: { 'x-ais-orderref': 'SNAP-C-ACT-001' },
341
+ queryString: {},
342
+ routeParameters: {},
343
+ payload: {},
344
+ },
345
+ responseObject: { httpStatusCode: 200, headers: {}, payload: {} },
346
+ });
347
+ expect(normalizeLog(dto)).toMatchSnapshot();
348
+ });
349
+
350
+ it('DBAction.UPDATE — DTO field structure', () => {
351
+ const dto = DBAction.UPDATE({
352
+ requestObject: {
353
+ method: 'updateOne',
354
+ payload: { id: 1, name: 'updated' },
355
+ },
356
+ });
357
+ expect(normalizeLog(dto)).toMatchSnapshot();
358
+ });
359
+
360
+ it('LogicAction.FUNCTION — DTO field structure', () => {
361
+ const dto = LogicAction.FUNCTION({ message: 'Processing' });
362
+ expect(normalizeLog(dto)).toMatchSnapshot();
363
+ });
364
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icd-iot-aicf/nestjs-logger",
3
- "version": "4.0.8",
3
+ "version": "4.0.10",
4
4
  "description": "",
5
5
  "author": "",
6
6
  "private": false,