@jnpt/nest-logging 0.0.0-stage → 1.1.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/README.md CHANGED
@@ -1,3 +1,840 @@
1
- # Temporary Holding Version
1
+ # NestJS Structured Logs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Library สำหรับทำ **structured JSON logging** ใน NestJS (ทั้ง **Express** และ **Fastify**) — port มาจาก Go library
4
+ [`github.com/jnpt-golangs/logging`](../go_logging) โดยมี feature และรูปแบบ log JSON เหมือนกันทุกอย่าง
5
+ มี log 2 ประเภทหลักคือ **`application`** และ **`request`**
6
+
7
+ ## Features
8
+
9
+ - 📝 **Structured JSON Logging** — ทุก log ออกเป็น JSON 1 บรรทัด ไปที่ `stdout` (field เหมือน Go version ทุกตัว)
10
+ - 🔗 **Correlation ID** — รับ/สร้าง `X-Correlation-ID` อัตโนมัติ และส่งต่อไปทุก log ใน request เดียวกันโดยไม่ต้องส่ง context เอง
11
+ - 📥 **Incoming Request Logging** — log request + response ของทุก route เป็น entry เดียว (Express middleware / Fastify hooks — เลือกให้อัตโนมัติ)
12
+ - 📤 **Outgoing Request Logging** — inject `HttpClient` ผ่าน `HttpClientModule` (Dynamic Module แบบ `@nestjs/axios`) หรือใช้ `fetch` ที่มี log ก็ได้ พร้อมส่ง correlation ID ต่อ
13
+ - 🔒 **Security** — mask header และ field ที่เป็นความลับอัตโนมัติ (case-insensitive, nested object/array)
14
+ - ⚙️ **Configurable** — config ได้ทุกอย่างเหมือน Go version และทุกค่าเป็น optional
15
+ - ⚡ **Express และ Fastify 100%** — `@nestjs/platform-express` และ `@nestjs/platform-fastify` ได้ข้อมูลใน log ครบเท่ากัน รวมถึง Express / Fastify แบบไม่ใช้ Nest
16
+ - ✅ ทดสอบแล้วกับ NestJS 10, 11, 12 (CommonJS และ ESM), Express 4/5, Fastify 4/5, Node.js >= 18
17
+
18
+ ---
19
+
20
+ ## สารบัญ
21
+
22
+ 1. [Installation](#installation)
23
+ 2. [Quick Start (3 ขั้นตอน)](#quick-start-3-ขั้นตอน)
24
+ 3. [Application Logging](#application-logging)
25
+ 4. [Incoming Request Logging (Middleware)](#incoming-request-logging-middleware)
26
+ 5. [Fastify](#fastify)
27
+ 6. [Outgoing Request Logging (HTTP Client)](#outgoing-request-logging-http-client)
28
+ 7. [Correlation ID](#correlation-id)
29
+ 8. [Masking](#masking)
30
+ 9. [Configuration](#configuration)
31
+ 10. [รูปแบบ Log JSON](#รูปแบบ-log-json)
32
+ 11. [API Reference](#api-reference)
33
+ 12. [เทียบกับ Go version](#เทียบกับ-go-version)
34
+ 13. [Development](#development)
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ ### 1. ติดตั้ง package จาก npm (แนะนำ)
41
+
42
+ package อยู่บน npm: [`@jnpt/nest-logging`](https://www.npmjs.com/package/@jnpt/nest-logging)
43
+
44
+ ```bash
45
+ npm install @jnpt/nest-logging
46
+ # หรือ
47
+ yarn add @jnpt/nest-logging
48
+ # หรือ
49
+ pnpm add @jnpt/nest-logging
50
+ ```
51
+
52
+ ถ้าใช้ NestJS บน Fastify ต้องติดตั้ง adapter ของ Nest ด้วย (ถ้ามีอยู่แล้วก็ข้ามได้):
53
+
54
+ ```bash
55
+ npm install @nestjs/platform-fastify
56
+ ```
57
+
58
+ ระบุ version ได้ เช่น `npm install @jnpt/nest-logging@1.0.0`, ดู version ทั้งหมดด้วย `npm view @jnpt/nest-logging versions`
59
+
60
+ #### วิธีอื่น (ไม่ผ่าน npm registry)
61
+
62
+ ```bash
63
+ # ก) จาก GitHub (ระบุ tag/branch ได้หลัง #)
64
+ npm install github:jnpt-nestjs-templates/logging#main
65
+
66
+ # ข) จากโฟลเดอร์ในเครื่อง (ระหว่างพัฒนา library)
67
+ npm install /path/to/nest_logging
68
+
69
+ # ค) จากไฟล์ tarball
70
+ cd /path/to/nest_logging && npm pack # ได้ไฟล์ jnpt-nest-logging-<version>.tgz
71
+ cd /path/to/your-nest-app && npm install /path/to/jnpt-nest-logging-<version>.tgz
72
+ ```
73
+
74
+ > วิธี ก/ข/ค จะ build `dist/` ผ่าน script `prepare` ตอนติดตั้ง
75
+ > **npm 12 ขึ้นไป:** ติดตั้งจาก git ต้องใส่ `--allow-git=all` และต้องอนุญาต install script ก่อนด้วย
76
+ > `npm install-scripts approve @jnpt/nest-logging` ไม่งั้น `prepare` จะถูกบล็อกและไม่มี `dist/` — ติดตั้งจาก npm registry ไม่เจอปัญหานี้
77
+
78
+ ### 2. Requirements
79
+
80
+ | สิ่งที่ต้องมี | เวอร์ชัน |
81
+ |---|---|
82
+ | Node.js | >= 18 (ใช้ `fetch`, `AsyncLocalStorage`, `crypto.randomUUID` ของ Node) |
83
+ | `@nestjs/common` | >= 9 (peer dependency — มีอยู่แล้วในโปรเจกต์ Nest) |
84
+ | HTTP platform | `@nestjs/platform-express` (default ของ Nest) **หรือ** `@nestjs/platform-fastify` |
85
+
86
+ ไม่มี runtime dependency อื่นเลย
87
+
88
+ ---
89
+
90
+ ## Quick Start (3 ขั้นตอน)
91
+
92
+ ### ขั้นที่ 1 — import `LoggingModule` ใน `AppModule`
93
+
94
+ ```ts
95
+ // app.module.ts
96
+ import { Module } from '@nestjs/common';
97
+ import { LoggingModule } from '@jnpt/nest-logging';
98
+
99
+ @Module({
100
+ imports: [
101
+ LoggingModule.forRoot({ applicationName: 'my-service' }), // config ทุกตัว optional
102
+ ],
103
+ controllers: [UsersController],
104
+ providers: [UsersService],
105
+ })
106
+ export class AppModule {}
107
+ ```
108
+
109
+ แค่นี้ทุก request ที่เข้ามาจะถูก log อัตโนมัติ (ยกเว้น `/health`, `/metrics`, `/favicon.ico`)
110
+
111
+ ### ขั้นที่ 2 — ใช้ `AppLogger` ใน service / controller
112
+
113
+ ```ts
114
+ // users.service.ts
115
+ import { Injectable } from '@nestjs/common';
116
+ import { AppLogger } from '@jnpt/nest-logging';
117
+
118
+ @Injectable()
119
+ export class UsersService {
120
+ private readonly log = new AppLogger(UsersService.name);
121
+
122
+ findOne(id: string) {
123
+ this.log.info('Fetching user', { user_id: id }); // correlation_id ใส่ให้อัตโนมัติ
124
+ // ...
125
+ }
126
+ }
127
+ ```
128
+
129
+ ### ขั้นที่ 3 (แนะนำ) — เปิด `rawBody` ใน `main.ts`
130
+
131
+ ```ts
132
+ // main.ts
133
+ import { NestFactory } from '@nestjs/core';
134
+ import { AppLogger } from '@jnpt/nest-logging';
135
+ import { AppModule } from './app.module';
136
+
137
+ async function bootstrap() {
138
+ const log = new AppLogger('main');
139
+ const app = await NestFactory.create(AppModule, { rawBody: true }); // ให้ log request body แบบ raw เหมือน Go
140
+ await app.listen(8080);
141
+ log.info('Starting server on :8080');
142
+ }
143
+ bootstrap();
144
+ ```
145
+
146
+ > `rawBody: true` ไม่บังคับ — ถ้าไม่เปิด middleware จะใช้ `req.body` ที่ Nest parse แล้วแทน (ดู [รายละเอียด](#request-body-ถูกเก็บอย่างไร))
147
+
148
+ ---
149
+
150
+ ## Application Logging
151
+
152
+ สร้าง logger ด้วยชื่อ (ไปอยู่ใน field `logger_name`):
153
+
154
+ ```ts
155
+ import { AppLogger } from '@jnpt/nest-logging';
156
+
157
+ const log = new AppLogger('MyService'); // ใช้ global default config (จาก LoggingModule.forRoot / configureDefaults)
158
+ const custom = new AppLogger('Other', { // ใช้ config ของตัวเอง (partial ได้ ที่เหลือใช้ค่า default)
159
+ applicationName: 'other-app',
160
+ minLevel: 'WARN',
161
+ });
162
+ ```
163
+
164
+ ### Methods
165
+
166
+ | Method | Level | หมายเหตุ |
167
+ |---|---|---|
168
+ | `debug(message, ...extra)` | DEBUG | |
169
+ | `info(message, ...extra)` | INFO | |
170
+ | `warn(message, ...extra)` | WARN | |
171
+ | `warnError(message, err, ...extra)` | WARN | แนบ `error` (class, message, stack_trace) |
172
+ | `error(message, ...extra)` | ERROR | |
173
+ | `errorWithErr(message, err, ...extra)` | ERROR | แนบ `error` (class, message, stack_trace) |
174
+ | `logIncomingHttp(...)` | ตาม status | middleware เรียกให้ ปกติไม่ต้องเรียกเอง |
175
+ | `logOutgoingHttp(...)` | ตาม status | `createLoggingFetch` เรียกให้ ปกติไม่ต้องเรียกเอง |
176
+ | `setOutput(writer)` | — | เปลี่ยนปลายทาง log (default `process.stdout`) |
177
+
178
+ ### ตัวอย่าง
179
+
180
+ ```ts
181
+ log.info('Processing started');
182
+ log.debug('Debug information');
183
+ log.warn('Warning message');
184
+
185
+ // extra เป็น object ธรรมดา
186
+ log.info('User action', { userId: '12345', action: 'LOGIN' });
187
+
188
+ // extra เป็น class instance / DTO ก็ได้ (แปลงผ่าน JSON)
189
+ class OrderDto { constructor(public orderId: string, public amount: number) {} }
190
+ log.info('Order created', new OrderDto('A-1', 100));
191
+
192
+ // ส่ง extra หลายตัว → merge เป็น object เดียว (key ซ้ำตัวหลังชนะ)
193
+ log.info('Merged', { a: 1 }, { b: 2 }); // extra: { a: 1, b: 2 }
194
+
195
+ // extra ที่ไม่ใช่ object (array, string, number) → เก็บใน key "data"
196
+ log.info('List', [1, 2, 3]); // extra: { data: [1, 2, 3] }
197
+
198
+ // log พร้อม error
199
+ try {
200
+ await doSomething();
201
+ } catch (err) {
202
+ log.errorWithErr('Error occurred', err, { step: 'doSomething' });
203
+ }
204
+ log.warnError('Invalid request body', new Error('email is required'));
205
+ ```
206
+
207
+ ### Log level
208
+
209
+ ลำดับ: `TRACE`(5000) < `DEBUG`(10000) < `INFO`(20000) < `WARN`(30000) < `ERROR`(40000)
210
+
211
+ `minLevel` (default `DEBUG`) — log ที่ level ต่ำกว่านี้จะไม่ถูกเขียน ถ้าใส่ค่าไม่ถูกต้องจะถือเป็น `DEBUG`
212
+
213
+ ### Error / Stack trace
214
+
215
+ - `error.class` = ชื่อ class ของ error เช่น `Error`, `TypeError`, `HttpException` (ถ้าไม่ใช่ `Error` จะเป็น `typeof` เช่น `string`)
216
+ - `error.stack_trace` = array ของ frame จาก `err.stack` จำกัดที่ `applicationLogging.maxStackTraceDepth` (default 50)
217
+ - เหมือน Go: ส่วน `error` จะถูกใส่ **เฉพาะเมื่อ** `applicationLogging.includeStackTrace = true` (default)
218
+ - V8 เก็บ stack แค่ 10 frame เป็นค่า default — ถ้าต้องการมากกว่านั้นให้ตั้ง `Error.stackTraceLimit = 50` ใน `main.ts`
219
+
220
+ ### เปลี่ยนปลายทาง output
221
+
222
+ ```ts
223
+ import { createWriteStream } from 'node:fs';
224
+ log.setOutput(createWriteStream('app.log', { flags: 'a' })); // อะไรก็ได้ที่มี write(string)
225
+ log.setOutput({ write: (line) => myCollector.push(line) }); // เช่นใน unit test
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Incoming Request Logging (Middleware)
231
+
232
+ ### แบบที่ 1 — ง่ายสุด (แนะนำ): `LoggingModule.forRoot()`
233
+
234
+ ```ts
235
+ @Module({
236
+ imports: [LoggingModule.forRoot()], // ใช้ default config ทั้งหมด
237
+ })
238
+ export class AppModule {}
239
+ ```
240
+
241
+ ```ts
242
+ @Module({
243
+ imports: [
244
+ LoggingModule.forRoot({ // หรือกำหนด config เอง (เฉพาะที่ต้องการ)
245
+ applicationName: 'my-service',
246
+ requestLogging: {
247
+ logBody: true,
248
+ logResponseBody: true,
249
+ maxBodySize: 10000,
250
+ excludePatterns: ['/health', '/metrics'],
251
+ },
252
+ }),
253
+ ],
254
+ })
255
+ export class AppModule {}
256
+ ```
257
+
258
+ `forRoot(config)` ทำ 2 อย่าง: (1) เรียก `configureDefaults(config)` ตั้ง global default config
259
+ และ (2) apply `LoggingMiddleware` กับทุก route (`forRoutes('*')`) — logger ของ middleware ชื่อ `LoggingMiddleware`
260
+
261
+ ### แบบที่ 2 — กำหนดเองว่าจะใช้กับ route ไหน: `LoggingMiddleware`
262
+
263
+ ```ts
264
+ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
265
+ import { configureDefaults, LoggingMiddleware } from '@jnpt/nest-logging';
266
+
267
+ configureDefaults({ applicationName: 'my-service' });
268
+
269
+ @Module({ /* ... */ })
270
+ export class AppModule implements NestModule {
271
+ configure(consumer: MiddlewareConsumer) {
272
+ consumer.apply(LoggingMiddleware).forRoutes(UsersController, OrdersController);
273
+ }
274
+ }
275
+ ```
276
+
277
+ ### แบบที่ 3 — ใช้ logger / config เฉพาะตัว: `createLoggingMiddleware(logger, config?)`
278
+
279
+ เทียบเท่า `middleware.LoggingMiddleware(log, config)` ของ Go ใช้กับ `app.use()` ได้เลย:
280
+
281
+ ```ts
282
+ // main.ts
283
+ import { AppLogger, createLoggingMiddleware } from '@jnpt/nest-logging';
284
+
285
+ const app = await NestFactory.create(AppModule, { rawBody: true });
286
+ const log = new AppLogger('main');
287
+ app.use(createLoggingMiddleware(log)); // ใช้ default config
288
+ // หรือ
289
+ app.use(createLoggingMiddleware(log, { requestLogging: { maxBodySize: 2048 } })); // config ของตัวเอง
290
+ ```
291
+
292
+ > **หมายเหตุ (เหมือน Go):** `config` ของ middleware ควบคุมแค่ exclude/include patterns, การเก็บ body และ `maxBodySize`
293
+ > ส่วน `application`, masking, `logHeaders` และการเปิด/ปิด log มาจาก config ของ **logger** ที่ส่งเข้าไป —
294
+ > ถ้าต้องการให้ตรงกันให้สร้าง logger ด้วย config เดียวกัน: `new AppLogger('main', cfg)` + `createLoggingMiddleware(log, cfg)`
295
+ >
296
+ > แบบที่ 3 ห้ามใช้ร่วมกับ `LoggingModule.forRoot()` ไม่งั้นจะได้ log ซ้ำ 2 รอบ — ถ้าจะตั้ง global config ให้ใช้ `configureDefaults()` แทน
297
+ > และควร `app.use(...)` ก่อน `app.listen()`
298
+
299
+ ### Middleware ทำอะไรบ้าง (ตามลำดับ — เหมือนกันทั้ง Express และ Fastify)
300
+
301
+ 1. ถ้า path ตรง `excludePatterns` (หรือไม่ตรง `includePatterns` เมื่อกำหนดไว้) → ข้าม ไม่ log และไม่ใส่ correlation ID
302
+ 2. อ่าน header `X-Correlation-ID` ถ้าไม่มีจะสร้าง UUID ใหม่ → ใส่ใน response header `X-Correlation-ID` และใน async context
303
+ 3. ดักเก็บ response body (`res.write` / `res.end`)
304
+ 4. เมื่อ response ส่งเสร็จ (รวมถึงกรณี exception, 404, client ตัดการเชื่อมต่อ) → เขียน log `"Incoming request"` 1 entry ที่มี:
305
+ method, uri (path ไม่รวม query), status_code, duration_ms, remote_address, user_agent,
306
+ request headers / query_params / path_params / body / content_type / content_length,
307
+ response headers / body / content_type / content_length
308
+ 5. level ตาม status: `< 400` → INFO, `400–499` → WARN, `>= 500` → ERROR
309
+
310
+ ### Request body ถูกเก็บอย่างไร
311
+
312
+ | กรณี | สิ่งที่ log |
313
+ |---|---|
314
+ | เปิด `rawBody: true` | raw bytes → ถ้า parse JSON ได้ใช้ JSON, ไม่ได้ใช้ข้อความดิบตัดที่ `maxBodySize` bytes + `... [TRUNCATED]` (เหมือน Go ทุกประการ) |
315
+ | ไม่เปิด `rawBody` | `req.body` ที่ Nest parse แล้ว (JSON / urlencoded → object), ถ้าเป็น string/Buffer ใช้กฎเดียวกับด้านบน |
316
+ | ไม่มี body | ไม่มี key `body` |
317
+
318
+ response body ใช้กฎเดียวกัน: JSON → object, อย่างอื่น → ข้อความตัดที่ `maxBodySize`
319
+ (เหมือน Go: JSON ไม่ถูกตัด, ตัดเฉพาะข้อความที่ไม่ใช่ JSON) — ถ้า `logResponseBody: false` จะไม่เก็บ body ไว้ในหน่วยความจำเลย
320
+
321
+ ---
322
+
323
+ ## Fastify
324
+
325
+ ทำงานได้ครบ 100% เท่ากับ Express ทั้งแบบ NestJS + `FastifyAdapter` และแบบ Fastify ล้วน
326
+ (ข้อมูลใน log เหมือนกันทุก field: path_params, query_params, request/response body, masking, correlation ID, level ตาม status)
327
+
328
+ ใน Fastify library จะใช้ **Fastify hooks** (`onRequest` + `preHandler`) แทน middleware
329
+ เพราะ middleware ของ Nest บน Fastify (ผ่าน middie) จะไม่เห็น `request.params` และ `request.body`
330
+
331
+ ### NestJS + Fastify (ตาม [NestJS Performance (Fastify)](https://docs.nestjs.com/techniques/performance))
332
+
333
+ ```bash
334
+ npm install @nestjs/platform-fastify
335
+ ```
336
+
337
+ ```ts
338
+ // main.ts
339
+ import { NestFactory } from '@nestjs/core';
340
+ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
341
+ import { AppLogger } from '@jnpt/nest-logging';
342
+ import { AppModule } from './app.module';
343
+
344
+ async function bootstrap() {
345
+ const log = new AppLogger('main');
346
+ const app = await NestFactory.create<NestFastifyApplication>(
347
+ AppModule,
348
+ new FastifyAdapter(),
349
+ { rawBody: true }, // optional: log raw request body เหมือน Go
350
+ );
351
+ await app.listen(8080, '0.0.0.0');
352
+ log.info('Starting server on :8080');
353
+ }
354
+ bootstrap();
355
+ ```
356
+
357
+ `AppModule` **ไม่ต้องแก้อะไรเลย** — ใช้ `LoggingModule.forRoot()` ตัวเดิม
358
+ มันจะตรวจ HTTP adapter เอง: เป็น Express ก็ใช้ `LoggingMiddleware`, เป็น Fastify ก็ลง hooks ให้อัตโนมัติ
359
+ `AppLogger`, `createLoggingFetch` และ correlation ID ใช้เหมือนเดิมทุกอย่าง
360
+
361
+ > บน Fastify อย่าใช้ `consumer.apply(LoggingMiddleware)` เอง (ได้ log ไม่ครบเพราะเหตุผลด้านบน) — ให้ใช้ `LoggingModule.forRoot()`
362
+ > หรือถ้าต้องการ logger/config เฉพาะตัวให้ใช้ `createFastifyLoggingPlugin` แบบด้านล่าง:
363
+ >
364
+ > ```ts
365
+ > const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
366
+ > await app.register(createFastifyLoggingPlugin(new AppLogger('main'), { requestLogging: { maxBodySize: 2048 } }));
367
+ > ```
368
+ > (กรณีนี้ไม่ต้อง import `LoggingModule` — ใช้ `configureDefaults()` ตั้ง global config แทน ไม่งั้นจะได้ log ซ้ำ)
369
+
370
+ ### Fastify ล้วน (ไม่ใช้ NestJS)
371
+
372
+ ```ts
373
+ import Fastify from 'fastify';
374
+ import { AppLogger, configureDefaults, createFastifyLoggingPlugin, createLoggingFetch } from '@jnpt/nest-logging';
375
+
376
+ configureDefaults({ applicationName: 'my-service' });
377
+ const log = new AppLogger('main');
378
+
379
+ const app = Fastify();
380
+ await app.register(createFastifyLoggingPlugin(log)); // หรือ createFastifyLoggingPlugin(log, config)
381
+
382
+ app.get('/api/users/:id', async (req) => {
383
+ new AppLogger('UserHandler').info('Fetching user'); // มี correlation_id อัตโนมัติ
384
+ return { id: (req.params as { id: string }).id };
385
+ });
386
+
387
+ await app.listen({ port: 8080, host: '0.0.0.0' });
388
+ ```
389
+
390
+ - plugin นี้ **ไม่ encapsulated** (เทียบเท่าห่อด้วย `fastify-plugin`) จึงครอบทุก route รวมถึง route ใน child plugin/prefix
391
+ - จะใช้ `registerFastifyLogging(app, log, config?)` (เรียก `addHook` ตรงๆ) แทน `register` ก็ได้ ผลเหมือนกัน
392
+ - เก็บ response body ได้ทั้ง payload ปกติและแบบ stream
393
+ - ไม่ต้องลง `fastify-plugin` หรือ `@fastify/middie` เพิ่ม
394
+
395
+ ### Express ล้วน (ไม่ใช้ NestJS)
396
+
397
+ ```ts
398
+ import express from 'express';
399
+ import { AppLogger, createLoggingMiddleware } from '@jnpt/nest-logging';
400
+
401
+ const app = express();
402
+ app.use(express.json());
403
+ app.use(createLoggingMiddleware(new AppLogger('main')));
404
+ ```
405
+
406
+ ---
407
+
408
+ ## Outgoing Request Logging (HTTP Client)
409
+
410
+ ### แบบที่ 1 (แนะนำสำหรับ NestJS) — `HttpClientModule` + inject `HttpClient`
411
+
412
+ ใช้เหมือน `HttpModule` / `HttpService` ของ NestJS: register module ครั้งเดียว แล้ว inject `HttpClient` ผ่าน constructor
413
+ (`HttpClientModule` สร้างด้วย [`ConfigurableModuleBuilder`](https://docs.nestjs.com/fundamentals/dynamic-modules#configurable-module-builder))
414
+
415
+ ```ts
416
+ // app.module.ts
417
+ import { Module } from '@nestjs/common';
418
+ import { HttpClientModule, LoggingModule } from '@jnpt/nest-logging';
419
+
420
+ @Module({
421
+ imports: [
422
+ LoggingModule.forRoot({ applicationName: 'my-service' }),
423
+ HttpClientModule.register({
424
+ baseUrl: 'https://jsonplaceholder.typicode.com', // optional — ใช้กับ URL แบบ relative
425
+ headers: { 'X-Api-Key': process.env.API_KEY! }, // optional — ส่งทุก request
426
+ }),
427
+ ],
428
+ providers: [TodoClient],
429
+ })
430
+ export class AppModule {}
431
+ ```
432
+
433
+ ```ts
434
+ // todo.client.ts
435
+ import { Injectable } from '@nestjs/common';
436
+ import { HttpClient } from '@jnpt/nest-logging';
437
+
438
+ @Injectable()
439
+ export class TodoClient {
440
+ constructor(private readonly http: HttpClient) {}
441
+
442
+ async getTodo(id: number) {
443
+ const res = await this.http.get(`/todos/${id}`);
444
+ return res.json(); // อ่าน body ได้ตามปกติ (logger อ่านจาก clone)
445
+ }
446
+
447
+ async createTodo(todo: { title: string }) {
448
+ const res = await this.http.post('/todos', todo); // object → JSON + Content-Type: application/json ให้อัตโนมัติ
449
+ return res.json();
450
+ }
451
+ }
452
+ ```
453
+
454
+ **Options ของ `register()` / ที่ `useFactory` คืนค่า** (ทุกตัว optional)
455
+
456
+ | Option | ความหมาย |
457
+ |---|---|
458
+ | `baseUrl` | prefix ของ URL แบบ relative (`'/todos/1'` → `baseUrl + '/todos/1'`); URL เต็ม (`https://...`) ใช้ตามเดิม |
459
+ | `headers` | header ที่ส่งทุก request (header ที่ส่งตอนเรียกจะทับ) |
460
+ | `loggerName` | `logger_name` ของ log ขาออก (default `"HttpClient"`) |
461
+ | `config` | logging config เฉพาะ client นี้ (default: config จาก `LoggingModule.forRoot`) |
462
+ | `fetch` | fetch implementation อื่น (default: global `fetch`) |
463
+ | `isGlobal` | `true` = ใช้ได้ทุก module โดยไม่ต้อง import ซ้ำ (เฉพาะ `register` / `registerAsync`) |
464
+
465
+ **อ่าน options จาก `ConfigService`** — `registerAsync` รองรับ `useFactory` / `useClass` / `useExisting` + `imports` / `inject` ตามมาตรฐาน Nest
466
+
467
+ ```ts
468
+ HttpClientModule.registerAsync({
469
+ imports: [ConfigModule],
470
+ inject: [ConfigService],
471
+ useFactory: (config: ConfigService) => ({ baseUrl: config.get('TODO_API_URL') }),
472
+ })
473
+ ```
474
+
475
+ **Methods ของ `HttpClient`** — ทุกตัวคืน `Promise<Response>` (fetch Response มาตรฐาน) และเหมือน `fetch` คือ **ไม่ throw เมื่อได้ 4xx/5xx** (เช็ค `res.ok` เอง) แต่ throw เมื่อ network error
476
+
477
+ | Method | หมายเหตุ |
478
+ |---|---|
479
+ | `get(url, init?)`, `head(url, init?)`, `delete(url, init?)` | `init` = `RequestInit` ของ fetch (headers, signal, ...) |
480
+ | `post(url, body?, init?)`, `put(...)`, `patch(...)` | `body` เป็น object/array/number/boolean → ส่งเป็น JSON; string / `Buffer` / `Blob` / `URLSearchParams` / `FormData` / stream → ส่งตามเดิม |
481
+ | `request(url, init)` | เหมือน `fetch(url, init)` |
482
+
483
+ ต้องการหลาย client (คนละ `baseUrl`) → สร้าง provider เอง เช่น
484
+ `{ provide: 'PAYMENT_HTTP', useFactory: () => new HttpClient({ baseUrl: 'https://pay.example.com' }) }`
485
+ และใช้นอก NestJS ได้ด้วย `new HttpClient(options)`
486
+
487
+ ### แบบที่ 2 — `createLoggingFetch` (fetch function)
488
+
489
+ `createLoggingFetch(logger, config?, baseFetch?)` คืนค่า function ที่ใช้แทน `fetch` ได้ทันที (เทียบเท่า `httpclient.NewClient` ของ Go) — `HttpClient` ด้านบนก็ใช้ตัวนี้ข้างใน
490
+
491
+ ```ts
492
+ import { AppLogger, createLoggingFetch } from '@jnpt/nest-logging';
493
+
494
+ const log = new AppLogger('MyService');
495
+ const fetch = createLoggingFetch(log); // ใช้ default config
496
+
497
+ // หรือกำหนด config เอง
498
+ const fetchWithConfig = createLoggingFetch(log, {
499
+ applicationName: 'my-service',
500
+ requestLogging: { logBody: true, logResponseBody: true },
501
+ });
502
+ ```
503
+
504
+ ### ทำอะไรบ้าง
505
+
506
+ 1. ใช้ correlation ID ของ request ปัจจุบัน (ถ้าไม่มี เช่นเรียกจาก cron job จะสร้างใหม่) แล้วใส่ header `X-Correlation-ID`
507
+ 2. เก็บ request body (string, `URLSearchParams`, `Buffer`/`Uint8Array`, `ArrayBuffer`, `Blob`, หรือ `Request` object) — stream/FormData จะส่งปกติแต่ไม่ log body
508
+ 3. เรียก `fetch` จริง แล้ว log `"Outgoing request"` 1 entry พร้อม `extra: { "direction": "outgoing" }`, level ตาม status
509
+ 4. ถ้า network error → log ERROR `"Outgoing request failed"` พร้อม `extra: { method, url, duration_ms }` และ error แล้ว **throw ต่อ**
510
+
511
+ ใช้กับ fetch implementation อื่นได้ผ่าน parameter ที่ 3 เช่น `createLoggingFetch(log, undefined, undiciFetch)`
512
+
513
+ ---
514
+
515
+ ## Correlation ID
516
+
517
+ Go ต้องส่ง `context.Context` ทุก method — ใน Node ใช้ `AsyncLocalStorage` แทน
518
+ correlation ID จึง "ตามไปเอง" ตลอด async chain ของ request (controller → service → repository → fetch) โดยไม่ต้องส่ง parameter
519
+
520
+ ```ts
521
+ import {
522
+ CorrelationIDHeader, // 'X-Correlation-ID'
523
+ getCorrelationId, // ดึง correlation ID ปัจจุบัน ('' ถ้าไม่มี)
524
+ withCorrelationId, // รัน function ภายใต้ correlation ID ที่กำหนด
525
+ generateCorrelationId, // สร้าง UUID v4 ใหม่
526
+ ensureCorrelationId, // ID ปัจจุบัน หรือสร้างใหม่ถ้าไม่มี
527
+ } from '@jnpt/nest-logging';
528
+
529
+ // ใน controller/service (หลัง middleware)
530
+ const id = getCorrelationId();
531
+
532
+ // งานที่ไม่ได้มาจาก HTTP request เช่น cron, queue consumer, Kafka message
533
+ await withCorrelationId(message.headers['x-correlation-id'] ?? generateCorrelationId(), async () => {
534
+ log.info('Processing message'); // มี correlation_id
535
+ await fetch('https://other-service/api'); // ส่ง X-Correlation-ID ต่อ (ถ้าใช้ createLoggingFetch)
536
+ });
537
+ ```
538
+
539
+ ---
540
+
541
+ ## Masking
542
+
543
+ ค่าที่ถูก mask จะถูกแทนด้วย `maskValue` (default `***MASKED***`)
544
+
545
+ - **Headers** (`maskedHeaders`) default: `Authorization`, `X-Api-Key`, `Cookie`, `Set-Cookie` — ใช้กับทั้ง request และ response
546
+ - **Body fields** (`maskedFields`) default: `password`, `secret`, `token`, `creditCard`, `ssn` — ไล่ทุกชั้นของ object/array ซ้อนกัน
547
+ - เทียบชื่อแบบ **case-insensitive** (`Password`, `PASSWORD`, `password` ถูก mask หมด)
548
+ - เป็นการเทียบชื่อ key ตรงตัว (ไม่ใช่ substring) — `accessToken` จะไม่ถูก mask ถ้าไม่ได้ใส่ไว้
549
+ - body ที่เป็นข้อความดิบ (ไม่ใช่ JSON) จะไม่ถูก mask (เหมือน Go)
550
+ - **Query string** ก็ถูก mask ด้วยรายชื่อเดียวกับ `maskedFields` ทั้ง `query_params` ของ incoming request และ `uri` ของ outgoing request (เช่น `?token=abc` → `?token=***MASKED***`) — ส่วนนี้ **เพิ่มจาก Go version** เพื่อไม่ให้ token/API key ใน URL หลุดไปใน log
551
+
552
+ การ **เพิ่ม** field (config เป็นการแทนที่ array ไม่ใช่ต่อท้าย):
553
+
554
+ ```ts
555
+ import { defaultConfig, LoggingModule } from '@jnpt/nest-logging';
556
+
557
+ LoggingModule.forRoot({
558
+ maskedHeaders: [...defaultConfig().maskedHeaders, 'X-Client-Secret'],
559
+ maskedFields: [...defaultConfig().maskedFields, 'pin', 'accessToken', 'refreshToken'],
560
+ maskValue: '[REDACTED]',
561
+ });
562
+ ```
563
+
564
+ ใช้ `MaskingUtil` ตรงๆ ก็ได้:
565
+
566
+ ```ts
567
+ import { buildConfig, MaskingUtil } from '@jnpt/nest-logging';
568
+ const masking = new MaskingUtil(buildConfig());
569
+ masking.maskBody({ user: 'a', password: 'b' }); // { user: 'a', password: '***MASKED***' }
570
+ masking.maskHeaders({ Authorization: 'Bearer x' }); // { Authorization: '***MASKED***' }
571
+ masking.maskUrl('https://api.example.com/x?token=abc&page=1'); // 'https://api.example.com/x?token=***MASKED***&page=1'
572
+ ```
573
+
574
+ ---
575
+
576
+ ## Configuration
577
+
578
+ config **ทุกตัวเป็น optional** — ส่งเฉพาะค่าที่ต้องการเปลี่ยน ที่เหลือใช้ค่า default
579
+ (object ซ้อนจะ merge ทีละ field, array จะ **แทนที่** ทั้ง array)
580
+
581
+ ```ts
582
+ import { ConfigInput } from '@jnpt/nest-logging';
583
+
584
+ const config: ConfigInput = {
585
+ applicationName: 'my-service', // field "application"
586
+ enabled: true, // ปิดทั้งหมด = false
587
+ minLevel: 'INFO', // TRACE | DEBUG | INFO | WARN | ERROR (ใช้กับ application log)
588
+ requestLogging: {
589
+ enabled: true, // ปิด log ประเภท request (incoming + outgoing)
590
+ logHeaders: true,
591
+ logBody: true, // request body
592
+ logResponseBody: true,
593
+ maxBodySize: 10240, // bytes, ใช้ตัดข้อความที่ไม่ใช่ JSON
594
+ excludePatterns: ['/health', '/metrics', '/favicon.ico'],
595
+ includePatterns: [], // ถ้ากำหนด จะ log เฉพาะ path ที่ตรง
596
+ },
597
+ applicationLogging: {
598
+ enabled: true, // ปิด log ประเภท application
599
+ includeStackTrace: true,
600
+ maxStackTraceDepth: 50,
601
+ },
602
+ maskedHeaders: ['Authorization', 'X-Api-Key', 'Cookie', 'Set-Cookie'],
603
+ maskedFields: ['password', 'secret', 'token', 'creditCard', 'ssn'],
604
+ maskValue: '***MASKED***',
605
+ };
606
+ ```
607
+
608
+ ### Default Values
609
+
610
+ | Property | Default |
611
+ |---|---|
612
+ | `applicationName` | `"application"` |
613
+ | `enabled` | `true` |
614
+ | `minLevel` | `"DEBUG"` |
615
+ | `requestLogging.enabled` | `true` |
616
+ | `requestLogging.logHeaders` | `true` |
617
+ | `requestLogging.logBody` | `true` |
618
+ | `requestLogging.logResponseBody` | `true` |
619
+ | `requestLogging.maxBodySize` | `10240` |
620
+ | `requestLogging.excludePatterns` | `["/health", "/metrics", "/favicon.ico"]` |
621
+ | `requestLogging.includePatterns` | `[]` |
622
+ | `applicationLogging.enabled` | `true` |
623
+ | `applicationLogging.includeStackTrace` | `true` |
624
+ | `applicationLogging.maxStackTraceDepth` | `50` |
625
+ | `maskedHeaders` | `["Authorization", "X-Api-Key", "Cookie", "Set-Cookie"]` |
626
+ | `maskedFields` | `["password", "secret", "token", "creditCard", "ssn"]` |
627
+ | `maskValue` | `"***MASKED***"` |
628
+
629
+ ### Path patterns (`excludePatterns` / `includePatterns`)
630
+
631
+ | Pattern | ตรงกับ |
632
+ |---|---|
633
+ | `/health` | ตรงตัวเท่านั้น (`/health`) |
634
+ | `/api/*` | ทุก path ที่ขึ้นต้นด้วย `/api` (prefix) |
635
+ | `/api/**` | ทุก path ที่ขึ้นต้นด้วย `/api` (prefix) |
636
+
637
+ ไม่รองรับ glob/regex แบบเต็ม (เหมือน Go) — ตรวจ `excludePatterns` ก่อนเสมอ แล้วจึงตรวจ `includePatterns`
638
+ path ที่ใช้เทียบคือ path เต็ม (รวม global prefix เช่น `/v1/health` ถ้าใช้ `app.setGlobalPrefix('v1')`)
639
+
640
+ ### ตั้ง config จาก environment variable
641
+
642
+ ```ts
643
+ // app.module.ts
644
+ LoggingModule.forRoot({
645
+ applicationName: process.env.APP_NAME ?? 'my-service',
646
+ minLevel: process.env.LOG_LEVEL ?? 'INFO',
647
+ requestLogging: { logResponseBody: process.env.NODE_ENV !== 'production' },
648
+ });
649
+ ```
650
+
651
+ ### ตั้ง config ตอนรันไทม์
652
+
653
+ ```ts
654
+ import { configureDefaults, getDefaultConfig } from '@jnpt/nest-logging';
655
+
656
+ configureDefaults({ minLevel: 'WARN' }); // logger ทุกตัวที่สร้างด้วย new AppLogger(name) จะใช้ค่าใหม่ทันที
657
+ getDefaultConfig().minLevel; // 'WARN'
658
+ ```
659
+
660
+ ---
661
+
662
+ ## รูปแบบ Log JSON
663
+
664
+ ### Application log
665
+
666
+ ```json
667
+ {
668
+ "@timestamp": "2026-10-25T15:42:45.614+07:00",
669
+ "@version": "1",
670
+ "application": "demo-service",
671
+ "message": "Creating user",
672
+ "logger_name": "UserHandler",
673
+ "thread_name": "main",
674
+ "level": "INFO",
675
+ "level_value": 20000,
676
+ "type": "application",
677
+ "correlation_id": "9b0f069a-d73d-4a1e-a131-cedbb2ae320f",
678
+ "method": "",
679
+ "uri": "",
680
+ "request_body": {},
681
+ "response_body": {},
682
+ "extra": { "email": "a@b.c" }
683
+ }
684
+ ```
685
+
686
+ ### Request log (incoming)
687
+
688
+ ```json
689
+ {
690
+ "@timestamp": "2026-10-25T15:42:45.614+07:00",
691
+ "@version": "1",
692
+ "application": "demo-service",
693
+ "message": "Incoming request",
694
+ "logger_name": "LoggingMiddleware",
695
+ "thread_name": "main",
696
+ "level": "INFO",
697
+ "level_value": 20000,
698
+ "type": "request",
699
+ "correlation_id": "9b0f069a-d73d-4a1e-a131-cedbb2ae320f",
700
+ "method": "POST",
701
+ "uri": "/api/users",
702
+ "status_code": 201,
703
+ "duration_ms": 3,
704
+ "remote_address": "::1",
705
+ "user_agent": "curl/8.7.1",
706
+ "request_body": {
707
+ "headers": { "Host": "localhost:8080", "Content-Type": "application/json", "Authorization": "***MASKED***" },
708
+ "query_params": { "a": "1, 2" },
709
+ "body": { "email": "a@b.c", "password": "***MASKED***" },
710
+ "content_type": "application/json",
711
+ "content_length": 40
712
+ },
713
+ "response_body": {
714
+ "headers": { "X-Correlation-Id": "9b0f069a-...", "Content-Type": "application/json; charset=utf-8" },
715
+ "body": { "status": "success", "data": { "email": "a@b.c", "password": "***MASKED***", "id": "123" } },
716
+ "content_type": "application/json; charset=utf-8",
717
+ "content_length": 104
718
+ }
719
+ }
720
+ ```
721
+
722
+ Outgoing log เหมือนกันแต่ `message` = `"Outgoing request"`, `uri` = URL เต็ม, ไม่มี `remote_address`/`user_agent`
723
+ และมี `"extra": { "direction": "outgoing" }`
724
+
725
+ ### คำอธิบาย Field
726
+
727
+ | Field | หมายเหตุ |
728
+ |---|---|
729
+ | `@timestamp` | ISO 8601 + timezone ของเครื่อง (`YYYY-MM-DDTHH:mm:ss.SSS+07:00`) |
730
+ | `@version` | `"1"` เสมอ |
731
+ | `application` | `config.applicationName` |
732
+ | `logger_name` | ชื่อที่ส่งให้ `new AppLogger(name)` |
733
+ | `thread_name` | `main` (หรือ `worker-N` ถ้ารันใน worker thread) |
734
+ | `level` / `level_value` | TRACE=5000 DEBUG=10000 INFO=20000 WARN=30000 ERROR=40000 |
735
+ | `type` | `"application"` หรือ `"request"` |
736
+ | `correlation_id` | จาก async context (`""` ถ้าไม่มี) |
737
+ | `method`, `uri` | มีค่าเฉพาะ request log (application log เป็น `""`) |
738
+ | `status_code`, `duration_ms` | เฉพาะ request log |
739
+ | `remote_address`, `user_agent` | เฉพาะ incoming request log |
740
+ | `request_body`, `response_body` | `headers`, `query_params`, `path_params`, `body`, `content_type`, `content_length` (application log เป็น `{}`) |
741
+ | `extra` | extra ที่ merge แล้ว (ไม่มี key ถ้าว่าง) |
742
+ | `error` | `class`, `message`, `stack_trace` (เฉพาะ `warnError`/`errorWithErr`) |
743
+
744
+ ชื่อ header ใน log ถูกจัดรูปแบบเหมือน Go (`content-type` → `Content-Type`, `x-correlation-id` → `X-Correlation-Id`)
745
+
746
+ ---
747
+
748
+ ## API Reference
749
+
750
+ | Export | ประเภท | เทียบกับ Go |
751
+ |---|---|---|
752
+ | `AppLogger` | class | `logger.AppLogger` |
753
+ | `new AppLogger(name)` | constructor | `logger.New(name)` |
754
+ | `new AppLogger(name, config)` | constructor | `logger.NewWithConfig(name, config)` |
755
+ | `configureDefaults(config)` | function | `logger.ConfigureDefaults` |
756
+ | `getDefaultConfig()` | function | `logger.GetDefaultConfig` |
757
+ | `defaultConfig()` | function | `logger.DefaultConfig` |
758
+ | `buildConfig(partial?)` | function | — (merge partial เข้ากับ default) |
759
+ | `LevelValue` | const | `logger.LevelValue` |
760
+ | `formatTimestamp(date)` | function | `logger.FormatTimestamp` |
761
+ | `MaskingUtil` | class | `logger.MaskingUtil` |
762
+ | `CorrelationIDHeader` | const | `logger.CorrelationIDHeader` |
763
+ | `withCorrelationId(id, fn)` | function | `logger.WithCorrelationID` |
764
+ | `getCorrelationId()` | function | `logger.GetCorrelationID` |
765
+ | `generateCorrelationId()` | function | `logger.GenerateCorrelationID` |
766
+ | `ensureCorrelationId()` | function | `logger.EnsureCorrelationID` |
767
+ | `mergeExtras(extras)` | function | `mergeExtras` (internal ใน Go) |
768
+ | `matchPattern` / `shouldSkip` | function | `matchPattern` / `shouldSkip` (internal ใน Go) |
769
+ | `LoggingModule.forRoot(config?)` | Nest module | `ConfigureDefaults` + `r.Use(LoggingMiddleware)` (เลือก Express/Fastify เอง) |
770
+ | `LoggingMiddleware` | Nest middleware class | `middleware.LoggingMiddleware(logger.New("LoggingMiddleware"))` |
771
+ | `createLoggingMiddleware(log, config?)` | function | `middleware.LoggingMiddleware(log, config)` (Express) |
772
+ | `createFastifyLoggingPlugin(log, config?)` | function | `middleware.LoggingMiddleware(log, config)` (Fastify plugin) |
773
+ | `registerFastifyLogging(fastify, log, config?)` | function | เหมือนด้านบน แต่เรียก `addHook` ตรงๆ |
774
+ | `HttpClientModule.register(options)` / `.registerAsync(...)` | Nest dynamic module | — (provide `HttpClient`) |
775
+ | `HttpClient` | injectable class | `httpclient.NewClient(log, config)` |
776
+ | `createLoggingFetch(log, config?, baseFetch?)` | function | `httpclient.NewClient(log, config)` |
777
+ | `LogEntry`, `ErrorInfo`, `RequestInfo`, `ResponseInfo`, `Config`, `ConfigInput`, ... | types | struct เดียวกัน |
778
+
779
+ ---
780
+
781
+ ## เทียบกับ Go version
782
+
783
+ | เรื่อง | Go | NestJS |
784
+ |---|---|---|
785
+ | ส่ง context | `log.Info(ctx, "msg")` | `log.info('msg')` — correlation ID มาจาก `AsyncLocalStorage` อัตโนมัติ |
786
+ | ชื่อ method | `Info`, `ErrorWithErr` | `info`, `errorWithErr` (camelCase) |
787
+ | ชื่อ config | `RequestLogging.MaxBodySize` | `requestLogging.maxBodySize` |
788
+ | Middleware | Gin `r.Use(...)` | `LoggingModule.forRoot()` (Express หรือ Fastify อัตโนมัติ) / `createLoggingMiddleware()` / `createFastifyLoggingPlugin()` |
789
+ | HTTP client | `*http.Client` | `HttpClient` (inject ผ่าน `HttpClientModule`) หรือ `fetch` function |
790
+ | `thread_name` | `goroutine-N` | `main` / `worker-N` |
791
+ | `error.class` | `%T` เช่น `*errors.errorString` | ชื่อ class เช่น `Error`, `TypeError` |
792
+ | `stack_trace` | stack ณ จุดที่เรียก log | stack ของ error ที่ส่งเข้ามา (มีประโยชน์กว่า) |
793
+ | default config | อ่านตอน `New()` | อ่านทุกครั้งที่ log — logger ที่สร้างก่อน `configureDefaults` ก็ได้ค่าใหม่ |
794
+ | `NewClient(log, nil)` | panic (config nil) | ใช้ default config |
795
+ | query string ใน log | ไม่ mask | mask ด้วย `maskedFields` (`query_params` และ URL ขาออก) |
796
+ | ลำดับ key ใน object ย่อย | เรียงตามตัวอักษร (Go map) | ตามลำดับที่ได้รับ (ค่าเหมือนกัน) |
797
+
798
+ **ไม่รองรับ:** การแปลง log ภายในของ Nest
799
+ (`[Nest] ... LOG [RoutesResolver]`) เป็น JSON — ถ้าต้องการปิด log เหล่านั้นใช้ `NestFactory.create(AppModule, { logger: ['error', 'warn'] })`
800
+
801
+ ---
802
+
803
+ ## Development
804
+
805
+ ```bash
806
+ npm install
807
+ npm run build # tsc → dist/
808
+ npm test # compile + node --test (unit + NestJS e2e บน Express และ Fastify + Fastify ล้วน + fetch + HttpClientModule)
809
+ npm run example # รัน examples/nest-example บน Express ที่ port 8080
810
+ npm run example:fastify # รัน example เดียวกันบน Fastify
811
+ ```
812
+
813
+ ทดลองยิง example:
814
+
815
+ ```bash
816
+ curl 'localhost:8080/api/users?a=1&a=2' -H 'Authorization: Bearer x'
817
+ curl localhost:8080/api/users/42
818
+ curl -XPOST localhost:8080/api/users -H 'content-type: application/json' -d '{"email":"a@b.c","password":"p"}'
819
+ curl -XPOST localhost:8080/api/users -H 'content-type: application/json' -H 'X-Correlation-ID: my-id' -d '{}'
820
+ curl localhost:8080/api/external
821
+ curl localhost:8080/health # ไม่ถูก log (excludePatterns)
822
+ ```
823
+
824
+ ### โครงสร้าง
825
+
826
+ ```
827
+ src/
828
+ logger/ # AppLogger, Config, LogEntry, MaskingUtil, correlation ID (AsyncLocalStorage)
829
+ middleware/ # LoggingMiddleware, createLoggingMiddleware (incoming, Express)
830
+ fastify/ # createFastifyLoggingPlugin, registerFastifyLogging (incoming, Fastify)
831
+ httpclient/ # HttpClient, HttpClientModule, createLoggingFetch (outgoing)
832
+ internal/ # incoming request tracker (ใช้ร่วม Express/Fastify), pattern matching, header/body helpers
833
+ logging.module.ts
834
+ examples/nest-example/ # เทียบเท่า examples/gin-example ของ Go (main.ts = Express, main-fastify.ts = Fastify)
835
+ test/ # node:test — unit + e2e (ผ่านบน NestJS 10/11/12, Express 4/5, Fastify 4/5)
836
+ ```
837
+
838
+ ## License
839
+
840
+ MIT