observability-kit 0.1.1
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 +661 -0
- package/dist/core/context/request-context.d.ts +9 -0
- package/dist/core/context/request-context.d.ts.map +1 -0
- package/dist/core/context/request-context.js +46 -0
- package/dist/core/context/request-context.js.map +1 -0
- package/dist/core/logger/adapters/adapter.interface.d.ts +5 -0
- package/dist/core/logger/adapters/adapter.interface.d.ts.map +1 -0
- package/dist/core/logger/adapters/adapter.interface.js +3 -0
- package/dist/core/logger/adapters/adapter.interface.js.map +1 -0
- package/dist/core/logger/adapters/node.adapter.d.ts +6 -0
- package/dist/core/logger/adapters/node.adapter.d.ts.map +1 -0
- package/dist/core/logger/adapters/node.adapter.js +12 -0
- package/dist/core/logger/adapters/node.adapter.js.map +1 -0
- package/dist/core/logger/adapters/pino.adapter.d.ts +18 -0
- package/dist/core/logger/adapters/pino.adapter.d.ts.map +1 -0
- package/dist/core/logger/adapters/pino.adapter.js +83 -0
- package/dist/core/logger/adapters/pino.adapter.js.map +1 -0
- package/dist/core/logger/structured-logger.d.ts +25 -0
- package/dist/core/logger/structured-logger.d.ts.map +1 -0
- package/dist/core/logger/structured-logger.js +47 -0
- package/dist/core/logger/structured-logger.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/nest/adapters/nest.adapter.d.ts +16 -0
- package/dist/nest/adapters/nest.adapter.d.ts.map +1 -0
- package/dist/nest/adapters/nest.adapter.js +58 -0
- package/dist/nest/adapters/nest.adapter.js.map +1 -0
- package/dist/nest/decorators/request-log.decorator.d.ts +3 -0
- package/dist/nest/decorators/request-log.decorator.d.ts.map +1 -0
- package/dist/nest/decorators/request-log.decorator.js +41 -0
- package/dist/nest/decorators/request-log.decorator.js.map +1 -0
- package/dist/nest/decorators/service-log.decorator.d.ts +3 -0
- package/dist/nest/decorators/service-log.decorator.d.ts.map +1 -0
- package/dist/nest/decorators/service-log.decorator.js +41 -0
- package/dist/nest/decorators/service-log.decorator.js.map +1 -0
- package/dist/nest/decorators/step-log.decorator.d.ts +3 -0
- package/dist/nest/decorators/step-log.decorator.d.ts.map +1 -0
- package/dist/nest/decorators/step-log.decorator.js +36 -0
- package/dist/nest/decorators/step-log.decorator.js.map +1 -0
- package/dist/nest/interceptors/request.interceptor.d.ts +11 -0
- package/dist/nest/interceptors/request.interceptor.d.ts.map +1 -0
- package/dist/nest/interceptors/request.interceptor.js +64 -0
- package/dist/nest/interceptors/request.interceptor.js.map +1 -0
- package/dist/nest/module/trace-logger.module.d.ts +8 -0
- package/dist/nest/module/trace-logger.module.d.ts.map +1 -0
- package/dist/nest/module/trace-logger.module.js +38 -0
- package/dist/nest/module/trace-logger.module.js.map +1 -0
- package/dist/nest/tokens.d.ts +2 -0
- package/dist/nest/tokens.d.ts.map +1 -0
- package/dist/nest/tokens.js +5 -0
- package/dist/nest/tokens.js.map +1 -0
- package/dist/nestjs.d.ts +8 -0
- package/dist/nestjs.d.ts.map +1 -0
- package/dist/nestjs.js +32 -0
- package/dist/nestjs.js.map +1 -0
- package/dist/types/options.d.ts +39 -0
- package/dist/types/options.d.ts.map +1 -0
- package/dist/types/options.js +3 -0
- package/dist/types/options.js.map +1 -0
- package/package.json +123 -0
package/README.md
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
# observability-kit
|
|
2
|
+
|
|
3
|
+
Structured logging + request tracing for **NestJS** and **plain Node.js**.
|
|
4
|
+
|
|
5
|
+
Automatic `requestId` correlation across controllers, services and internal steps — no manual ID passing.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install observability-kit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Peer deps (NestJS only):
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @nestjs/common @nestjs/core rxjs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Entry points
|
|
24
|
+
|
|
25
|
+
| Import | Contents |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `observability-kit` | Core — context helpers + logger |
|
|
28
|
+
| `observability-kit/nestjs` | Core + NestJS module, decorators and interceptor |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
HTTP Request
|
|
36
|
+
↓
|
|
37
|
+
TraceInterceptor reads / generates requestId
|
|
38
|
+
↓
|
|
39
|
+
AsyncLocalStorage context requestId lives here for the full async chain
|
|
40
|
+
↓
|
|
41
|
+
@RequestLog emits request.start / request.end
|
|
42
|
+
↓
|
|
43
|
+
@ServiceLog emits service.enter / service.exit
|
|
44
|
+
↓
|
|
45
|
+
@StepLog emits step.<name>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Every log line carries the same `requestId` automatically.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## NestJS setup
|
|
53
|
+
|
|
54
|
+
### 1. Register the module
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// app.module.ts
|
|
58
|
+
import { Module } from '@nestjs/common';
|
|
59
|
+
import { TraceLoggerModule } from 'observability-kit/nestjs';
|
|
60
|
+
|
|
61
|
+
@Module({
|
|
62
|
+
imports: [
|
|
63
|
+
TraceLoggerModule.forRoot({
|
|
64
|
+
serviceName: 'my-api',
|
|
65
|
+
requestIdHeaderName: 'x-request-id', // default
|
|
66
|
+
enableResponseHeader: true,
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
export class AppModule {}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. Register the interceptor globally
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// main.ts
|
|
77
|
+
import { NestFactory } from '@nestjs/core';
|
|
78
|
+
import { AppModule } from './app.module';
|
|
79
|
+
import { TraceInterceptor } from 'observability-kit/nestjs';
|
|
80
|
+
import { logger, PinoLogAdapter } from 'observability-kit';
|
|
81
|
+
|
|
82
|
+
async function bootstrap() {
|
|
83
|
+
logger.setAdapter(PinoLogAdapter.pretty()); // desarrollo — salida legible
|
|
84
|
+
// logger.setAdapter(new PinoLogAdapter()); // producción — JSON puro
|
|
85
|
+
|
|
86
|
+
const app = await NestFactory.create(AppModule);
|
|
87
|
+
app.useGlobalInterceptors(app.get(TraceInterceptor));
|
|
88
|
+
await app.listen(3000);
|
|
89
|
+
}
|
|
90
|
+
bootstrap();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Decorar controllers y services
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { Controller, Get, Param } from '@nestjs/common';
|
|
97
|
+
import { RequestLog, ServiceLog, StepLog } from 'observability-kit/nestjs';
|
|
98
|
+
|
|
99
|
+
@Controller('orders')
|
|
100
|
+
export class OrdersController {
|
|
101
|
+
constructor(private readonly ordersService: OrdersService) {}
|
|
102
|
+
|
|
103
|
+
@Get(':id')
|
|
104
|
+
@RequestLog()
|
|
105
|
+
findOne(@Param('id') id: string) {
|
|
106
|
+
return this.ordersService.findOne(id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Injectable()
|
|
111
|
+
export class OrdersService {
|
|
112
|
+
@ServiceLog()
|
|
113
|
+
async findOne(id: string) {
|
|
114
|
+
return this.calculateTotal(id);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@StepLog('calculate-total', {
|
|
118
|
+
payloadBuilder: (id: string) => ({ id }),
|
|
119
|
+
})
|
|
120
|
+
private calculateTotal(id: string) { /* ... */ }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Decoradores
|
|
127
|
+
|
|
128
|
+
### `@RequestLog(options?)`
|
|
129
|
+
|
|
130
|
+
Aplica en métodos de **controller**. Emite `request.start`, `request.end` y `request.error`.
|
|
131
|
+
|
|
132
|
+
| Opción | Tipo | Descripción |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `payloadBuilder` | `(...args) => object` | Extrae payload de los argumentos del método |
|
|
135
|
+
|
|
136
|
+
### `@ServiceLog(options?)`
|
|
137
|
+
|
|
138
|
+
Aplica en métodos de **service**. Emite `service.enter`, `service.exit` y `service.error`.
|
|
139
|
+
|
|
140
|
+
| Opción | Tipo | Default |
|
|
141
|
+
|---|---|---|
|
|
142
|
+
| `payloadBuilder` | `(...args) => object` | — |
|
|
143
|
+
| `level` | `LogLevel` | `'info'` |
|
|
144
|
+
| `logExit` | `boolean` | `true` |
|
|
145
|
+
|
|
146
|
+
### `@StepLog(stepName, options?)`
|
|
147
|
+
|
|
148
|
+
Aplica en métodos internos. Emite `step.<stepName>` y `step.<stepName>.error`.
|
|
149
|
+
|
|
150
|
+
| Opción | Tipo | Default |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `payloadBuilder` | `(...args) => object` | — |
|
|
153
|
+
| `level` | `LogLevel` | `'debug'` |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Logger y adaptadores
|
|
158
|
+
|
|
159
|
+
La librería usa **Pino** como backend por defecto.
|
|
160
|
+
|
|
161
|
+
### Desarrollo — una línea por evento, colores
|
|
162
|
+
|
|
163
|
+
Requiere `pino-pretty`:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm install -D pino-pretty
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { logger, PinoLogAdapter } from 'observability-kit';
|
|
171
|
+
|
|
172
|
+
logger.setAdapter(PinoLogAdapter.pretty());
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Output:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
[06:14:42] INFO: request.start | endpoint=/orders/1 | method=GET | req=46cfd3ae
|
|
179
|
+
[06:14:42] INFO: service.enter | service=OrdersService.findOne | req=46cfd3ae
|
|
180
|
+
[06:14:42] DEBUG: step.calculate-total | service=OrdersService.calculateTotal | req=46cfd3ae | payload={"id":"1"}
|
|
181
|
+
[06:14:42] INFO: service.exit | service=OrdersService.findOne | req=46cfd3ae
|
|
182
|
+
[06:14:42] INFO: request.end | endpoint=/orders/1 | method=GET | req=46cfd3ae | payload={"durationMs":12}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Nivel mínimo:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
logger.setAdapter(PinoLogAdapter.pretty({ level: 'info' })); // oculta debug
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Producción — JSON puro, async non-blocking
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import pino from 'pino';
|
|
195
|
+
import { logger, PinoLogAdapter } from 'observability-kit';
|
|
196
|
+
|
|
197
|
+
const dest = pino.destination({ sync: false });
|
|
198
|
+
logger.setAdapter(new PinoLogAdapter({ destination: dest }));
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Adaptador personalizado
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
import type { ILogAdapter, LogLevel } from 'observability-kit';
|
|
205
|
+
import { logger } from 'observability-kit';
|
|
206
|
+
|
|
207
|
+
class MyAdapter implements ILogAdapter {
|
|
208
|
+
log(level: LogLevel, message: string, data: Record<string, unknown>): void {
|
|
209
|
+
// enviar a cualquier sistema
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
logger.setAdapter(new MyAdapter());
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Plain Node.js
|
|
219
|
+
|
|
220
|
+
Sin NestJS. Importa desde `observability-kit`.
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { startContext, logger } from 'observability-kit';
|
|
224
|
+
import http from 'http';
|
|
225
|
+
|
|
226
|
+
http.createServer(async (req, res) => {
|
|
227
|
+
await startContext(
|
|
228
|
+
{ requestId: req.headers['x-request-id'] as string, endpoint: req.url, method: req.method },
|
|
229
|
+
async () => {
|
|
230
|
+
logger.info('request.start', { service: 'http-server' });
|
|
231
|
+
res.end('OK');
|
|
232
|
+
logger.info('request.end', { service: 'http-server' });
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
}).listen(3000);
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Context helpers
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { getContext, getRequestId, setContextField } from 'observability-kit';
|
|
244
|
+
|
|
245
|
+
const ctx = getContext(); // { requestId, endpoint?, method?, service? }
|
|
246
|
+
const id = getRequestId(); // string | undefined
|
|
247
|
+
setContextField('service', 'payment-worker');
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Estructura del proyecto
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
src/
|
|
256
|
+
types/options.ts
|
|
257
|
+
core/
|
|
258
|
+
context/request-context.ts
|
|
259
|
+
logger/
|
|
260
|
+
structured-logger.ts
|
|
261
|
+
adapters/
|
|
262
|
+
adapter.interface.ts
|
|
263
|
+
node.adapter.ts
|
|
264
|
+
pino.adapter.ts
|
|
265
|
+
nest/
|
|
266
|
+
adapters/nest.adapter.ts
|
|
267
|
+
decorators/
|
|
268
|
+
request-log.decorator.ts
|
|
269
|
+
service-log.decorator.ts
|
|
270
|
+
step-log.decorator.ts
|
|
271
|
+
interceptors/request.interceptor.ts
|
|
272
|
+
module/trace-logger.module.ts
|
|
273
|
+
index.ts
|
|
274
|
+
nestjs.ts
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
MIT
|
|
282
|
+
|
|
283
|
+

|
|
284
|
+

|
|
285
|
+
|
|
286
|
+
## Status
|
|
287
|
+
|
|
288
|
+
Early version – feedback welcome.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { RequestLog, ServiceLog, StepLog } from 'observability-kit/nestjs';
|
|
294
|
+
|
|
295
|
+
@Controller('users')
|
|
296
|
+
export class UsersController {
|
|
297
|
+
@Get()
|
|
298
|
+
@RequestLog()
|
|
299
|
+
findAll() { … }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@Injectable()
|
|
303
|
+
export class UsersService {
|
|
304
|
+
@ServiceLog()
|
|
305
|
+
async findAll() { … }
|
|
306
|
+
|
|
307
|
+
@StepLog('fetch-from-db')
|
|
308
|
+
private async fetchFromDb() { … }
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Every log line above is automatically enriched with the same `requestId`—no manual passing required.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Why observability-kit?
|
|
317
|
+
|
|
318
|
+
Most logging libraries solve **logging**.
|
|
319
|
+
|
|
320
|
+
This library solves **request correlation** across controllers, services, and internal steps without passing IDs manually through the call chain.
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
HTTP Request
|
|
324
|
+
↓
|
|
325
|
+
TraceInterceptor ← reads / generates requestId
|
|
326
|
+
↓
|
|
327
|
+
AsyncLocalStorage context ← requestId lives here for the full async chain
|
|
328
|
+
↓
|
|
329
|
+
Controller (@RequestLog) ← logs request.start / request.end
|
|
330
|
+
↓
|
|
331
|
+
Service (@ServiceLog) ← logs service.enter / service.exit
|
|
332
|
+
↓
|
|
333
|
+
Step (@StepLog) ← logs step.<name>
|
|
334
|
+
↓
|
|
335
|
+
Structured JSON logs ← every line carries requestId, service, timestamp
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
With a single interceptor registration every downstream call—no matter how deep—carries the same `requestId` automatically.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Quick Start
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
npm install observability-kit
|
|
346
|
+
# NestJS peer deps:
|
|
347
|
+
npm install @nestjs/common @nestjs/core rxjs
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
// app.module.ts
|
|
352
|
+
import { TraceLoggerModule } from 'observability-kit/nestjs';
|
|
353
|
+
|
|
354
|
+
@Module({ imports: [TraceLoggerModule.forRoot({ serviceName: 'my-api' })] })
|
|
355
|
+
export class AppModule {}
|
|
356
|
+
|
|
357
|
+
// main.ts
|
|
358
|
+
const app = await NestFactory.create(AppModule);
|
|
359
|
+
app.useGlobalInterceptors(app.get(TraceInterceptor)); // ← one line
|
|
360
|
+
await app.listen(3000);
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
That's it. Every request now carries a correlated `requestId` through the full async chain.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## Features
|
|
368
|
+
|
|
369
|
+
- **Request-ID lifecycle** – reads `x-request-id` header or auto-generates a UUID v4; stored in `AsyncLocalStorage` for the full async chain.
|
|
370
|
+
- **Structured logs** – every entry is a plain JSON object `{ requestId, service, message, … }`. Payload is omitted when empty.
|
|
371
|
+
- **Pluggable adapters** – default writes to `stdout/stderr`; swap for pino, winston, or the built-in NestJS `Logger` with one line.
|
|
372
|
+
- **NestJS integration** – module, interceptor, and three method decorators.
|
|
373
|
+
- **Plain Node.js** – zero NestJS dependency; import from `observability-kit`.
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Install
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
npm install observability-kit
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Peer dependencies (only needed for NestJS integration):
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
npm install @nestjs/common @nestjs/core rxjs
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## Package entry points
|
|
392
|
+
|
|
393
|
+
| Import path | Contents |
|
|
394
|
+
|-----------------------------|----------------------------------|
|
|
395
|
+
| `observability-kit` | Core – context helpers + logger |
|
|
396
|
+
| `observability-kit/nestjs` | Core + NestJS module/decorators |
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## NestJS Setup
|
|
401
|
+
|
|
402
|
+
### 1 – Register the module
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { Module } from '@nestjs/common';
|
|
406
|
+
import { TraceLoggerModule } from 'observability-kit/nestjs';
|
|
407
|
+
|
|
408
|
+
@Module({
|
|
409
|
+
imports: [
|
|
410
|
+
TraceLoggerModule.forRoot({
|
|
411
|
+
serviceName: 'my-api',
|
|
412
|
+
requestIdHeaderName: 'x-request-id', // default
|
|
413
|
+
enableResponseHeader: true, // echo id back to client
|
|
414
|
+
}),
|
|
415
|
+
],
|
|
416
|
+
})
|
|
417
|
+
export class AppModule {}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### 2 – Register the interceptor globally
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// main.ts
|
|
424
|
+
import { NestFactory } from '@nestjs/core';
|
|
425
|
+
import { TraceInterceptor } from 'observability-kit/nestjs';
|
|
426
|
+
import { AppModule } from './app.module';
|
|
427
|
+
|
|
428
|
+
async function bootstrap() {
|
|
429
|
+
const app = await NestFactory.create(AppModule);
|
|
430
|
+
app.useGlobalInterceptors(app.get(TraceInterceptor));
|
|
431
|
+
await app.listen(3000);
|
|
432
|
+
}
|
|
433
|
+
bootstrap();
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
The interceptor automatically:
|
|
437
|
+
- Reads / generates the `requestId`.
|
|
438
|
+
- Stores `{ requestId, endpoint, method }` in `AsyncLocalStorage`.
|
|
439
|
+
- Optionally sets the `x-request-id` response header.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## NestJS Decorators
|
|
444
|
+
|
|
445
|
+
### `@RequestLog(options?)`
|
|
446
|
+
|
|
447
|
+
Apply to **controller** methods. Emits `request.start`, `request.end` (with `durationMs`), and `request.error`.
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
import { Controller, Get, Param } from '@nestjs/common';
|
|
451
|
+
import { RequestLog } from 'observability-kit/nestjs';
|
|
452
|
+
|
|
453
|
+
@Controller('users')
|
|
454
|
+
export class UsersController {
|
|
455
|
+
@Get(':id')
|
|
456
|
+
@RequestLog({
|
|
457
|
+
payloadBuilder: (id: string) => ({ id }),
|
|
458
|
+
})
|
|
459
|
+
async findOne(@Param('id') id: string) {
|
|
460
|
+
return this.usersService.findOne(id);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
Emitted logs:
|
|
466
|
+
|
|
467
|
+
```json
|
|
468
|
+
{ "level": "info", "message": "request.start", "requestId": "…", "endpoint": "/users/42", "method": "GET", "service": "UsersController.findOne", "payload": { "id": "42" } }
|
|
469
|
+
{ "level": "info", "message": "request.end", "requestId": "…", "service": "UsersController.findOne", "payload": { "durationMs": 12 } }
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
### `@ServiceLog(options?)`
|
|
475
|
+
|
|
476
|
+
Apply to **service / use-case** methods. Emits `service.enter`, `service.exit`, and `service.error`.
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
import { Injectable } from '@nestjs/common';
|
|
480
|
+
import { ServiceLog } from 'observability-kit/nestjs';
|
|
481
|
+
|
|
482
|
+
@Injectable()
|
|
483
|
+
export class UsersService {
|
|
484
|
+
@ServiceLog({
|
|
485
|
+
payloadBuilder: (id: string) => ({ id }),
|
|
486
|
+
logExit: true, // default true
|
|
487
|
+
level: 'info', // default 'info'
|
|
488
|
+
})
|
|
489
|
+
async findOne(id: string) {
|
|
490
|
+
return this.repo.findById(id);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
### `@StepLog(stepName, options?)`
|
|
498
|
+
|
|
499
|
+
Apply to internal / domain step methods. Emits `step.<stepName>` (default level `debug`).
|
|
500
|
+
|
|
501
|
+
```typescript
|
|
502
|
+
import { StepLog } from 'observability-kit/nestjs';
|
|
503
|
+
|
|
504
|
+
export class PaymentService {
|
|
505
|
+
@StepLog('validate-amount', {
|
|
506
|
+
payloadBuilder: (amount: number) => ({ amount }),
|
|
507
|
+
level: 'debug', // default
|
|
508
|
+
})
|
|
509
|
+
private async validateAmount(amount: number) {
|
|
510
|
+
if (amount <= 0) throw new Error('Invalid amount');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Plain Node.js Setup
|
|
518
|
+
|
|
519
|
+
No NestJS required. Import from `observability-kit`.
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { startContext, logger } from 'observability-kit';
|
|
523
|
+
import http from 'http';
|
|
524
|
+
|
|
525
|
+
http.createServer(async (req, res) => {
|
|
526
|
+
const requestId = (req.headers['x-request-id'] as string) ?? undefined;
|
|
527
|
+
|
|
528
|
+
await startContext(
|
|
529
|
+
{
|
|
530
|
+
requestId,
|
|
531
|
+
endpoint: req.url,
|
|
532
|
+
method: req.method,
|
|
533
|
+
service: 'http-server',
|
|
534
|
+
},
|
|
535
|
+
async () => {
|
|
536
|
+
logger.info('request.start', { service: 'http-server' });
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
// handle request …
|
|
540
|
+
logger.debug('step.process', { service: 'http-server', payload: { url: req.url } });
|
|
541
|
+
res.end('OK');
|
|
542
|
+
logger.info('request.end', { service: 'http-server' });
|
|
543
|
+
} catch (err) {
|
|
544
|
+
logger.error('request.error', { service: 'http-server', payload: { error: (err as Error).message } });
|
|
545
|
+
res.statusCode = 500;
|
|
546
|
+
res.end('Error');
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
);
|
|
550
|
+
}).listen(3000);
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Emitted log format:
|
|
554
|
+
|
|
555
|
+
```json
|
|
556
|
+
{ "level": "info", "message": "request.start", "timestamp": "2026-03-01T00:00:00.000Z", "service": "http-server", "requestId": "f47ac10b-…", "endpoint": "/", "method": "GET" }
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
## Custom log adapter (pino example)
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
import pino from 'pino';
|
|
565
|
+
import { logger } from 'observability-kit';
|
|
566
|
+
import type { ILogAdapter, LogLevel } from 'observability-kit';
|
|
567
|
+
|
|
568
|
+
const pinoLogger = pino();
|
|
569
|
+
|
|
570
|
+
class PinoAdapter implements ILogAdapter {
|
|
571
|
+
log(level: LogLevel, _message: string, data: Record<string, unknown>): void {
|
|
572
|
+
pinoLogger[level](data);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Swap once at startup – affects all subsequent logger calls globally
|
|
577
|
+
logger.setAdapter(new PinoAdapter());
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## Context helpers
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
import { getContext, getRequestId, setContextField } from 'observability-kit';
|
|
586
|
+
|
|
587
|
+
// Anywhere inside an active context (ALS chain):
|
|
588
|
+
const ctx = getContext(); // { requestId, endpoint?, method?, service? }
|
|
589
|
+
const id = getRequestId(); // string | undefined
|
|
590
|
+
setContextField('service', 'payment-worker'); // mutate current context
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## Structured log entry shape
|
|
596
|
+
|
|
597
|
+
```
|
|
598
|
+
{
|
|
599
|
+
level: "debug" | "info" | "warn" | "error"
|
|
600
|
+
message: string // e.g. "request.start"
|
|
601
|
+
timestamp: string // ISO 8601
|
|
602
|
+
service: string // "ClassName.methodName"
|
|
603
|
+
requestId?: string // present when inside an ALS context
|
|
604
|
+
endpoint?: string // present in HTTP contexts
|
|
605
|
+
method?: string // present in HTTP contexts
|
|
606
|
+
payload?: object // omitted when empty / undefined
|
|
607
|
+
}
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Project structure
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
src/
|
|
616
|
+
types/
|
|
617
|
+
options.ts Shared TypeScript types + interfaces
|
|
618
|
+
core/
|
|
619
|
+
context/
|
|
620
|
+
request-context.ts AsyncLocalStorage – startContext / getContext / …
|
|
621
|
+
logger/
|
|
622
|
+
structured-logger.ts StructuredLogger class + `logger` singleton
|
|
623
|
+
adapters/
|
|
624
|
+
adapter.interface.ts ILogAdapter interface
|
|
625
|
+
node.adapter.ts Default adapter (stdout/stderr JSON lines)
|
|
626
|
+
nest/
|
|
627
|
+
adapters/
|
|
628
|
+
nest.adapter.ts NestJS Logger adapter
|
|
629
|
+
decorators/
|
|
630
|
+
request-log.decorator.ts @RequestLog
|
|
631
|
+
service-log.decorator.ts @ServiceLog
|
|
632
|
+
step-log.decorator.ts @StepLog
|
|
633
|
+
interceptors/
|
|
634
|
+
request.interceptor.ts TraceInterceptor (ALS context population)
|
|
635
|
+
module/
|
|
636
|
+
trace-logger.module.ts TraceLoggerModule.forRoot()
|
|
637
|
+
index.ts Core entry point (no NestJS dependency)
|
|
638
|
+
nestjs.ts NestJS entry point (core + NestJS integration)
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
## Build
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
npm run build # via NestJS CLI (nest build)
|
|
647
|
+
npm run build:tsc # via tsc directly
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## Roadmap
|
|
653
|
+
|
|
654
|
+
- [ ] OpenTelemetry support
|
|
655
|
+
- [ ] Fastify plugin
|
|
656
|
+
- [ ] Express middleware
|
|
657
|
+
- [ ] Performance benchmarks
|
|
658
|
+
|
|
659
|
+
## License
|
|
660
|
+
|
|
661
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { RequestContext, StartContextOptions } from '../../types/options.js';
|
|
3
|
+
export declare const requestContextStorage: AsyncLocalStorage<RequestContext>;
|
|
4
|
+
export declare function startContext(options: StartContextOptions, callback: () => Promise<unknown> | unknown): Promise<unknown>;
|
|
5
|
+
export declare function runWithContext<T>(context: RequestContext, fn: () => T): T;
|
|
6
|
+
export declare function getContext(): RequestContext | undefined;
|
|
7
|
+
export declare function getRequestId(): string | undefined;
|
|
8
|
+
export declare function setContextField<K extends keyof RequestContext>(key: K, value: RequestContext[K]): void;
|
|
9
|
+
//# sourceMappingURL=request-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../../../src/core/context/request-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,KAAK,EACV,cAAc,EACd,mBAAmB,EACpB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,qBAAqB,mCAA0C,CAAC;AAE7E,wBAAgB,YAAY,CAC1B,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,GACzC,OAAO,CAAC,OAAO,CAAC,CAkBlB;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEzE;AAED,wBAAgB,UAAU,IAAI,cAAc,GAAG,SAAS,CAEvD;AAED,wBAAgB,YAAY,IAAI,MAAM,GAAG,SAAS,CAEjD;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,cAAc,EAC5D,GAAG,EAAE,CAAC,EACN,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,GACvB,IAAI,CAKN"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.requestContextStorage = void 0;
|
|
4
|
+
exports.startContext = startContext;
|
|
5
|
+
exports.runWithContext = runWithContext;
|
|
6
|
+
exports.getContext = getContext;
|
|
7
|
+
exports.getRequestId = getRequestId;
|
|
8
|
+
exports.setContextField = setContextField;
|
|
9
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
exports.requestContextStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
12
|
+
function startContext(options, callback) {
|
|
13
|
+
const context = {
|
|
14
|
+
requestId: options.requestId ?? (0, node_crypto_1.randomUUID)(),
|
|
15
|
+
...(options.endpoint !== undefined ? { endpoint: options.endpoint } : {}),
|
|
16
|
+
...(options.method !== undefined ? { method: options.method } : {}),
|
|
17
|
+
...(options.service !== undefined ? { service: options.service } : {}),
|
|
18
|
+
};
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
exports.requestContextStorage.run(context, () => {
|
|
21
|
+
try {
|
|
22
|
+
const result = callback();
|
|
23
|
+
Promise.resolve(result).then(resolve).catch(reject);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
reject(err);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function runWithContext(context, fn) {
|
|
32
|
+
return exports.requestContextStorage.run(context, fn);
|
|
33
|
+
}
|
|
34
|
+
function getContext() {
|
|
35
|
+
return exports.requestContextStorage.getStore();
|
|
36
|
+
}
|
|
37
|
+
function getRequestId() {
|
|
38
|
+
return exports.requestContextStorage.getStore()?.requestId;
|
|
39
|
+
}
|
|
40
|
+
function setContextField(key, value) {
|
|
41
|
+
const store = exports.requestContextStorage.getStore();
|
|
42
|
+
if (store) {
|
|
43
|
+
store[key] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=request-context.js.map
|