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