@infineit/winston-logger 1.0.31 → 1.0.33
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 +1213 -101
- package/context/infrastructure/nestjs/contextModule.js +1 -10
- package/context/infrastructure/nestjs/contextModule.js.map +1 -1
- package/index.d.ts +5 -0
- package/index.js +10 -1
- package/index.js.map +1 -1
- package/logger/domain/loggerService.d.ts +3 -1
- package/logger/domain/loggerService.js +15 -2
- package/logger/domain/loggerService.js.map +1 -1
- package/logger/infrastructure/forwarding/centralLogForwarder.d.ts +15 -0
- package/logger/infrastructure/forwarding/centralLogForwarder.js +81 -0
- package/logger/infrastructure/forwarding/centralLogForwarder.js.map +1 -0
- package/logger/infrastructure/forwarding/httpCentralLogForwarder.d.ts +8 -0
- package/logger/infrastructure/forwarding/httpCentralLogForwarder.js +88 -0
- package/logger/infrastructure/forwarding/httpCentralLogForwarder.js.map +1 -0
- package/logger/infrastructure/forwarding/kafkaCentralLogForwarder.d.ts +9 -0
- package/logger/infrastructure/forwarding/kafkaCentralLogForwarder.js +50 -0
- package/logger/infrastructure/forwarding/kafkaCentralLogForwarder.js.map +1 -0
- package/logger/infrastructure/nestjs/loggerModule.d.ts +4 -0
- package/logger/infrastructure/nestjs/loggerModule.js +14 -0
- package/logger/infrastructure/nestjs/loggerModule.js.map +1 -1
- package/package.json +1 -1
- package/tsconfig.lib.tsbuildinfo +1 -1
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@ Enterprise-level logging library for NestJS applications with support for multip
|
|
|
6
6
|
|
|
7
7
|
- ✅ **Multi-Context Support**: Works in HTTP services, Kafka consumers, Bull/BullMQ workers, CRON jobs, and CLI tasks
|
|
8
8
|
- ✅ **Transport Architecture**: Pluggable transport system with Winston as one transport among many
|
|
9
|
+
- ✅ **Optional Central Forwarding**: Forward logs to centralized logging service via HTTP or Kafka (optional, fire-and-forget)
|
|
9
10
|
- ✅ **Log Normalization**: Automatic normalization and error serialization before transport
|
|
10
11
|
- ✅ **CLS Integration**: Correlation ID support via AsyncLocalStorage (CLS)
|
|
11
12
|
- ✅ **Fire-and-Forget**: Non-blocking, failure-safe logging that never breaks business logic
|
|
12
|
-
- ✅ **Zero Dependencies**: No database, HTTP framework, or ORM dependencies
|
|
13
|
+
- ✅ **Zero Dependencies**: No database, HTTP framework, or ORM dependencies (HTTP forwarding uses built-in Node.js modules)
|
|
13
14
|
- ✅ **Type-Safe**: Full TypeScript support with comprehensive type definitions
|
|
14
15
|
|
|
15
16
|
## Table of Contents
|
|
@@ -20,8 +21,12 @@ Enterprise-level logging library for NestJS applications with support for multip
|
|
|
20
21
|
- [Configuration](#configuration)
|
|
21
22
|
- [Usage in Different Contexts](#usage-in-different-contexts)
|
|
22
23
|
- [Transport Architecture](#transport-architecture)
|
|
24
|
+
- [Central Log Forwarding](#central-log-forwarding)
|
|
23
25
|
- [Log Normalization](#log-normalization)
|
|
24
26
|
- [Correlation ID (CLS)](#correlation-id-cls)
|
|
27
|
+
- [CLS Context Boundaries and Edge Cases](#cls-context-boundaries-and-edge-cases)
|
|
28
|
+
- [Correlation ID Format Validation](#correlation-id-format-validation)
|
|
29
|
+
- [Complete Integration Examples](#complete-integration-examples)
|
|
25
30
|
- [API Reference](#api-reference)
|
|
26
31
|
- [Migration Guide (Legacy app.useLogger Users)](#migration-guide-legacy-appuselogger-users)
|
|
27
32
|
- [Best Practices](#best-practices)
|
|
@@ -44,6 +49,27 @@ This package requires the following peer dependencies (usually already installed
|
|
|
44
49
|
- `@nestjs/config` 4.2.0
|
|
45
50
|
- `@nestjs/core` 11.1.9
|
|
46
51
|
- `nestjs-cls` 5.0.1
|
|
52
|
+
- `uuid` (required for correlation ID generation in HTTP contexts)
|
|
53
|
+
|
|
54
|
+
### ⚠️ Critical Setup Requirement
|
|
55
|
+
|
|
56
|
+
**IMPORTANT:** `ClsModule.forRoot()` **MUST** be imported in your `AppModule` **BEFORE** `ContextModule`. This is required to avoid dependency resolution errors with `HttpAdapterHost`.
|
|
57
|
+
|
|
58
|
+
**Correct import order:**
|
|
59
|
+
```typescript
|
|
60
|
+
@Module({
|
|
61
|
+
imports: [
|
|
62
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
63
|
+
ClsModule.forRoot({ ... }), // ✅ FIRST - Required
|
|
64
|
+
ContextModule, // ✅ THEN - Uses existing ClsModule
|
|
65
|
+
LoggerModule.forRoot(), // ✅ THEN - Configures logger
|
|
66
|
+
],
|
|
67
|
+
})
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**❌ Incorrect:** Importing `ContextModule` before `ClsModule` will cause `HttpAdapterHost` dependency errors.
|
|
71
|
+
|
|
72
|
+
**Note:** `ContextModule` does NOT import `ClsModule` internally. Applications must configure `ClsModule.forRoot()` themselves.
|
|
47
73
|
|
|
48
74
|
---
|
|
49
75
|
|
|
@@ -54,12 +80,26 @@ This package requires the following peer dependencies (usually already installed
|
|
|
54
80
|
```typescript
|
|
55
81
|
import { Module } from '@nestjs/common';
|
|
56
82
|
import { ConfigModule } from '@nestjs/config';
|
|
83
|
+
import { ClsModule } from 'nestjs-cls'; // REQUIRED: Must import ClsModule first
|
|
84
|
+
import { v4 as uuidv4 } from 'uuid'; // REQUIRED: For correlation ID generation
|
|
57
85
|
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
58
86
|
|
|
59
87
|
@Module({
|
|
60
88
|
imports: [
|
|
61
89
|
ConfigModule.forRoot({ isGlobal: true }),
|
|
62
|
-
|
|
90
|
+
|
|
91
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported BEFORE ContextModule
|
|
92
|
+
ClsModule.forRoot({
|
|
93
|
+
global: true,
|
|
94
|
+
middleware: {
|
|
95
|
+
mount: true, // Enable middleware for HTTP requests
|
|
96
|
+
generateId: true, // Auto-generate correlation ID
|
|
97
|
+
idGenerator: (req: Request) =>
|
|
98
|
+
(req.headers['x-correlation-id'] as string) || uuidv4(),
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
|
|
102
|
+
ContextModule, // Required for CLS (correlation ID support) - uses existing ClsModule
|
|
63
103
|
LoggerModule.forRoot(), // Configure logger
|
|
64
104
|
],
|
|
65
105
|
})
|
|
@@ -169,10 +209,29 @@ const config: LoggerModuleConfig = {
|
|
|
169
209
|
organization: 'my-org', // Organization name (optional)
|
|
170
210
|
context: 'user-service', // Bounded context name (optional)
|
|
171
211
|
app: 'api-gateway', // Application name (optional)
|
|
212
|
+
// Optional: Central log forwarding (disabled by default)
|
|
213
|
+
forwardToCentral: false, // Enable forwarding to centralized logging service
|
|
214
|
+
transportType: 'http', // 'http' or 'kafka' (required if forwardToCentral=true)
|
|
215
|
+
httpEndpoint: 'https://...', // HTTP endpoint (required if transportType='http')
|
|
216
|
+
// kafkaTopic: 'project-logs', // Kafka topic (required if transportType='kafka')
|
|
172
217
|
};
|
|
173
218
|
|
|
219
|
+
import { ClsModule } from 'nestjs-cls';
|
|
220
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
221
|
+
|
|
174
222
|
@Module({
|
|
175
223
|
imports: [
|
|
224
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported BEFORE ContextModule
|
|
225
|
+
ClsModule.forRoot({
|
|
226
|
+
global: true,
|
|
227
|
+
middleware: {
|
|
228
|
+
mount: true,
|
|
229
|
+
generateId: true,
|
|
230
|
+
idGenerator: (req: Request) =>
|
|
231
|
+
(req.headers['x-correlation-id'] as string) || uuidv4(),
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
ContextModule, // Uses existing ClsModule
|
|
176
235
|
LoggerModule.forRoot(config),
|
|
177
236
|
],
|
|
178
237
|
})
|
|
@@ -192,6 +251,14 @@ LOG_IN_FILE=true
|
|
|
192
251
|
LOGGER_ORGANIZATION=my-org
|
|
193
252
|
LOGGER_CONTEXT=user-service
|
|
194
253
|
LOGGER_APP=api-gateway
|
|
254
|
+
|
|
255
|
+
# Optional: Central log forwarding (disabled by default)
|
|
256
|
+
LOGGER_FORWARD_TO_CENTRAL=false
|
|
257
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
258
|
+
LOGGER_HTTP_ENDPOINT=https://logging-service.example.com/api/logs
|
|
259
|
+
# OR for Kafka:
|
|
260
|
+
# LOGGER_TRANSPORT_TYPE=kafka
|
|
261
|
+
# LOGGER_KAFKA_TOPIC=project-logs-sales
|
|
195
262
|
```
|
|
196
263
|
|
|
197
264
|
```typescript
|
|
@@ -206,16 +273,35 @@ export default registerAs('logger', () => ({
|
|
|
206
273
|
organization: process.env.LOGGER_ORGANIZATION,
|
|
207
274
|
context: process.env.LOGGER_CONTEXT,
|
|
208
275
|
app: process.env.LOGGER_APP,
|
|
276
|
+
// Optional: Central log forwarding
|
|
277
|
+
forwardToCentral: process.env.LOGGER_FORWARD_TO_CENTRAL,
|
|
278
|
+
transportType: process.env.LOGGER_TRANSPORT_TYPE,
|
|
279
|
+
httpEndpoint: process.env.LOGGER_HTTP_ENDPOINT,
|
|
280
|
+
kafkaTopic: process.env.LOGGER_KAFKA_TOPIC,
|
|
209
281
|
}));
|
|
210
282
|
```
|
|
211
283
|
|
|
212
284
|
```typescript
|
|
285
|
+
import { ClsModule } from 'nestjs-cls';
|
|
286
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
287
|
+
|
|
213
288
|
@Module({
|
|
214
289
|
imports: [
|
|
215
290
|
ConfigModule.forRoot({
|
|
216
291
|
isGlobal: true,
|
|
217
292
|
load: [loggerConfig],
|
|
218
293
|
}),
|
|
294
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported BEFORE ContextModule
|
|
295
|
+
ClsModule.forRoot({
|
|
296
|
+
global: true,
|
|
297
|
+
middleware: {
|
|
298
|
+
mount: true,
|
|
299
|
+
generateId: true,
|
|
300
|
+
idGenerator: (req: Request) =>
|
|
301
|
+
(req.headers['x-correlation-id'] as string) || uuidv4(),
|
|
302
|
+
},
|
|
303
|
+
}),
|
|
304
|
+
ContextModule, // Uses existing ClsModule configuration
|
|
219
305
|
LoggerModule.forRoot(), // Will read from ConfigService
|
|
220
306
|
],
|
|
221
307
|
})
|
|
@@ -233,12 +319,52 @@ export class AppModule {}
|
|
|
233
319
|
| `organization` | `string` | No | `undefined` | Organization or project name |
|
|
234
320
|
| `context` | `string` | No | `undefined` | Bounded context name |
|
|
235
321
|
| `app` | `string` | No | `undefined` | Application or microservice name |
|
|
322
|
+
| `forwardToCentral` | `boolean \| string` | No | `false` | Enable forwarding to centralized logging service (`true`/`false` or `'true'`/`'false'`) |
|
|
323
|
+
| `transportType` | `'kafka' \| 'http'` | No | `undefined` | Transport type for central forwarding (required if `forwardToCentral=true`) |
|
|
324
|
+
| `httpEndpoint` | `string` | No | `undefined` | HTTP endpoint for central forwarding (required if `transportType='http'`) |
|
|
325
|
+
| `kafkaTopic` | `string` | No | `undefined` | Kafka topic for central forwarding (required if `transportType='kafka'`) |
|
|
236
326
|
|
|
237
327
|
### Default Behavior
|
|
238
328
|
|
|
239
329
|
- **Development/Testing**: Console and file logging enabled by default
|
|
240
330
|
- **Production**: Console and file logging disabled by default (unless explicitly enabled)
|
|
241
331
|
- **Slack**: Only enabled in production/testing when `slack_webhook` is provided
|
|
332
|
+
- **Central Forwarding**: Disabled by default (set `forwardToCentral=true` to enable)
|
|
333
|
+
|
|
334
|
+
### Environment-Specific Configuration Tips
|
|
335
|
+
|
|
336
|
+
**Development:**
|
|
337
|
+
```bash
|
|
338
|
+
# Disable forwarding in development (optional)
|
|
339
|
+
LOGGER_FORWARD_TO_CENTRAL=false
|
|
340
|
+
# OR use local endpoint for testing
|
|
341
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
342
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
343
|
+
LOGGER_HTTP_ENDPOINT=http://localhost:3000/api/logs
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Production:**
|
|
347
|
+
```bash
|
|
348
|
+
# Enable forwarding in production
|
|
349
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
350
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
351
|
+
LOGGER_HTTP_ENDPOINT=https://central-logging.example.com/api/logs
|
|
352
|
+
# OR use Kafka for better performance
|
|
353
|
+
# LOGGER_TRANSPORT_TYPE=kafka
|
|
354
|
+
# LOGGER_KAFKA_TOPIC=project-logs-<project-name>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
**Project-Specific Topics (Kafka):**
|
|
358
|
+
```bash
|
|
359
|
+
# Sales project
|
|
360
|
+
LOGGER_KAFKA_TOPIC=project-logs-sales
|
|
361
|
+
|
|
362
|
+
# Document project
|
|
363
|
+
LOGGER_KAFKA_TOPIC=project-logs-document
|
|
364
|
+
|
|
365
|
+
# Common service
|
|
366
|
+
LOGGER_KAFKA_TOPIC=project-logs-common
|
|
367
|
+
```
|
|
242
368
|
|
|
243
369
|
---
|
|
244
370
|
|
|
@@ -271,21 +397,26 @@ export class UserController {
|
|
|
271
397
|
}
|
|
272
398
|
```
|
|
273
399
|
|
|
274
|
-
**Important**: For HTTP services, you must configure CLS middleware manually
|
|
400
|
+
**Important**: For HTTP services, you must configure CLS middleware manually. **ClsModule MUST be imported BEFORE ContextModule:**
|
|
275
401
|
|
|
276
402
|
```typescript
|
|
277
403
|
import { ClsModule } from 'nestjs-cls';
|
|
404
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
405
|
+
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
278
406
|
|
|
279
407
|
@Module({
|
|
280
408
|
imports: [
|
|
409
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported FIRST
|
|
281
410
|
ClsModule.forRoot({
|
|
411
|
+
global: true,
|
|
282
412
|
middleware: {
|
|
283
413
|
mount: true, // Enable middleware
|
|
284
414
|
generateId: true, // Auto-generate correlation ID
|
|
285
|
-
idGenerator: (req: Request) =>
|
|
415
|
+
idGenerator: (req: Request) =>
|
|
416
|
+
(req.headers['x-correlation-id'] as string) || uuidv4(),
|
|
286
417
|
},
|
|
287
418
|
}),
|
|
288
|
-
ContextModule, // Provides CLS service
|
|
419
|
+
ContextModule, // Provides CLS service - uses existing ClsModule
|
|
289
420
|
LoggerModule.forRoot(),
|
|
290
421
|
],
|
|
291
422
|
})
|
|
@@ -478,8 +609,10 @@ export class KafkaTransport implements LoggerTransport {
|
|
|
478
609
|
### Registering Custom Transports
|
|
479
610
|
|
|
480
611
|
```typescript
|
|
481
|
-
import { Provider } from '@nestjs/common';
|
|
482
|
-
import {
|
|
612
|
+
import { Provider, Module } from '@nestjs/common';
|
|
613
|
+
import { ClsModule } from 'nestjs-cls';
|
|
614
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
615
|
+
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
483
616
|
import { KafkaTransport } from './kafka-transport';
|
|
484
617
|
|
|
485
618
|
const customTransports: Provider[] = [
|
|
@@ -491,6 +624,17 @@ const customTransports: Provider[] = [
|
|
|
491
624
|
|
|
492
625
|
@Module({
|
|
493
626
|
imports: [
|
|
627
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported BEFORE ContextModule
|
|
628
|
+
ClsModule.forRoot({
|
|
629
|
+
global: true,
|
|
630
|
+
middleware: {
|
|
631
|
+
mount: true,
|
|
632
|
+
generateId: true,
|
|
633
|
+
idGenerator: (req: Request) =>
|
|
634
|
+
(req.headers['x-correlation-id'] as string) || uuidv4(),
|
|
635
|
+
},
|
|
636
|
+
}),
|
|
637
|
+
ContextModule, // Uses existing ClsModule
|
|
494
638
|
LoggerModule.forRoot(config, customTransports),
|
|
495
639
|
],
|
|
496
640
|
})
|
|
@@ -509,6 +653,210 @@ All transports must:
|
|
|
509
653
|
|
|
510
654
|
---
|
|
511
655
|
|
|
656
|
+
## Central Log Forwarding
|
|
657
|
+
|
|
658
|
+
### Overview
|
|
659
|
+
|
|
660
|
+
The logger can optionally forward all logs to a centralized logging microservice via HTTP or Kafka. This is an **optional feature** that runs alongside existing logging (console, file, Slack) without modifying or affecting it.
|
|
661
|
+
|
|
662
|
+
**Key characteristics:**
|
|
663
|
+
- ✅ **Optional**: Only enabled when `forwardToCentral=true` (disabled by default)
|
|
664
|
+
- ✅ **Fire-and-forget**: Non-blocking, never throws, failures are swallowed
|
|
665
|
+
- ✅ **Preserves correlationId**: Automatically includes correlationId from CLS
|
|
666
|
+
- ✅ **Non-invasive**: Original logging (console, file, Slack) continues normally and is unaffected
|
|
667
|
+
|
|
668
|
+
### Configuration
|
|
669
|
+
|
|
670
|
+
**Via Environment Variables:**
|
|
671
|
+
```bash
|
|
672
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
673
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
674
|
+
LOGGER_HTTP_ENDPOINT=https://logging-service.example.com/api/logs
|
|
675
|
+
# OR for Kafka:
|
|
676
|
+
# LOGGER_TRANSPORT_TYPE=kafka
|
|
677
|
+
# LOGGER_KAFKA_TOPIC=project-logs-sales
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**Via Module Configuration:**
|
|
681
|
+
```typescript
|
|
682
|
+
import { ClsModule } from 'nestjs-cls';
|
|
683
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
684
|
+
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
685
|
+
|
|
686
|
+
@Module({
|
|
687
|
+
imports: [
|
|
688
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported BEFORE ContextModule
|
|
689
|
+
ClsModule.forRoot({
|
|
690
|
+
global: true,
|
|
691
|
+
middleware: {
|
|
692
|
+
mount: true,
|
|
693
|
+
generateId: true,
|
|
694
|
+
idGenerator: (req: Request) =>
|
|
695
|
+
(req.headers['x-correlation-id'] as string) || uuidv4(),
|
|
696
|
+
},
|
|
697
|
+
}),
|
|
698
|
+
ContextModule, // Uses existing ClsModule
|
|
699
|
+
LoggerModule.forRoot({
|
|
700
|
+
// ... existing config
|
|
701
|
+
forwardToCentral: true,
|
|
702
|
+
transportType: 'http', // or 'kafka'
|
|
703
|
+
httpEndpoint: 'https://logging-service.example.com/api/logs',
|
|
704
|
+
// OR for Kafka:
|
|
705
|
+
// kafkaTopic: 'project-logs-sales',
|
|
706
|
+
}),
|
|
707
|
+
],
|
|
708
|
+
})
|
|
709
|
+
export class AppModule {}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### HTTP Forwarding
|
|
713
|
+
|
|
714
|
+
Forwards logs via HTTP POST requests to the configured endpoint. Uses Node.js built-in `http`/`https` modules (no external dependencies).
|
|
715
|
+
|
|
716
|
+
**Configuration:**
|
|
717
|
+
```bash
|
|
718
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
719
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
720
|
+
LOGGER_HTTP_ENDPOINT=https://central-logging.example.com/api/logs
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
**Usage:** No code changes required. All logs are automatically forwarded with correlationId preserved.
|
|
724
|
+
|
|
725
|
+
**Request Format:**
|
|
726
|
+
- **Method**: POST
|
|
727
|
+
- **Headers**: `Content-Type: application/json`, `x-correlation-id: <correlationId>` (if available)
|
|
728
|
+
- **Body**: `NormalizedLog` JSON object
|
|
729
|
+
|
|
730
|
+
### Kafka Forwarding
|
|
731
|
+
|
|
732
|
+
Forwards logs to a configured Kafka topic. Requires a Kafka producer instance provided by your application.
|
|
733
|
+
|
|
734
|
+
**Prerequisites:** Provide Kafka producer via `KafkaProducerKey`:
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
import { Module, Provider } from '@nestjs/common';
|
|
738
|
+
import { Kafka } from 'kafkajs';
|
|
739
|
+
import { LoggerModule, KafkaProducerKey } from '@infineit/winston-logger';
|
|
740
|
+
|
|
741
|
+
@Module({
|
|
742
|
+
imports: [
|
|
743
|
+
LoggerModule.forRoot({
|
|
744
|
+
forwardToCentral: true,
|
|
745
|
+
transportType: 'kafka',
|
|
746
|
+
kafkaTopic: 'project-logs-sales',
|
|
747
|
+
}),
|
|
748
|
+
],
|
|
749
|
+
providers: [
|
|
750
|
+
{
|
|
751
|
+
provide: KafkaProducerKey,
|
|
752
|
+
useFactory: async () => {
|
|
753
|
+
const kafka = new Kafka({ brokers: ['localhost:9092'] });
|
|
754
|
+
const producer = kafka.producer();
|
|
755
|
+
await producer.connect();
|
|
756
|
+
return producer;
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
})
|
|
761
|
+
export class AppModule {}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
**Usage:** No code changes required. All logs are automatically forwarded with correlationId preserved.
|
|
765
|
+
|
|
766
|
+
**Message Format:**
|
|
767
|
+
- **Topic**: Configured `kafkaTopic`
|
|
768
|
+
- **Key**: `correlationId` or `'no-correlation-id'` if not available
|
|
769
|
+
- **Headers**: `x-correlation-id: <correlationId>` (if available)
|
|
770
|
+
- **Value**: `NormalizedLog` JSON string
|
|
771
|
+
|
|
772
|
+
### Correlation ID Preservation
|
|
773
|
+
|
|
774
|
+
Correlation ID is automatically preserved and forwarded:
|
|
775
|
+
|
|
776
|
+
**HTTP Forwarding:**
|
|
777
|
+
1. Correlation ID from CLS is included in log normalization
|
|
778
|
+
2. Forwarder sends log with `correlationId` in HTTP header (`x-correlation-id`) and body
|
|
779
|
+
|
|
780
|
+
**Kafka Forwarding:**
|
|
781
|
+
1. Correlation ID from CLS is included in log normalization
|
|
782
|
+
2. Forwarder sends log with `correlationId` as message key, header, and in message value
|
|
783
|
+
|
|
784
|
+
**No changes required:** If CLS is configured, correlationId is automatically included in forwarded logs.
|
|
785
|
+
|
|
786
|
+
### Fire-and-Forget Behavior
|
|
787
|
+
|
|
788
|
+
Central log forwarding is completely fire-and-forget:
|
|
789
|
+
|
|
790
|
+
- ✅ **Never blocks**: Forwarding happens asynchronously, never blocks business logic
|
|
791
|
+
- ✅ **Never throws**: All errors are swallowed, never propagate to application code
|
|
792
|
+
- ✅ **Never affects business logic**: Forwarding failures are logged to `console.error` only
|
|
793
|
+
- ✅ **Never breaks logging**: Original logging (console, file, Slack) continues normally and is unaffected
|
|
794
|
+
|
|
795
|
+
**Important:** Original logging behavior remains completely unchanged. Forwarding is an additional, optional layer that runs in parallel.
|
|
796
|
+
|
|
797
|
+
### Environment-Specific Configuration
|
|
798
|
+
|
|
799
|
+
**Development:**
|
|
800
|
+
```bash
|
|
801
|
+
# Option 1: Disable forwarding in development
|
|
802
|
+
LOGGER_FORWARD_TO_CENTRAL=false
|
|
803
|
+
|
|
804
|
+
# Option 2: Use local endpoint for testing
|
|
805
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
806
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
807
|
+
LOGGER_HTTP_ENDPOINT=http://localhost:3000/api/logs
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
**Production:**
|
|
811
|
+
```bash
|
|
812
|
+
# HTTP forwarding (simpler setup)
|
|
813
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
814
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
815
|
+
LOGGER_HTTP_ENDPOINT=https://central-logging.example.com/api/logs
|
|
816
|
+
|
|
817
|
+
# OR Kafka forwarding (better performance, requires Kafka producer)
|
|
818
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
819
|
+
LOGGER_TRANSPORT_TYPE=kafka
|
|
820
|
+
LOGGER_KAFKA_TOPIC=project-logs-<project-name>
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
**Project-Specific Topics (Kafka):**
|
|
824
|
+
Use different Kafka topics per project for better log organization:
|
|
825
|
+
```bash
|
|
826
|
+
# Sales project
|
|
827
|
+
LOGGER_KAFKA_TOPIC=project-logs-sales
|
|
828
|
+
|
|
829
|
+
# Document project
|
|
830
|
+
LOGGER_KAFKA_TOPIC=project-logs-document
|
|
831
|
+
|
|
832
|
+
# Common service
|
|
833
|
+
LOGGER_KAFKA_TOPIC=project-logs-common
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
### Troubleshooting
|
|
837
|
+
|
|
838
|
+
**Forwarding Not Working:**
|
|
839
|
+
1. Check configuration: `LOGGER_FORWARD_TO_CENTRAL` must be `'true'` or `true`
|
|
840
|
+
2. Verify transport type: Must be `'http'` or `'kafka'`
|
|
841
|
+
3. Check endpoint/topic: Must be configured correctly
|
|
842
|
+
4. For Kafka: Ensure `KafkaProducerKey` is provided in module providers
|
|
843
|
+
5. Check console errors: Forwarding errors are logged to `console.error` with message `"CentralLogForwarder forward error (swallowed): <error>"`
|
|
844
|
+
|
|
845
|
+
**Correlation ID Missing:**
|
|
846
|
+
- Ensure CLS is configured: `ClsModule.forRoot()` with `generateId: true`
|
|
847
|
+
- Ensure `ContextModule` is imported
|
|
848
|
+
- Check that original logs have `correlationId` field (if not, forwarded logs won't either)
|
|
849
|
+
|
|
850
|
+
**HTTP Forwarding Timeout:**
|
|
851
|
+
- HTTP forwarding has a 5-second timeout
|
|
852
|
+
- Timeouts are swallowed (no errors thrown)
|
|
853
|
+
- Consider using Kafka instead if central service is slow
|
|
854
|
+
- Or improve central service performance
|
|
855
|
+
|
|
856
|
+
**Note:** Forwarding failures do not affect your application. Original logging continues normally, and business logic is never impacted.
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
512
860
|
## Log Normalization
|
|
513
861
|
|
|
514
862
|
### Automatic Normalization
|
|
@@ -624,159 +972,913 @@ Correlation ID is **optional** - `undefined` is valid:
|
|
|
624
972
|
|
|
625
973
|
### HTTP Context Setup
|
|
626
974
|
|
|
627
|
-
For HTTP services, configure CLS middleware
|
|
975
|
+
For HTTP services, configure CLS middleware. **ClsModule MUST be imported BEFORE ContextModule:**
|
|
628
976
|
|
|
629
977
|
```typescript
|
|
630
978
|
import { ClsModule } from 'nestjs-cls';
|
|
631
979
|
import { v4 as uuidv4 } from 'uuid';
|
|
980
|
+
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
632
981
|
|
|
633
982
|
@Module({
|
|
634
983
|
imports: [
|
|
984
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported FIRST
|
|
635
985
|
ClsModule.forRoot({
|
|
986
|
+
global: true,
|
|
636
987
|
middleware: {
|
|
637
988
|
mount: true,
|
|
638
989
|
generateId: true,
|
|
639
990
|
idGenerator: (req: Request) => {
|
|
640
991
|
// Use existing correlation ID from header, or generate new one
|
|
641
|
-
return req.headers['x-correlation-id'] as string || uuidv4();
|
|
992
|
+
return (req.headers['x-correlation-id'] as string) || uuidv4();
|
|
642
993
|
},
|
|
643
994
|
},
|
|
644
995
|
}),
|
|
645
|
-
ContextModule, //
|
|
996
|
+
ContextModule, // Uses existing ClsModule configuration
|
|
646
997
|
LoggerModule.forRoot(),
|
|
647
998
|
],
|
|
648
999
|
})
|
|
649
1000
|
export class AppModule {}
|
|
650
1001
|
```
|
|
651
1002
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
## API Reference
|
|
1003
|
+
**Important:** The import order is critical. `ClsModule.forRoot()` must be imported before `ContextModule` to avoid `HttpAdapterHost` dependency resolution errors.
|
|
655
1004
|
|
|
656
|
-
###
|
|
1005
|
+
### CLS Context Boundaries and Edge Cases
|
|
657
1006
|
|
|
658
|
-
|
|
1007
|
+
#### Async Context Propagation
|
|
659
1008
|
|
|
660
|
-
|
|
1009
|
+
CLS (AsyncLocalStorage) automatically propagates correlation ID within the same async context chain:
|
|
661
1010
|
|
|
662
1011
|
```typescript
|
|
663
|
-
//
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
//
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
warn(message: string | Error, data?: LogData, profile?: string): void
|
|
670
|
-
error(message: string | Error, data?: LogData, profile?: string): void
|
|
671
|
-
fatal(message: string | Error, data?: LogData, profile?: string): void
|
|
672
|
-
emergency(message: string | Error, data?: LogData, profile?: string): void
|
|
673
|
-
|
|
674
|
-
// Profile (currently not implemented in transport architecture)
|
|
675
|
-
startProfile(id: string): void
|
|
1012
|
+
// ✅ Works: Same async context
|
|
1013
|
+
async handleRequest() {
|
|
1014
|
+
this.contextStorage.setContextId('abc-123');
|
|
1015
|
+
await this.serviceA.process(); // Can read correlationId
|
|
1016
|
+
await this.serviceB.process(); // Can read correlationId
|
|
1017
|
+
}
|
|
676
1018
|
```
|
|
677
1019
|
|
|
678
|
-
####
|
|
1020
|
+
#### Context Boundaries - Where Correlation ID is Lost
|
|
1021
|
+
|
|
1022
|
+
Correlation ID is **lost** when crossing async boundaries:
|
|
679
1023
|
|
|
680
1024
|
```typescript
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1025
|
+
// ❌ Lost: Event emitter
|
|
1026
|
+
this.contextStorage.setContextId('abc-123');
|
|
1027
|
+
eventEmitter.on('event', () => {
|
|
1028
|
+
this.logger.info('Log'); // correlationId: undefined
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// ❌ Lost: setTimeout/setInterval
|
|
1032
|
+
this.contextStorage.setContextId('abc-123');
|
|
1033
|
+
setTimeout(() => {
|
|
1034
|
+
this.logger.info('Log'); // correlationId: undefined
|
|
1035
|
+
}, 100);
|
|
1036
|
+
|
|
1037
|
+
// ❌ Lost: Manual promise without context preservation
|
|
1038
|
+
this.contextStorage.setContextId('abc-123');
|
|
1039
|
+
new Promise((resolve) => {
|
|
1040
|
+
setTimeout(resolve, 100);
|
|
1041
|
+
}).then(() => {
|
|
1042
|
+
this.logger.info('Log'); // correlationId: undefined
|
|
1043
|
+
});
|
|
689
1044
|
```
|
|
690
1045
|
|
|
691
|
-
####
|
|
1046
|
+
#### Solutions for Context Boundaries
|
|
1047
|
+
|
|
1048
|
+
**1. Event Emitters - Re-set correlationId:**
|
|
692
1049
|
|
|
693
1050
|
```typescript
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
correlationId?: string; // Override CLS correlation ID
|
|
700
|
-
error?: SerializedError; // Error (must be serialized)
|
|
701
|
-
props?: Record<string, any>; // Custom properties
|
|
702
|
-
durationMs?: number; // Duration in milliseconds
|
|
703
|
-
}
|
|
1051
|
+
// ✅ Solution: Set correlationId in event handler
|
|
1052
|
+
eventEmitter.on('event', async (data) => {
|
|
1053
|
+
this.contextStorage.setContextId(data.correlationId);
|
|
1054
|
+
this.logger.info('Processing event'); // correlationId available
|
|
1055
|
+
});
|
|
704
1056
|
```
|
|
705
1057
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
Service for managing CLS context (correlation ID).
|
|
709
|
-
|
|
710
|
-
#### Methods
|
|
1058
|
+
**2. Timers - Wrap in CLS.run():**
|
|
711
1059
|
|
|
712
1060
|
```typescript
|
|
713
|
-
|
|
714
|
-
|
|
1061
|
+
import { ClsService } from 'nestjs-cls';
|
|
1062
|
+
|
|
1063
|
+
// ✅ Solution: Use CLS.run() for timers
|
|
1064
|
+
const correlationId = this.contextStorage.getContextId();
|
|
1065
|
+
setTimeout(() => {
|
|
1066
|
+
this.clsService.run(async () => {
|
|
1067
|
+
this.clsService.set('CLS_ID', correlationId);
|
|
1068
|
+
this.logger.info('Log'); // correlationId available
|
|
1069
|
+
});
|
|
1070
|
+
}, 100);
|
|
1071
|
+
```
|
|
715
1072
|
|
|
716
|
-
|
|
717
|
-
setContextId(id: string): void
|
|
1073
|
+
**3. Worker Threads - Manual Propagation:**
|
|
718
1074
|
|
|
719
|
-
|
|
720
|
-
|
|
1075
|
+
```typescript
|
|
1076
|
+
// ✅ Solution: Pass correlationId explicitly to workers
|
|
1077
|
+
const correlationId = this.contextStorage.getContextId();
|
|
1078
|
+
worker.postMessage({ correlationId, data: ... });
|
|
721
1079
|
|
|
722
|
-
|
|
723
|
-
|
|
1080
|
+
worker.on('message', (msg) => {
|
|
1081
|
+
this.contextStorage.setContextId(msg.correlationId);
|
|
1082
|
+
this.logger.info('Processing'); // correlationId available
|
|
1083
|
+
});
|
|
724
1084
|
```
|
|
725
1085
|
|
|
726
|
-
|
|
1086
|
+
**4. Queues and Background Jobs - Always Re-set:**
|
|
727
1087
|
|
|
728
|
-
|
|
1088
|
+
```typescript
|
|
1089
|
+
// ✅ Solution: Extract and set at job start
|
|
1090
|
+
@Process('job')
|
|
1091
|
+
async handleJob(job: Job) {
|
|
1092
|
+
// Always set correlationId at job start
|
|
1093
|
+
const correlationId = job.data.correlationId || job.id.toString();
|
|
1094
|
+
this.contextStorage.setContextId(correlationId);
|
|
1095
|
+
|
|
1096
|
+
this.logger.info('Job started'); // correlationId available
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
729
1099
|
|
|
730
|
-
|
|
1100
|
+
#### CLS Failure Handling
|
|
731
1101
|
|
|
732
|
-
|
|
1102
|
+
The logger handles CLS failures gracefully:
|
|
733
1103
|
|
|
734
|
-
|
|
1104
|
+
```typescript
|
|
1105
|
+
// If getContextId() throws, logger continues with undefined
|
|
1106
|
+
try {
|
|
1107
|
+
const id = this.contextStorage.getContextId(); // May throw
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
// LoggerService.getLogData() catches this
|
|
1110
|
+
// Falls back to undefined correlationId
|
|
1111
|
+
}
|
|
735
1112
|
|
|
736
|
-
|
|
1113
|
+
this.logger.info('Log'); // Never throws, correlationId: undefined if CLS fails
|
|
1114
|
+
```
|
|
737
1115
|
|
|
738
|
-
|
|
1116
|
+
### Correlation ID Format Validation
|
|
739
1117
|
|
|
740
|
-
|
|
1118
|
+
The library does **not** validate correlation ID format. Applications should validate format before setting.
|
|
741
1119
|
|
|
742
|
-
|
|
1120
|
+
#### Recommended Format
|
|
743
1121
|
|
|
744
|
-
|
|
1122
|
+
**Standard UUID v4** (recommended for most cases):
|
|
745
1123
|
|
|
746
|
-
|
|
1124
|
+
```typescript
|
|
1125
|
+
import { v4 as uuidv4, validate as uuidValidate } from 'uuid';
|
|
1126
|
+
|
|
1127
|
+
// ✅ Validate before setting
|
|
1128
|
+
const correlationId = req.headers['x-correlation-id'] || uuidv4();
|
|
1129
|
+
if (uuidValidate(correlationId)) {
|
|
1130
|
+
this.contextStorage.setContextId(correlationId);
|
|
1131
|
+
} else {
|
|
1132
|
+
// Generate new one or use fallback
|
|
1133
|
+
this.contextStorage.setContextId(uuidv4());
|
|
1134
|
+
}
|
|
1135
|
+
```
|
|
747
1136
|
|
|
748
|
-
|
|
1137
|
+
**Custom Format Validation:**
|
|
749
1138
|
|
|
750
1139
|
```typescript
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
import { LoggerModule } from '@infineit/winston-logger';
|
|
1140
|
+
// Example: Custom format (prefix + timestamp + random)
|
|
1141
|
+
const CORRELATION_ID_PATTERN = /^req-[0-9]{13}-[a-z0-9]{8}$/;
|
|
754
1142
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
// ❌ DEPRECATED: app.useLogger() is removed
|
|
759
|
-
app.useLogger(app.get(LoggerModule));
|
|
760
|
-
|
|
761
|
-
await app.listen(3000);
|
|
762
|
-
|
|
763
|
-
// ❌ DEPRECATED: app.flushLogs() is removed
|
|
764
|
-
app.flushLogs();
|
|
1143
|
+
function validateCorrelationId(id: string): boolean {
|
|
1144
|
+
return CORRELATION_ID_PATTERN.test(id);
|
|
765
1145
|
}
|
|
766
1146
|
|
|
767
|
-
|
|
1147
|
+
// ✅ Validate before setting
|
|
1148
|
+
const correlationId = message.correlationId;
|
|
1149
|
+
if (correlationId && validateCorrelationId(correlationId)) {
|
|
1150
|
+
this.contextStorage.setContextId(correlationId);
|
|
1151
|
+
} else {
|
|
1152
|
+
// Generate new one with custom format
|
|
1153
|
+
const newId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 8)}`;
|
|
1154
|
+
this.contextStorage.setContextId(newId);
|
|
1155
|
+
}
|
|
768
1156
|
```
|
|
769
1157
|
|
|
770
|
-
|
|
1158
|
+
#### Format Guidelines
|
|
771
1159
|
|
|
772
|
-
|
|
773
|
-
|
|
1160
|
+
**Recommended:**
|
|
1161
|
+
- ✅ UUID v4: `550e8400-e29b-41d4-a716-446655440000`
|
|
1162
|
+
- ✅ Alphanumeric: `req-abc123def456`
|
|
1163
|
+
- ✅ With timestamp: `req-1640000000000-abc123`
|
|
1164
|
+
|
|
1165
|
+
**Avoid:**
|
|
1166
|
+
- ❌ Empty strings: `""`
|
|
1167
|
+
- ❌ Very long strings: > 255 characters
|
|
1168
|
+
- ❌ Special characters that break systems: `null`, `undefined` as string
|
|
1169
|
+
- ❌ Newlines or control characters
|
|
1170
|
+
|
|
1171
|
+
#### Validation in HTTP Middleware
|
|
1172
|
+
|
|
1173
|
+
```typescript
|
|
1174
|
+
import { ClsModule } from 'nestjs-cls';
|
|
1175
|
+
import { v4 as uuidv4, validate as uuidValidate } from 'uuid';
|
|
1176
|
+
|
|
1177
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported BEFORE ContextModule in AppModule
|
|
1178
|
+
ClsModule.forRoot({
|
|
1179
|
+
global: true,
|
|
1180
|
+
middleware: {
|
|
1181
|
+
mount: true,
|
|
1182
|
+
generateId: true,
|
|
1183
|
+
idGenerator: (req: Request) => {
|
|
1184
|
+
const headerId = req.headers['x-correlation-id'] as string;
|
|
1185
|
+
|
|
1186
|
+
// Validate format
|
|
1187
|
+
if (headerId && uuidValidate(headerId)) {
|
|
1188
|
+
return headerId;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Generate new one if invalid
|
|
1192
|
+
return uuidv4();
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
})
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
#### Validation in Kafka Consumers
|
|
1199
|
+
|
|
1200
|
+
```typescript
|
|
1201
|
+
@KafkaListener('order.created')
|
|
1202
|
+
async handleOrderCreated(message: OrderCreatedEvent) {
|
|
1203
|
+
// Validate before setting
|
|
1204
|
+
let correlationId = message.correlationId;
|
|
1205
|
+
|
|
1206
|
+
if (!correlationId || !uuidValidate(correlationId)) {
|
|
1207
|
+
// Use message ID as fallback or generate new
|
|
1208
|
+
correlationId = message.id || uuidv4();
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
this.contextStorage.setContextId(correlationId);
|
|
1212
|
+
this.logger.info('Processing order');
|
|
1213
|
+
}
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
---
|
|
1217
|
+
|
|
1218
|
+
## Complete Integration Examples
|
|
1219
|
+
|
|
1220
|
+
### HTTP Entry Point - Controller with Middleware
|
|
1221
|
+
|
|
1222
|
+
**app.module.ts:**
|
|
1223
|
+
|
|
1224
|
+
```typescript
|
|
1225
|
+
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
1226
|
+
import { ConfigModule } from '@nestjs/config';
|
|
1227
|
+
import { ClsModule } from 'nestjs-cls';
|
|
1228
|
+
import { v4 as uuidv4, validate as uuidValidate } from 'uuid';
|
|
1229
|
+
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
1230
|
+
|
|
1231
|
+
@Module({
|
|
1232
|
+
imports: [
|
|
1233
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
1234
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported FIRST
|
|
1235
|
+
ClsModule.forRoot({
|
|
1236
|
+
global: true,
|
|
1237
|
+
middleware: {
|
|
1238
|
+
mount: true,
|
|
1239
|
+
generateId: true,
|
|
1240
|
+
idGenerator: (req: Request) => {
|
|
1241
|
+
const headerId = req.headers['x-correlation-id'] as string;
|
|
1242
|
+
return (headerId && uuidValidate(headerId)) ? headerId : uuidv4();
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
}),
|
|
1246
|
+
ContextModule, // Uses existing ClsModule configuration
|
|
1247
|
+
LoggerModule.forRoot({
|
|
1248
|
+
nodeEnv: process.env.NODE_ENV,
|
|
1249
|
+
organization: 'sales',
|
|
1250
|
+
context: 'api',
|
|
1251
|
+
app: 'gateway',
|
|
1252
|
+
}),
|
|
1253
|
+
],
|
|
1254
|
+
})
|
|
1255
|
+
export class AppModule implements NestModule {
|
|
1256
|
+
configure(consumer: MiddlewareConsumer) {
|
|
1257
|
+
// Additional middleware can be added here
|
|
1258
|
+
// ClsModule middleware is already mounted above
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
**sales.controller.ts:**
|
|
1264
|
+
|
|
1265
|
+
```typescript
|
|
1266
|
+
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
|
1267
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1268
|
+
import { KafkaProducer } from './kafka-producer';
|
|
1269
|
+
|
|
1270
|
+
@Controller('sales')
|
|
1271
|
+
export class SalesController {
|
|
1272
|
+
constructor(
|
|
1273
|
+
private readonly logger: LoggerService,
|
|
1274
|
+
private readonly contextStorage: ContextStorageService,
|
|
1275
|
+
private readonly kafkaProducer: KafkaProducer,
|
|
1276
|
+
) {}
|
|
1277
|
+
|
|
1278
|
+
@Post('orders')
|
|
1279
|
+
async createOrder(@Body() orderData: CreateOrderDto) {
|
|
1280
|
+
// Correlation ID automatically available from CLS (set by middleware)
|
|
1281
|
+
this.logger.info('Creating order', {
|
|
1282
|
+
props: { userId: orderData.userId, amount: orderData.amount },
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
try {
|
|
1286
|
+
const order = await this.orderService.create(orderData);
|
|
1287
|
+
|
|
1288
|
+
// Extract correlationId from CLS before publishing to Kafka
|
|
1289
|
+
const correlationId = this.contextStorage.getContextId();
|
|
1290
|
+
|
|
1291
|
+
// Publish to Kafka with correlationId
|
|
1292
|
+
await this.kafkaProducer.send({
|
|
1293
|
+
topic: 'order.created',
|
|
1294
|
+
messages: [{
|
|
1295
|
+
key: order.id,
|
|
1296
|
+
value: JSON.stringify({
|
|
1297
|
+
...order,
|
|
1298
|
+
correlationId, // Propagate correlationId
|
|
1299
|
+
}),
|
|
1300
|
+
}],
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
this.logger.info('Order created and published', {
|
|
1304
|
+
props: { orderId: order.id },
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
return order;
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
this.logger.error(error, {
|
|
1310
|
+
props: { userId: orderData.userId },
|
|
1311
|
+
});
|
|
1312
|
+
throw error;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
```
|
|
1317
|
+
|
|
1318
|
+
### Kafka Producer - Propagating Correlation ID
|
|
1319
|
+
|
|
1320
|
+
**kafka-producer.service.ts:**
|
|
1321
|
+
|
|
1322
|
+
```typescript
|
|
1323
|
+
import { Injectable } from '@nestjs/common';
|
|
1324
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1325
|
+
import { Kafka } from 'kafkajs';
|
|
1326
|
+
|
|
1327
|
+
@Injectable()
|
|
1328
|
+
export class KafkaProducer {
|
|
1329
|
+
constructor(
|
|
1330
|
+
private readonly kafka: Kafka,
|
|
1331
|
+
private readonly logger: LoggerService,
|
|
1332
|
+
private readonly contextStorage: ContextStorageService,
|
|
1333
|
+
) {}
|
|
1334
|
+
|
|
1335
|
+
async send(topic: string, messages: Array<{ key?: string; value: string }>) {
|
|
1336
|
+
// Get current correlationId from CLS
|
|
1337
|
+
const correlationId = this.contextStorage.getContextId();
|
|
1338
|
+
|
|
1339
|
+
// Add correlationId to message headers
|
|
1340
|
+
const enrichedMessages = messages.map(msg => ({
|
|
1341
|
+
...msg,
|
|
1342
|
+
headers: {
|
|
1343
|
+
'x-correlation-id': correlationId || 'no-correlation-id',
|
|
1344
|
+
},
|
|
1345
|
+
}));
|
|
1346
|
+
|
|
1347
|
+
try {
|
|
1348
|
+
await this.kafka.producer().send({
|
|
1349
|
+
topic,
|
|
1350
|
+
messages: enrichedMessages,
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
this.logger.info('Message published to Kafka', {
|
|
1354
|
+
props: { topic, messageCount: messages.length, correlationId },
|
|
1355
|
+
});
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
this.logger.error(error, {
|
|
1358
|
+
props: { topic, correlationId },
|
|
1359
|
+
});
|
|
1360
|
+
throw error;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
### Kafka Consumer - Extracting and Setting Correlation ID
|
|
1367
|
+
|
|
1368
|
+
**order.consumer.ts:**
|
|
1369
|
+
|
|
1370
|
+
```typescript
|
|
1371
|
+
import { Injectable } from '@nestjs/common';
|
|
1372
|
+
import { KafkaListener } from '@nestjs/microservices';
|
|
1373
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1374
|
+
import { validate as uuidValidate } from 'uuid';
|
|
1375
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
1376
|
+
|
|
1377
|
+
@Injectable()
|
|
1378
|
+
export class OrderConsumer {
|
|
1379
|
+
constructor(
|
|
1380
|
+
private readonly logger: LoggerService,
|
|
1381
|
+
private readonly contextStorage: ContextStorageService,
|
|
1382
|
+
) {}
|
|
1383
|
+
|
|
1384
|
+
@KafkaListener('order.created')
|
|
1385
|
+
async handleOrderCreated(message: OrderCreatedEvent) {
|
|
1386
|
+
// Extract correlationId from message or headers
|
|
1387
|
+
let correlationId = message.correlationId;
|
|
1388
|
+
|
|
1389
|
+
// Fallback to message ID or generate new
|
|
1390
|
+
if (!correlationId || !uuidValidate(correlationId)) {
|
|
1391
|
+
correlationId = message.id || uuidv4();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// CRITICAL: Set correlationId at the start of handler
|
|
1395
|
+
this.contextStorage.setContextId(correlationId);
|
|
1396
|
+
|
|
1397
|
+
this.logger.info('Order received from Kafka', {
|
|
1398
|
+
props: { orderId: message.id, correlationId },
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
try {
|
|
1402
|
+
await this.processOrder(message);
|
|
1403
|
+
this.logger.info('Order processed successfully', {
|
|
1404
|
+
props: { orderId: message.id },
|
|
1405
|
+
});
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
this.logger.error(error, {
|
|
1408
|
+
props: { orderId: message.id, correlationId },
|
|
1409
|
+
});
|
|
1410
|
+
throw error;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
```
|
|
1415
|
+
|
|
1416
|
+
### Bull/BullMQ Worker - Complete Example
|
|
1417
|
+
|
|
1418
|
+
**email.processor.ts:**
|
|
1419
|
+
|
|
1420
|
+
```typescript
|
|
1421
|
+
import { Injectable } from '@nestjs/common';
|
|
1422
|
+
import { Processor, Process } from '@nestjs/bull';
|
|
1423
|
+
import { Job } from 'bull';
|
|
1424
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1425
|
+
|
|
1426
|
+
interface EmailJob {
|
|
1427
|
+
to: string;
|
|
1428
|
+
subject: string;
|
|
1429
|
+
body: string;
|
|
1430
|
+
correlationId?: string;
|
|
1431
|
+
userId: string;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
@Processor('email')
|
|
1435
|
+
export class EmailProcessor {
|
|
1436
|
+
constructor(
|
|
1437
|
+
private readonly logger: LoggerService,
|
|
1438
|
+
private readonly contextStorage: ContextStorageService,
|
|
1439
|
+
private readonly emailService: EmailService,
|
|
1440
|
+
) {}
|
|
1441
|
+
|
|
1442
|
+
@Process('send')
|
|
1443
|
+
async handleSendEmail(job: Job<EmailJob>) {
|
|
1444
|
+
const start = Date.now();
|
|
1445
|
+
|
|
1446
|
+
// CRITICAL: Set correlationId from job data or use job ID
|
|
1447
|
+
const correlationId = job.data.correlationId || job.id.toString();
|
|
1448
|
+
this.contextStorage.setContextId(correlationId);
|
|
1449
|
+
|
|
1450
|
+
this.logger.info('Email job started', {
|
|
1451
|
+
props: {
|
|
1452
|
+
jobId: job.id,
|
|
1453
|
+
to: job.data.to,
|
|
1454
|
+
correlationId,
|
|
1455
|
+
},
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
try {
|
|
1459
|
+
await this.emailService.send(job.data);
|
|
1460
|
+
|
|
1461
|
+
const duration = Date.now() - start;
|
|
1462
|
+
this.logger.info('Email sent successfully', {
|
|
1463
|
+
props: {
|
|
1464
|
+
jobId: job.id,
|
|
1465
|
+
durationMs: duration,
|
|
1466
|
+
},
|
|
1467
|
+
});
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
this.logger.error(error, {
|
|
1470
|
+
props: {
|
|
1471
|
+
jobId: job.id,
|
|
1472
|
+
correlationId,
|
|
1473
|
+
durationMs: Date.now() - start,
|
|
1474
|
+
},
|
|
1475
|
+
});
|
|
1476
|
+
throw error; // Re-throw for Bull retry mechanism
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
```
|
|
1481
|
+
|
|
1482
|
+
**Enqueuing jobs with correlationId:**
|
|
1483
|
+
|
|
1484
|
+
```typescript
|
|
1485
|
+
@Injectable()
|
|
1486
|
+
export class EmailService {
|
|
1487
|
+
constructor(
|
|
1488
|
+
private readonly emailQueue: Queue,
|
|
1489
|
+
private readonly contextStorage: ContextStorageService,
|
|
1490
|
+
) {}
|
|
1491
|
+
|
|
1492
|
+
async sendEmail(data: EmailData) {
|
|
1493
|
+
// Get current correlationId from CLS (if in HTTP context)
|
|
1494
|
+
const correlationId = this.contextStorage.getContextId();
|
|
1495
|
+
|
|
1496
|
+
// Enqueue job with correlationId
|
|
1497
|
+
await this.emailQueue.add('send', {
|
|
1498
|
+
...data,
|
|
1499
|
+
correlationId, // Propagate correlationId to job
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
### CRON / Background Jobs - Complete Example
|
|
1506
|
+
|
|
1507
|
+
**cleanup.scheduler.ts:**
|
|
1508
|
+
|
|
1509
|
+
```typescript
|
|
1510
|
+
import { Injectable } from '@nestjs/common';
|
|
1511
|
+
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
1512
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1513
|
+
|
|
1514
|
+
@Injectable()
|
|
1515
|
+
export class CleanupScheduler {
|
|
1516
|
+
constructor(
|
|
1517
|
+
private readonly logger: LoggerService,
|
|
1518
|
+
private readonly contextStorage: ContextStorageService,
|
|
1519
|
+
private readonly cleanupService: CleanupService,
|
|
1520
|
+
) {}
|
|
1521
|
+
|
|
1522
|
+
@Cron(CronExpression.EVERY_HOUR)
|
|
1523
|
+
async hourlyCleanup() {
|
|
1524
|
+
// Generate unique correlationId for this job
|
|
1525
|
+
const correlationId = `cleanup-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1526
|
+
this.contextStorage.setContextId(correlationId);
|
|
1527
|
+
|
|
1528
|
+
this.logger.info('Hourly cleanup job started', {
|
|
1529
|
+
props: { correlationId },
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
try {
|
|
1533
|
+
const deleted = await this.cleanupService.deleteOldRecords();
|
|
1534
|
+
|
|
1535
|
+
this.logger.info('Hourly cleanup completed', {
|
|
1536
|
+
props: {
|
|
1537
|
+
correlationId,
|
|
1538
|
+
deletedCount: deleted,
|
|
1539
|
+
},
|
|
1540
|
+
});
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
this.logger.error(error, {
|
|
1543
|
+
props: { correlationId },
|
|
1544
|
+
});
|
|
1545
|
+
// Don't throw - CRON jobs should fail silently to prevent crashes
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
|
1550
|
+
async dailyReport() {
|
|
1551
|
+
const correlationId = `daily-report-${Date.now()}`;
|
|
1552
|
+
this.contextStorage.setContextId(correlationId);
|
|
1553
|
+
|
|
1554
|
+
this.logger.info('Daily report generation started', {
|
|
1555
|
+
props: { correlationId },
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
try {
|
|
1559
|
+
await this.reportService.generateDailyReport();
|
|
1560
|
+
this.logger.info('Daily report generated', {
|
|
1561
|
+
props: { correlationId },
|
|
1562
|
+
});
|
|
1563
|
+
} catch (error) {
|
|
1564
|
+
this.logger.error(error, {
|
|
1565
|
+
props: { correlationId },
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
### End-to-End Correlation ID Flow Example
|
|
1573
|
+
|
|
1574
|
+
**Scenario: Sales → Kafka → Document → HTTP → Common Service**
|
|
1575
|
+
|
|
1576
|
+
**1. HTTP Entry (Sales Service):**
|
|
1577
|
+
|
|
1578
|
+
```typescript
|
|
1579
|
+
// sales.controller.ts
|
|
1580
|
+
export class SalesController {
|
|
1581
|
+
constructor(
|
|
1582
|
+
private readonly logger: LoggerService,
|
|
1583
|
+
private readonly contextStorage: ContextStorageService,
|
|
1584
|
+
private readonly kafkaProducer: KafkaProducer,
|
|
1585
|
+
) {}
|
|
1586
|
+
|
|
1587
|
+
@Post('orders')
|
|
1588
|
+
async createOrder(@Body() orderData: CreateOrderDto) {
|
|
1589
|
+
// Correlation ID set by ClsModule middleware from HTTP header
|
|
1590
|
+
// Header: x-correlation-id: abc-123-def-456
|
|
1591
|
+
|
|
1592
|
+
this.logger.info('Order creation request received', {
|
|
1593
|
+
props: { userId: orderData.userId },
|
|
1594
|
+
});
|
|
1595
|
+
// Log: { correlationId: 'abc-123-def-456', ... }
|
|
1596
|
+
|
|
1597
|
+
const order = await this.orderService.create(orderData);
|
|
1598
|
+
|
|
1599
|
+
// Extract correlationId from CLS before Kafka publish
|
|
1600
|
+
const correlationId = this.contextStorage.getContextId(); // 'abc-123-def-456'
|
|
1601
|
+
|
|
1602
|
+
// Publish to Kafka with correlationId
|
|
1603
|
+
await this.kafkaProducer.send({
|
|
1604
|
+
topic: 'order.created',
|
|
1605
|
+
messages: [{
|
|
1606
|
+
value: JSON.stringify({
|
|
1607
|
+
...order,
|
|
1608
|
+
correlationId, // Propagate: 'abc-123-def-456'
|
|
1609
|
+
}),
|
|
1610
|
+
}],
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
return order;
|
|
1614
|
+
}
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1617
|
+
**2. Kafka Consumer (Document Service):**
|
|
1618
|
+
|
|
1619
|
+
```typescript
|
|
1620
|
+
// document.consumer.ts
|
|
1621
|
+
@KafkaListener('order.created')
|
|
1622
|
+
async handleOrderCreated(message: OrderCreatedEvent) {
|
|
1623
|
+
// Extract correlationId from message
|
|
1624
|
+
const correlationId = message.correlationId; // 'abc-123-def-456'
|
|
1625
|
+
|
|
1626
|
+
// CRITICAL: Set in CLS at handler start
|
|
1627
|
+
this.contextStorage.setContextId(correlationId);
|
|
1628
|
+
|
|
1629
|
+
this.logger.info('Processing order document', {
|
|
1630
|
+
props: { orderId: message.id },
|
|
1631
|
+
});
|
|
1632
|
+
// Log: { correlationId: 'abc-123-def-456', ... }
|
|
1633
|
+
|
|
1634
|
+
try {
|
|
1635
|
+
await this.documentService.createInvoice(message);
|
|
1636
|
+
|
|
1637
|
+
// Make HTTP call to Common Service with correlationId
|
|
1638
|
+
await this.httpClient.post('http://common-service/invoices', {
|
|
1639
|
+
orderId: message.id,
|
|
1640
|
+
correlationId, // Propagate: 'abc-123-def-456'
|
|
1641
|
+
}, {
|
|
1642
|
+
headers: {
|
|
1643
|
+
'x-correlation-id': correlationId, // Propagate in header
|
|
1644
|
+
},
|
|
1645
|
+
});
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
this.logger.error(error, {
|
|
1648
|
+
props: { orderId: message.id },
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
```
|
|
1653
|
+
|
|
1654
|
+
**3. HTTP Handler (Common Service):**
|
|
1655
|
+
|
|
1656
|
+
```typescript
|
|
1657
|
+
// common.controller.ts
|
|
1658
|
+
@Post('invoices')
|
|
1659
|
+
async createInvoice(@Body() data: CreateInvoiceDto, @Headers() headers: Headers) {
|
|
1660
|
+
// ClsModule middleware extracts correlationId from header
|
|
1661
|
+
// Header: x-correlation-id: abc-123-def-456
|
|
1662
|
+
|
|
1663
|
+
this.logger.info('Invoice creation request received', {
|
|
1664
|
+
props: { orderId: data.orderId },
|
|
1665
|
+
});
|
|
1666
|
+
// Log: { correlationId: 'abc-123-def-456', ... }
|
|
1667
|
+
|
|
1668
|
+
const invoice = await this.invoiceService.create(data);
|
|
1669
|
+
|
|
1670
|
+
this.logger.info('Invoice created', {
|
|
1671
|
+
props: { invoiceId: invoice.id, orderId: data.orderId },
|
|
1672
|
+
});
|
|
1673
|
+
// Log: { correlationId: 'abc-123-def-456', ... }
|
|
1674
|
+
|
|
1675
|
+
return invoice;
|
|
1676
|
+
}
|
|
1677
|
+
```
|
|
1678
|
+
|
|
1679
|
+
**4. Common Service (app.module.ts):**
|
|
1680
|
+
|
|
1681
|
+
```typescript
|
|
1682
|
+
import { ClsModule } from 'nestjs-cls';
|
|
1683
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
1684
|
+
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
1685
|
+
|
|
1686
|
+
@Module({
|
|
1687
|
+
imports: [
|
|
1688
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported FIRST
|
|
1689
|
+
ClsModule.forRoot({
|
|
1690
|
+
global: true,
|
|
1691
|
+
middleware: {
|
|
1692
|
+
mount: true,
|
|
1693
|
+
generateId: true,
|
|
1694
|
+
idGenerator: (req: Request) => {
|
|
1695
|
+
// Extract from header (propagated from Document Service)
|
|
1696
|
+
return (req.headers['x-correlation-id'] as string) || uuidv4();
|
|
1697
|
+
},
|
|
1698
|
+
},
|
|
1699
|
+
}),
|
|
1700
|
+
ContextModule, // Uses existing ClsModule configuration
|
|
1701
|
+
LoggerModule.forRoot(),
|
|
1702
|
+
],
|
|
1703
|
+
})
|
|
1704
|
+
export class AppModule {}
|
|
1705
|
+
```
|
|
1706
|
+
|
|
1707
|
+
**Flow Summary:**
|
|
1708
|
+
|
|
1709
|
+
```
|
|
1710
|
+
HTTP Request → Sales Service
|
|
1711
|
+
Header: x-correlation-id: abc-123-def-456
|
|
1712
|
+
CLS: abc-123-def-456
|
|
1713
|
+
Log: { correlationId: 'abc-123-def-456' }
|
|
1714
|
+
|
|
1715
|
+
↓ Kafka Message
|
|
1716
|
+
|
|
1717
|
+
Kafka Topic: order.created
|
|
1718
|
+
Payload: { ..., correlationId: 'abc-123-def-456' }
|
|
1719
|
+
|
|
1720
|
+
↓ Consumer
|
|
1721
|
+
|
|
1722
|
+
Document Service
|
|
1723
|
+
CLS: abc-123-def-456 (set at handler start)
|
|
1724
|
+
Log: { correlationId: 'abc-123-def-456' }
|
|
1725
|
+
|
|
1726
|
+
↓ HTTP Request
|
|
1727
|
+
|
|
1728
|
+
HTTP Request → Common Service
|
|
1729
|
+
Header: x-correlation-id: abc-123-def-456
|
|
1730
|
+
CLS: abc-123-def-456 (extracted by middleware)
|
|
1731
|
+
Log: { correlationId: 'abc-123-def-456' }
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
**Key Points:**
|
|
1735
|
+
|
|
1736
|
+
1. ✅ **HTTP → Kafka**: Extract from CLS, include in message payload
|
|
1737
|
+
2. ✅ **Kafka → Consumer**: Extract from message, set in CLS at handler start
|
|
1738
|
+
3. ✅ **Consumer → HTTP**: Extract from CLS, include in HTTP header
|
|
1739
|
+
4. ✅ **HTTP → Service**: CLS middleware extracts from header automatically
|
|
1740
|
+
5. ✅ **All logs**: Same correlationId throughout the flow
|
|
1741
|
+
|
|
1742
|
+
---
|
|
1743
|
+
|
|
1744
|
+
## API Reference
|
|
1745
|
+
|
|
1746
|
+
### LoggerService
|
|
1747
|
+
|
|
1748
|
+
Main logging service injected into your classes.
|
|
1749
|
+
|
|
1750
|
+
#### Methods
|
|
1751
|
+
|
|
1752
|
+
```typescript
|
|
1753
|
+
// Generic log method
|
|
1754
|
+
log(level: LogLevel, message: string | Error, data?: LogData, profile?: string): void
|
|
1755
|
+
|
|
1756
|
+
// Convenience methods
|
|
1757
|
+
debug(message: string, data?: LogData, profile?: string): void
|
|
1758
|
+
info(message: string, data?: LogData, profile?: string): void
|
|
1759
|
+
warn(message: string | Error, data?: LogData, profile?: string): void
|
|
1760
|
+
error(message: string | Error, data?: LogData, profile?: string): void
|
|
1761
|
+
fatal(message: string | Error, data?: LogData, profile?: string): void
|
|
1762
|
+
emergency(message: string | Error, data?: LogData, profile?: string): void
|
|
1763
|
+
|
|
1764
|
+
// Profile (currently not implemented in transport architecture)
|
|
1765
|
+
startProfile(id: string): void
|
|
1766
|
+
```
|
|
1767
|
+
|
|
1768
|
+
#### LogLevel
|
|
1769
|
+
|
|
1770
|
+
```typescript
|
|
1771
|
+
enum LogLevel {
|
|
1772
|
+
Emergency = 'emergency', // One or more systems are unusable
|
|
1773
|
+
Fatal = 'fatal', // A person must take an action immediately
|
|
1774
|
+
Error = 'error', // Error events are likely to cause problems
|
|
1775
|
+
Warn = 'warn', // Warning events might cause problems
|
|
1776
|
+
Info = 'info', // Routine information
|
|
1777
|
+
Debug = 'debug', // Debug or trace information
|
|
1778
|
+
}
|
|
1779
|
+
```
|
|
1780
|
+
|
|
1781
|
+
#### LogData
|
|
1782
|
+
|
|
1783
|
+
```typescript
|
|
1784
|
+
interface LogData {
|
|
1785
|
+
organization?: string; // Override config organization
|
|
1786
|
+
context?: string; // Override config context
|
|
1787
|
+
app?: string; // Override config app
|
|
1788
|
+
sourceClass?: string; // Override auto-detected class name
|
|
1789
|
+
correlationId?: string; // Override CLS correlation ID
|
|
1790
|
+
error?: SerializedError; // Error (must be serialized)
|
|
1791
|
+
props?: Record<string, any>; // Custom properties
|
|
1792
|
+
durationMs?: number; // Duration in milliseconds
|
|
1793
|
+
}
|
|
1794
|
+
```
|
|
1795
|
+
|
|
1796
|
+
### ContextStorageService
|
|
1797
|
+
|
|
1798
|
+
Service for managing CLS context (correlation ID).
|
|
1799
|
+
|
|
1800
|
+
#### Methods
|
|
1801
|
+
|
|
1802
|
+
```typescript
|
|
1803
|
+
// Get correlation ID from CLS
|
|
1804
|
+
getContextId(): string | undefined
|
|
1805
|
+
|
|
1806
|
+
// Set correlation ID in CLS
|
|
1807
|
+
setContextId(id: string): void
|
|
1808
|
+
|
|
1809
|
+
// Get any value from CLS
|
|
1810
|
+
get<T>(key: string): T | undefined
|
|
1811
|
+
|
|
1812
|
+
// Set any value in CLS
|
|
1813
|
+
set<T>(key: string, value: T): void
|
|
1814
|
+
```
|
|
1815
|
+
|
|
1816
|
+
---
|
|
1817
|
+
|
|
1818
|
+
## Migration Guide (Legacy app.useLogger Users)
|
|
1819
|
+
|
|
1820
|
+
### ⚠️ Deprecated: app.useLogger() and app.flushLogs()
|
|
1821
|
+
|
|
1822
|
+
If you're migrating from an older version of `@infineit/winston-logger` that used `app.useLogger()` and `app.flushLogs()`, this section is for you.
|
|
1823
|
+
|
|
1824
|
+
**Status**: These methods have been **removed** and are **no longer available**.
|
|
1825
|
+
|
|
1826
|
+
### Why Was It Removed?
|
|
1827
|
+
|
|
1828
|
+
The `app.useLogger()` and `app.flushLogs()` methods were removed for the following architectural reasons:
|
|
1829
|
+
|
|
1830
|
+
1. **HTTP Assumption Violation**: These methods assumed HTTP runtime, making the logger unusable in Kafka consumers, Bull workers, CRON jobs, and CLI tasks.
|
|
1831
|
+
|
|
1832
|
+
2. **Middleware Auto-Mounting**: The old pattern auto-mounted middleware, violating the library's core principle that applications must configure middleware themselves.
|
|
1833
|
+
|
|
1834
|
+
3. **Transport Architecture**: The new architecture uses a pluggable `LoggerTransport[]` system where Winston is one transport among many, not the only transport.
|
|
1835
|
+
|
|
1836
|
+
4. **Fire-and-Forget Guarantee**: The new architecture ensures all logging is truly fire-and-forget without requiring explicit `flushLogs()` calls.
|
|
1837
|
+
|
|
1838
|
+
### Old Deprecated Usage ❌
|
|
1839
|
+
|
|
1840
|
+
```typescript
|
|
1841
|
+
// ❌ DEPRECATED - This no longer works
|
|
774
1842
|
import { NestFactory } from '@nestjs/core';
|
|
1843
|
+
import { LoggerModule } from '@infineit/winston-logger';
|
|
1844
|
+
|
|
1845
|
+
async function bootstrap() {
|
|
1846
|
+
const app = await NestFactory.create(AppModule);
|
|
1847
|
+
|
|
1848
|
+
// ❌ DEPRECATED: app.useLogger() is removed
|
|
1849
|
+
app.useLogger(app.get(LoggerModule));
|
|
1850
|
+
|
|
1851
|
+
await app.listen(3000);
|
|
1852
|
+
|
|
1853
|
+
// ❌ DEPRECATED: app.flushLogs() is removed
|
|
1854
|
+
app.flushLogs();
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
bootstrap();
|
|
1858
|
+
```
|
|
1859
|
+
|
|
1860
|
+
### New Correct Usage ✅
|
|
1861
|
+
|
|
1862
|
+
```typescript
|
|
1863
|
+
// ✅ CORRECT: Use LoggerService injection
|
|
1864
|
+
import { NestFactory } from '@nestjs/core';
|
|
1865
|
+
import { ClsModule } from 'nestjs-cls';
|
|
1866
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
775
1867
|
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
776
1868
|
|
|
777
1869
|
@Module({
|
|
778
1870
|
imports: [
|
|
779
|
-
ContextModule
|
|
1871
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported BEFORE ContextModule
|
|
1872
|
+
ClsModule.forRoot({
|
|
1873
|
+
global: true,
|
|
1874
|
+
middleware: {
|
|
1875
|
+
mount: true,
|
|
1876
|
+
generateId: true,
|
|
1877
|
+
idGenerator: (req: Request) =>
|
|
1878
|
+
(req.headers['x-correlation-id'] as string) || uuidv4(),
|
|
1879
|
+
},
|
|
1880
|
+
}),
|
|
1881
|
+
ContextModule, // Uses existing ClsModule
|
|
780
1882
|
LoggerModule.forRoot(),
|
|
781
1883
|
],
|
|
782
1884
|
})
|
|
@@ -935,14 +2037,22 @@ async function bootstrap() {
|
|
|
935
2037
|
```typescript
|
|
936
2038
|
import { NestFactory } from '@nestjs/core';
|
|
937
2039
|
import { ClsModule } from 'nestjs-cls';
|
|
2040
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
938
2041
|
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
939
2042
|
|
|
940
2043
|
@Module({
|
|
941
2044
|
imports: [
|
|
2045
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported FIRST
|
|
942
2046
|
ClsModule.forRoot({
|
|
943
|
-
|
|
2047
|
+
global: true,
|
|
2048
|
+
middleware: {
|
|
2049
|
+
mount: true,
|
|
2050
|
+
generateId: true,
|
|
2051
|
+
idGenerator: (req: Request) =>
|
|
2052
|
+
(req.headers['x-correlation-id'] as string) || uuidv4(),
|
|
2053
|
+
},
|
|
944
2054
|
}),
|
|
945
|
-
ContextModule, // ✅
|
|
2055
|
+
ContextModule, // ✅ Uses existing ClsModule
|
|
946
2056
|
LoggerModule.forRoot(), // ✅ Add
|
|
947
2057
|
],
|
|
948
2058
|
})
|
|
@@ -960,10 +2070,11 @@ async function bootstrap() {
|
|
|
960
2070
|
|
|
961
2071
|
If you encounter issues during migration:
|
|
962
2072
|
|
|
963
|
-
1. **Check module imports**: Ensure `
|
|
964
|
-
2. **Check CLS setup**: For HTTP services, ensure `ClsModule` is configured
|
|
965
|
-
3. **Check
|
|
966
|
-
4. **
|
|
2073
|
+
1. **Check module imports**: Ensure `ClsModule.forRoot()` is imported **BEFORE** `ContextModule`, then `LoggerModule.forRoot()` in your `AppModule`
|
|
2074
|
+
2. **Check CLS setup**: For HTTP services, ensure `ClsModule.forRoot()` is configured with `middleware: { mount: true, generateId: true }`
|
|
2075
|
+
3. **Check import order**: `ClsModule` must come before `ContextModule` to avoid `HttpAdapterHost` dependency errors
|
|
2076
|
+
4. **Check injection**: Ensure `LoggerService` is injected via constructor
|
|
2077
|
+
5. **Review examples**: See the "Usage in Different Contexts" section above
|
|
967
2078
|
|
|
968
2079
|
---
|
|
969
2080
|
|
|
@@ -1131,15 +2242,17 @@ import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
|
1131
2242
|
@Module({
|
|
1132
2243
|
imports: [
|
|
1133
2244
|
ConfigModule.forRoot({ isGlobal: true }),
|
|
2245
|
+
// ⚠️ CRITICAL: ClsModule MUST be imported FIRST
|
|
1134
2246
|
ClsModule.forRoot({
|
|
2247
|
+
global: true,
|
|
1135
2248
|
middleware: {
|
|
1136
2249
|
mount: true,
|
|
1137
2250
|
generateId: true,
|
|
1138
2251
|
idGenerator: (req: Request) =>
|
|
1139
|
-
req.headers['x-correlation-id'] as string || uuidv4(),
|
|
2252
|
+
(req.headers['x-correlation-id'] as string) || uuidv4(),
|
|
1140
2253
|
},
|
|
1141
2254
|
}),
|
|
1142
|
-
ContextModule,
|
|
2255
|
+
ContextModule, // Uses existing ClsModule configuration
|
|
1143
2256
|
LoggerModule.forRoot({
|
|
1144
2257
|
nodeEnv: process.env.NODE_ENV,
|
|
1145
2258
|
organization: 'my-org',
|
|
@@ -1149,7 +2262,6 @@ import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
|
1149
2262
|
],
|
|
1150
2263
|
})
|
|
1151
2264
|
export class AppModule {}
|
|
1152
|
-
```
|
|
1153
2265
|
|
|
1154
2266
|
```typescript
|
|
1155
2267
|
import { Injectable } from '@nestjs/common';
|