@infineit/winston-logger 1.0.31 → 1.0.32
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 +979 -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 +15 -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)
|
|
@@ -169,6 +174,11 @@ const config: LoggerModuleConfig = {
|
|
|
169
174
|
organization: 'my-org', // Organization name (optional)
|
|
170
175
|
context: 'user-service', // Bounded context name (optional)
|
|
171
176
|
app: 'api-gateway', // Application name (optional)
|
|
177
|
+
// Optional: Central log forwarding (disabled by default)
|
|
178
|
+
forwardToCentral: false, // Enable forwarding to centralized logging service
|
|
179
|
+
transportType: 'http', // 'http' or 'kafka' (required if forwardToCentral=true)
|
|
180
|
+
httpEndpoint: 'https://...', // HTTP endpoint (required if transportType='http')
|
|
181
|
+
// kafkaTopic: 'project-logs', // Kafka topic (required if transportType='kafka')
|
|
172
182
|
};
|
|
173
183
|
|
|
174
184
|
@Module({
|
|
@@ -192,6 +202,14 @@ LOG_IN_FILE=true
|
|
|
192
202
|
LOGGER_ORGANIZATION=my-org
|
|
193
203
|
LOGGER_CONTEXT=user-service
|
|
194
204
|
LOGGER_APP=api-gateway
|
|
205
|
+
|
|
206
|
+
# Optional: Central log forwarding (disabled by default)
|
|
207
|
+
LOGGER_FORWARD_TO_CENTRAL=false
|
|
208
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
209
|
+
LOGGER_HTTP_ENDPOINT=https://logging-service.example.com/api/logs
|
|
210
|
+
# OR for Kafka:
|
|
211
|
+
# LOGGER_TRANSPORT_TYPE=kafka
|
|
212
|
+
# LOGGER_KAFKA_TOPIC=project-logs-sales
|
|
195
213
|
```
|
|
196
214
|
|
|
197
215
|
```typescript
|
|
@@ -206,6 +224,11 @@ export default registerAs('logger', () => ({
|
|
|
206
224
|
organization: process.env.LOGGER_ORGANIZATION,
|
|
207
225
|
context: process.env.LOGGER_CONTEXT,
|
|
208
226
|
app: process.env.LOGGER_APP,
|
|
227
|
+
// Optional: Central log forwarding
|
|
228
|
+
forwardToCentral: process.env.LOGGER_FORWARD_TO_CENTRAL,
|
|
229
|
+
transportType: process.env.LOGGER_TRANSPORT_TYPE,
|
|
230
|
+
httpEndpoint: process.env.LOGGER_HTTP_ENDPOINT,
|
|
231
|
+
kafkaTopic: process.env.LOGGER_KAFKA_TOPIC,
|
|
209
232
|
}));
|
|
210
233
|
```
|
|
211
234
|
|
|
@@ -233,12 +256,52 @@ export class AppModule {}
|
|
|
233
256
|
| `organization` | `string` | No | `undefined` | Organization or project name |
|
|
234
257
|
| `context` | `string` | No | `undefined` | Bounded context name |
|
|
235
258
|
| `app` | `string` | No | `undefined` | Application or microservice name |
|
|
259
|
+
| `forwardToCentral` | `boolean \| string` | No | `false` | Enable forwarding to centralized logging service (`true`/`false` or `'true'`/`'false'`) |
|
|
260
|
+
| `transportType` | `'kafka' \| 'http'` | No | `undefined` | Transport type for central forwarding (required if `forwardToCentral=true`) |
|
|
261
|
+
| `httpEndpoint` | `string` | No | `undefined` | HTTP endpoint for central forwarding (required if `transportType='http'`) |
|
|
262
|
+
| `kafkaTopic` | `string` | No | `undefined` | Kafka topic for central forwarding (required if `transportType='kafka'`) |
|
|
236
263
|
|
|
237
264
|
### Default Behavior
|
|
238
265
|
|
|
239
266
|
- **Development/Testing**: Console and file logging enabled by default
|
|
240
267
|
- **Production**: Console and file logging disabled by default (unless explicitly enabled)
|
|
241
268
|
- **Slack**: Only enabled in production/testing when `slack_webhook` is provided
|
|
269
|
+
- **Central Forwarding**: Disabled by default (set `forwardToCentral=true` to enable)
|
|
270
|
+
|
|
271
|
+
### Environment-Specific Configuration Tips
|
|
272
|
+
|
|
273
|
+
**Development:**
|
|
274
|
+
```bash
|
|
275
|
+
# Disable forwarding in development (optional)
|
|
276
|
+
LOGGER_FORWARD_TO_CENTRAL=false
|
|
277
|
+
# OR use local endpoint for testing
|
|
278
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
279
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
280
|
+
LOGGER_HTTP_ENDPOINT=http://localhost:3000/api/logs
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Production:**
|
|
284
|
+
```bash
|
|
285
|
+
# Enable forwarding in production
|
|
286
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
287
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
288
|
+
LOGGER_HTTP_ENDPOINT=https://central-logging.example.com/api/logs
|
|
289
|
+
# OR use Kafka for better performance
|
|
290
|
+
# LOGGER_TRANSPORT_TYPE=kafka
|
|
291
|
+
# LOGGER_KAFKA_TOPIC=project-logs-<project-name>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Project-Specific Topics (Kafka):**
|
|
295
|
+
```bash
|
|
296
|
+
# Sales project
|
|
297
|
+
LOGGER_KAFKA_TOPIC=project-logs-sales
|
|
298
|
+
|
|
299
|
+
# Document project
|
|
300
|
+
LOGGER_KAFKA_TOPIC=project-logs-document
|
|
301
|
+
|
|
302
|
+
# Common service
|
|
303
|
+
LOGGER_KAFKA_TOPIC=project-logs-common
|
|
304
|
+
```
|
|
242
305
|
|
|
243
306
|
---
|
|
244
307
|
|
|
@@ -509,6 +572,195 @@ All transports must:
|
|
|
509
572
|
|
|
510
573
|
---
|
|
511
574
|
|
|
575
|
+
## Central Log Forwarding
|
|
576
|
+
|
|
577
|
+
### Overview
|
|
578
|
+
|
|
579
|
+
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.
|
|
580
|
+
|
|
581
|
+
**Key characteristics:**
|
|
582
|
+
- ✅ **Optional**: Only enabled when `forwardToCentral=true` (disabled by default)
|
|
583
|
+
- ✅ **Fire-and-forget**: Non-blocking, never throws, failures are swallowed
|
|
584
|
+
- ✅ **Preserves correlationId**: Automatically includes correlationId from CLS
|
|
585
|
+
- ✅ **Non-invasive**: Original logging (console, file, Slack) continues normally and is unaffected
|
|
586
|
+
|
|
587
|
+
### Configuration
|
|
588
|
+
|
|
589
|
+
**Via Environment Variables:**
|
|
590
|
+
```bash
|
|
591
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
592
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
593
|
+
LOGGER_HTTP_ENDPOINT=https://logging-service.example.com/api/logs
|
|
594
|
+
# OR for Kafka:
|
|
595
|
+
# LOGGER_TRANSPORT_TYPE=kafka
|
|
596
|
+
# LOGGER_KAFKA_TOPIC=project-logs-sales
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**Via Module Configuration:**
|
|
600
|
+
```typescript
|
|
601
|
+
@Module({
|
|
602
|
+
imports: [
|
|
603
|
+
LoggerModule.forRoot({
|
|
604
|
+
// ... existing config
|
|
605
|
+
forwardToCentral: true,
|
|
606
|
+
transportType: 'http', // or 'kafka'
|
|
607
|
+
httpEndpoint: 'https://logging-service.example.com/api/logs',
|
|
608
|
+
// OR for Kafka:
|
|
609
|
+
// kafkaTopic: 'project-logs-sales',
|
|
610
|
+
}),
|
|
611
|
+
],
|
|
612
|
+
})
|
|
613
|
+
export class AppModule {}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### HTTP Forwarding
|
|
617
|
+
|
|
618
|
+
Forwards logs via HTTP POST requests to the configured endpoint. Uses Node.js built-in `http`/`https` modules (no external dependencies).
|
|
619
|
+
|
|
620
|
+
**Configuration:**
|
|
621
|
+
```bash
|
|
622
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
623
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
624
|
+
LOGGER_HTTP_ENDPOINT=https://central-logging.example.com/api/logs
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**Usage:** No code changes required. All logs are automatically forwarded with correlationId preserved.
|
|
628
|
+
|
|
629
|
+
**Request Format:**
|
|
630
|
+
- **Method**: POST
|
|
631
|
+
- **Headers**: `Content-Type: application/json`, `x-correlation-id: <correlationId>` (if available)
|
|
632
|
+
- **Body**: `NormalizedLog` JSON object
|
|
633
|
+
|
|
634
|
+
### Kafka Forwarding
|
|
635
|
+
|
|
636
|
+
Forwards logs to a configured Kafka topic. Requires a Kafka producer instance provided by your application.
|
|
637
|
+
|
|
638
|
+
**Prerequisites:** Provide Kafka producer via `KafkaProducerKey`:
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
import { Module, Provider } from '@nestjs/common';
|
|
642
|
+
import { Kafka } from 'kafkajs';
|
|
643
|
+
import { LoggerModule, KafkaProducerKey } from '@infineit/winston-logger';
|
|
644
|
+
|
|
645
|
+
@Module({
|
|
646
|
+
imports: [
|
|
647
|
+
LoggerModule.forRoot({
|
|
648
|
+
forwardToCentral: true,
|
|
649
|
+
transportType: 'kafka',
|
|
650
|
+
kafkaTopic: 'project-logs-sales',
|
|
651
|
+
}),
|
|
652
|
+
],
|
|
653
|
+
providers: [
|
|
654
|
+
{
|
|
655
|
+
provide: KafkaProducerKey,
|
|
656
|
+
useFactory: async () => {
|
|
657
|
+
const kafka = new Kafka({ brokers: ['localhost:9092'] });
|
|
658
|
+
const producer = kafka.producer();
|
|
659
|
+
await producer.connect();
|
|
660
|
+
return producer;
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
],
|
|
664
|
+
})
|
|
665
|
+
export class AppModule {}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Usage:** No code changes required. All logs are automatically forwarded with correlationId preserved.
|
|
669
|
+
|
|
670
|
+
**Message Format:**
|
|
671
|
+
- **Topic**: Configured `kafkaTopic`
|
|
672
|
+
- **Key**: `correlationId` or `'no-correlation-id'` if not available
|
|
673
|
+
- **Headers**: `x-correlation-id: <correlationId>` (if available)
|
|
674
|
+
- **Value**: `NormalizedLog` JSON string
|
|
675
|
+
|
|
676
|
+
### Correlation ID Preservation
|
|
677
|
+
|
|
678
|
+
Correlation ID is automatically preserved and forwarded:
|
|
679
|
+
|
|
680
|
+
**HTTP Forwarding:**
|
|
681
|
+
1. Correlation ID from CLS is included in log normalization
|
|
682
|
+
2. Forwarder sends log with `correlationId` in HTTP header (`x-correlation-id`) and body
|
|
683
|
+
|
|
684
|
+
**Kafka Forwarding:**
|
|
685
|
+
1. Correlation ID from CLS is included in log normalization
|
|
686
|
+
2. Forwarder sends log with `correlationId` as message key, header, and in message value
|
|
687
|
+
|
|
688
|
+
**No changes required:** If CLS is configured, correlationId is automatically included in forwarded logs.
|
|
689
|
+
|
|
690
|
+
### Fire-and-Forget Behavior
|
|
691
|
+
|
|
692
|
+
Central log forwarding is completely fire-and-forget:
|
|
693
|
+
|
|
694
|
+
- ✅ **Never blocks**: Forwarding happens asynchronously, never blocks business logic
|
|
695
|
+
- ✅ **Never throws**: All errors are swallowed, never propagate to application code
|
|
696
|
+
- ✅ **Never affects business logic**: Forwarding failures are logged to `console.error` only
|
|
697
|
+
- ✅ **Never breaks logging**: Original logging (console, file, Slack) continues normally and is unaffected
|
|
698
|
+
|
|
699
|
+
**Important:** Original logging behavior remains completely unchanged. Forwarding is an additional, optional layer that runs in parallel.
|
|
700
|
+
|
|
701
|
+
### Environment-Specific Configuration
|
|
702
|
+
|
|
703
|
+
**Development:**
|
|
704
|
+
```bash
|
|
705
|
+
# Option 1: Disable forwarding in development
|
|
706
|
+
LOGGER_FORWARD_TO_CENTRAL=false
|
|
707
|
+
|
|
708
|
+
# Option 2: Use local endpoint for testing
|
|
709
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
710
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
711
|
+
LOGGER_HTTP_ENDPOINT=http://localhost:3000/api/logs
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
**Production:**
|
|
715
|
+
```bash
|
|
716
|
+
# HTTP forwarding (simpler setup)
|
|
717
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
718
|
+
LOGGER_TRANSPORT_TYPE=http
|
|
719
|
+
LOGGER_HTTP_ENDPOINT=https://central-logging.example.com/api/logs
|
|
720
|
+
|
|
721
|
+
# OR Kafka forwarding (better performance, requires Kafka producer)
|
|
722
|
+
LOGGER_FORWARD_TO_CENTRAL=true
|
|
723
|
+
LOGGER_TRANSPORT_TYPE=kafka
|
|
724
|
+
LOGGER_KAFKA_TOPIC=project-logs-<project-name>
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
**Project-Specific Topics (Kafka):**
|
|
728
|
+
Use different Kafka topics per project for better log organization:
|
|
729
|
+
```bash
|
|
730
|
+
# Sales project
|
|
731
|
+
LOGGER_KAFKA_TOPIC=project-logs-sales
|
|
732
|
+
|
|
733
|
+
# Document project
|
|
734
|
+
LOGGER_KAFKA_TOPIC=project-logs-document
|
|
735
|
+
|
|
736
|
+
# Common service
|
|
737
|
+
LOGGER_KAFKA_TOPIC=project-logs-common
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### Troubleshooting
|
|
741
|
+
|
|
742
|
+
**Forwarding Not Working:**
|
|
743
|
+
1. Check configuration: `LOGGER_FORWARD_TO_CENTRAL` must be `'true'` or `true`
|
|
744
|
+
2. Verify transport type: Must be `'http'` or `'kafka'`
|
|
745
|
+
3. Check endpoint/topic: Must be configured correctly
|
|
746
|
+
4. For Kafka: Ensure `KafkaProducerKey` is provided in module providers
|
|
747
|
+
5. Check console errors: Forwarding errors are logged to `console.error` with message `"CentralLogForwarder forward error (swallowed): <error>"`
|
|
748
|
+
|
|
749
|
+
**Correlation ID Missing:**
|
|
750
|
+
- Ensure CLS is configured: `ClsModule.forRoot()` with `generateId: true`
|
|
751
|
+
- Ensure `ContextModule` is imported
|
|
752
|
+
- Check that original logs have `correlationId` field (if not, forwarded logs won't either)
|
|
753
|
+
|
|
754
|
+
**HTTP Forwarding Timeout:**
|
|
755
|
+
- HTTP forwarding has a 5-second timeout
|
|
756
|
+
- Timeouts are swallowed (no errors thrown)
|
|
757
|
+
- Consider using Kafka instead if central service is slow
|
|
758
|
+
- Or improve central service performance
|
|
759
|
+
|
|
760
|
+
**Note:** Forwarding failures do not affect your application. Original logging continues normally, and business logic is never impacted.
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
512
764
|
## Log Normalization
|
|
513
765
|
|
|
514
766
|
### Automatic Normalization
|
|
@@ -649,6 +901,732 @@ import { v4 as uuidv4 } from 'uuid';
|
|
|
649
901
|
export class AppModule {}
|
|
650
902
|
```
|
|
651
903
|
|
|
904
|
+
### CLS Context Boundaries and Edge Cases
|
|
905
|
+
|
|
906
|
+
#### Async Context Propagation
|
|
907
|
+
|
|
908
|
+
CLS (AsyncLocalStorage) automatically propagates correlation ID within the same async context chain:
|
|
909
|
+
|
|
910
|
+
```typescript
|
|
911
|
+
// ✅ Works: Same async context
|
|
912
|
+
async handleRequest() {
|
|
913
|
+
this.contextStorage.setContextId('abc-123');
|
|
914
|
+
await this.serviceA.process(); // Can read correlationId
|
|
915
|
+
await this.serviceB.process(); // Can read correlationId
|
|
916
|
+
}
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
#### Context Boundaries - Where Correlation ID is Lost
|
|
920
|
+
|
|
921
|
+
Correlation ID is **lost** when crossing async boundaries:
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
// ❌ Lost: Event emitter
|
|
925
|
+
this.contextStorage.setContextId('abc-123');
|
|
926
|
+
eventEmitter.on('event', () => {
|
|
927
|
+
this.logger.info('Log'); // correlationId: undefined
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
// ❌ Lost: setTimeout/setInterval
|
|
931
|
+
this.contextStorage.setContextId('abc-123');
|
|
932
|
+
setTimeout(() => {
|
|
933
|
+
this.logger.info('Log'); // correlationId: undefined
|
|
934
|
+
}, 100);
|
|
935
|
+
|
|
936
|
+
// ❌ Lost: Manual promise without context preservation
|
|
937
|
+
this.contextStorage.setContextId('abc-123');
|
|
938
|
+
new Promise((resolve) => {
|
|
939
|
+
setTimeout(resolve, 100);
|
|
940
|
+
}).then(() => {
|
|
941
|
+
this.logger.info('Log'); // correlationId: undefined
|
|
942
|
+
});
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
#### Solutions for Context Boundaries
|
|
946
|
+
|
|
947
|
+
**1. Event Emitters - Re-set correlationId:**
|
|
948
|
+
|
|
949
|
+
```typescript
|
|
950
|
+
// ✅ Solution: Set correlationId in event handler
|
|
951
|
+
eventEmitter.on('event', async (data) => {
|
|
952
|
+
this.contextStorage.setContextId(data.correlationId);
|
|
953
|
+
this.logger.info('Processing event'); // correlationId available
|
|
954
|
+
});
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
**2. Timers - Wrap in CLS.run():**
|
|
958
|
+
|
|
959
|
+
```typescript
|
|
960
|
+
import { ClsService } from 'nestjs-cls';
|
|
961
|
+
|
|
962
|
+
// ✅ Solution: Use CLS.run() for timers
|
|
963
|
+
const correlationId = this.contextStorage.getContextId();
|
|
964
|
+
setTimeout(() => {
|
|
965
|
+
this.clsService.run(async () => {
|
|
966
|
+
this.clsService.set('CLS_ID', correlationId);
|
|
967
|
+
this.logger.info('Log'); // correlationId available
|
|
968
|
+
});
|
|
969
|
+
}, 100);
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
**3. Worker Threads - Manual Propagation:**
|
|
973
|
+
|
|
974
|
+
```typescript
|
|
975
|
+
// ✅ Solution: Pass correlationId explicitly to workers
|
|
976
|
+
const correlationId = this.contextStorage.getContextId();
|
|
977
|
+
worker.postMessage({ correlationId, data: ... });
|
|
978
|
+
|
|
979
|
+
worker.on('message', (msg) => {
|
|
980
|
+
this.contextStorage.setContextId(msg.correlationId);
|
|
981
|
+
this.logger.info('Processing'); // correlationId available
|
|
982
|
+
});
|
|
983
|
+
```
|
|
984
|
+
|
|
985
|
+
**4. Queues and Background Jobs - Always Re-set:**
|
|
986
|
+
|
|
987
|
+
```typescript
|
|
988
|
+
// ✅ Solution: Extract and set at job start
|
|
989
|
+
@Process('job')
|
|
990
|
+
async handleJob(job: Job) {
|
|
991
|
+
// Always set correlationId at job start
|
|
992
|
+
const correlationId = job.data.correlationId || job.id.toString();
|
|
993
|
+
this.contextStorage.setContextId(correlationId);
|
|
994
|
+
|
|
995
|
+
this.logger.info('Job started'); // correlationId available
|
|
996
|
+
}
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
#### CLS Failure Handling
|
|
1000
|
+
|
|
1001
|
+
The logger handles CLS failures gracefully:
|
|
1002
|
+
|
|
1003
|
+
```typescript
|
|
1004
|
+
// If getContextId() throws, logger continues with undefined
|
|
1005
|
+
try {
|
|
1006
|
+
const id = this.contextStorage.getContextId(); // May throw
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
// LoggerService.getLogData() catches this
|
|
1009
|
+
// Falls back to undefined correlationId
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
this.logger.info('Log'); // Never throws, correlationId: undefined if CLS fails
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
### Correlation ID Format Validation
|
|
1016
|
+
|
|
1017
|
+
The library does **not** validate correlation ID format. Applications should validate format before setting.
|
|
1018
|
+
|
|
1019
|
+
#### Recommended Format
|
|
1020
|
+
|
|
1021
|
+
**Standard UUID v4** (recommended for most cases):
|
|
1022
|
+
|
|
1023
|
+
```typescript
|
|
1024
|
+
import { v4 as uuidv4, validate as uuidValidate } from 'uuid';
|
|
1025
|
+
|
|
1026
|
+
// ✅ Validate before setting
|
|
1027
|
+
const correlationId = req.headers['x-correlation-id'] || uuidv4();
|
|
1028
|
+
if (uuidValidate(correlationId)) {
|
|
1029
|
+
this.contextStorage.setContextId(correlationId);
|
|
1030
|
+
} else {
|
|
1031
|
+
// Generate new one or use fallback
|
|
1032
|
+
this.contextStorage.setContextId(uuidv4());
|
|
1033
|
+
}
|
|
1034
|
+
```
|
|
1035
|
+
|
|
1036
|
+
**Custom Format Validation:**
|
|
1037
|
+
|
|
1038
|
+
```typescript
|
|
1039
|
+
// Example: Custom format (prefix + timestamp + random)
|
|
1040
|
+
const CORRELATION_ID_PATTERN = /^req-[0-9]{13}-[a-z0-9]{8}$/;
|
|
1041
|
+
|
|
1042
|
+
function validateCorrelationId(id: string): boolean {
|
|
1043
|
+
return CORRELATION_ID_PATTERN.test(id);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// ✅ Validate before setting
|
|
1047
|
+
const correlationId = message.correlationId;
|
|
1048
|
+
if (correlationId && validateCorrelationId(correlationId)) {
|
|
1049
|
+
this.contextStorage.setContextId(correlationId);
|
|
1050
|
+
} else {
|
|
1051
|
+
// Generate new one with custom format
|
|
1052
|
+
const newId = `req-${Date.now()}-${Math.random().toString(36).substr(2, 8)}`;
|
|
1053
|
+
this.contextStorage.setContextId(newId);
|
|
1054
|
+
}
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
#### Format Guidelines
|
|
1058
|
+
|
|
1059
|
+
**Recommended:**
|
|
1060
|
+
- ✅ UUID v4: `550e8400-e29b-41d4-a716-446655440000`
|
|
1061
|
+
- ✅ Alphanumeric: `req-abc123def456`
|
|
1062
|
+
- ✅ With timestamp: `req-1640000000000-abc123`
|
|
1063
|
+
|
|
1064
|
+
**Avoid:**
|
|
1065
|
+
- ❌ Empty strings: `""`
|
|
1066
|
+
- ❌ Very long strings: > 255 characters
|
|
1067
|
+
- ❌ Special characters that break systems: `null`, `undefined` as string
|
|
1068
|
+
- ❌ Newlines or control characters
|
|
1069
|
+
|
|
1070
|
+
#### Validation in HTTP Middleware
|
|
1071
|
+
|
|
1072
|
+
```typescript
|
|
1073
|
+
import { v4 as uuidv4, validate as uuidValidate } from 'uuid';
|
|
1074
|
+
|
|
1075
|
+
ClsModule.forRoot({
|
|
1076
|
+
middleware: {
|
|
1077
|
+
mount: true,
|
|
1078
|
+
generateId: true,
|
|
1079
|
+
idGenerator: (req: Request) => {
|
|
1080
|
+
const headerId = req.headers['x-correlation-id'] as string;
|
|
1081
|
+
|
|
1082
|
+
// Validate format
|
|
1083
|
+
if (headerId && uuidValidate(headerId)) {
|
|
1084
|
+
return headerId;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Generate new one if invalid
|
|
1088
|
+
return uuidv4();
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
})
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
#### Validation in Kafka Consumers
|
|
1095
|
+
|
|
1096
|
+
```typescript
|
|
1097
|
+
@KafkaListener('order.created')
|
|
1098
|
+
async handleOrderCreated(message: OrderCreatedEvent) {
|
|
1099
|
+
// Validate before setting
|
|
1100
|
+
let correlationId = message.correlationId;
|
|
1101
|
+
|
|
1102
|
+
if (!correlationId || !uuidValidate(correlationId)) {
|
|
1103
|
+
// Use message ID as fallback or generate new
|
|
1104
|
+
correlationId = message.id || uuidv4();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
this.contextStorage.setContextId(correlationId);
|
|
1108
|
+
this.logger.info('Processing order');
|
|
1109
|
+
}
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
## Complete Integration Examples
|
|
1115
|
+
|
|
1116
|
+
### HTTP Entry Point - Controller with Middleware
|
|
1117
|
+
|
|
1118
|
+
**app.module.ts:**
|
|
1119
|
+
|
|
1120
|
+
```typescript
|
|
1121
|
+
import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
1122
|
+
import { ConfigModule } from '@nestjs/config';
|
|
1123
|
+
import { ClsModule } from 'nestjs-cls';
|
|
1124
|
+
import { v4 as uuidv4, validate as uuidValidate } from 'uuid';
|
|
1125
|
+
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
1126
|
+
|
|
1127
|
+
@Module({
|
|
1128
|
+
imports: [
|
|
1129
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
1130
|
+
ClsModule.forRoot({
|
|
1131
|
+
middleware: {
|
|
1132
|
+
mount: true,
|
|
1133
|
+
generateId: true,
|
|
1134
|
+
idGenerator: (req: Request) => {
|
|
1135
|
+
const headerId = req.headers['x-correlation-id'] as string;
|
|
1136
|
+
return (headerId && uuidValidate(headerId)) ? headerId : uuidv4();
|
|
1137
|
+
},
|
|
1138
|
+
},
|
|
1139
|
+
}),
|
|
1140
|
+
ContextModule,
|
|
1141
|
+
LoggerModule.forRoot({
|
|
1142
|
+
nodeEnv: process.env.NODE_ENV,
|
|
1143
|
+
organization: 'sales',
|
|
1144
|
+
context: 'api',
|
|
1145
|
+
app: 'gateway',
|
|
1146
|
+
}),
|
|
1147
|
+
],
|
|
1148
|
+
})
|
|
1149
|
+
export class AppModule implements NestModule {
|
|
1150
|
+
configure(consumer: MiddlewareConsumer) {
|
|
1151
|
+
// Additional middleware can be added here
|
|
1152
|
+
// ClsModule middleware is already mounted above
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
**sales.controller.ts:**
|
|
1158
|
+
|
|
1159
|
+
```typescript
|
|
1160
|
+
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
|
1161
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1162
|
+
import { KafkaProducer } from './kafka-producer';
|
|
1163
|
+
|
|
1164
|
+
@Controller('sales')
|
|
1165
|
+
export class SalesController {
|
|
1166
|
+
constructor(
|
|
1167
|
+
private readonly logger: LoggerService,
|
|
1168
|
+
private readonly contextStorage: ContextStorageService,
|
|
1169
|
+
private readonly kafkaProducer: KafkaProducer,
|
|
1170
|
+
) {}
|
|
1171
|
+
|
|
1172
|
+
@Post('orders')
|
|
1173
|
+
async createOrder(@Body() orderData: CreateOrderDto) {
|
|
1174
|
+
// Correlation ID automatically available from CLS (set by middleware)
|
|
1175
|
+
this.logger.info('Creating order', {
|
|
1176
|
+
props: { userId: orderData.userId, amount: orderData.amount },
|
|
1177
|
+
});
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
const order = await this.orderService.create(orderData);
|
|
1181
|
+
|
|
1182
|
+
// Extract correlationId from CLS before publishing to Kafka
|
|
1183
|
+
const correlationId = this.contextStorage.getContextId();
|
|
1184
|
+
|
|
1185
|
+
// Publish to Kafka with correlationId
|
|
1186
|
+
await this.kafkaProducer.send({
|
|
1187
|
+
topic: 'order.created',
|
|
1188
|
+
messages: [{
|
|
1189
|
+
key: order.id,
|
|
1190
|
+
value: JSON.stringify({
|
|
1191
|
+
...order,
|
|
1192
|
+
correlationId, // Propagate correlationId
|
|
1193
|
+
}),
|
|
1194
|
+
}],
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
this.logger.info('Order created and published', {
|
|
1198
|
+
props: { orderId: order.id },
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
return order;
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
this.logger.error(error, {
|
|
1204
|
+
props: { userId: orderData.userId },
|
|
1205
|
+
});
|
|
1206
|
+
throw error;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
### Kafka Producer - Propagating Correlation ID
|
|
1213
|
+
|
|
1214
|
+
**kafka-producer.service.ts:**
|
|
1215
|
+
|
|
1216
|
+
```typescript
|
|
1217
|
+
import { Injectable } from '@nestjs/common';
|
|
1218
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1219
|
+
import { Kafka } from 'kafkajs';
|
|
1220
|
+
|
|
1221
|
+
@Injectable()
|
|
1222
|
+
export class KafkaProducer {
|
|
1223
|
+
constructor(
|
|
1224
|
+
private readonly kafka: Kafka,
|
|
1225
|
+
private readonly logger: LoggerService,
|
|
1226
|
+
private readonly contextStorage: ContextStorageService,
|
|
1227
|
+
) {}
|
|
1228
|
+
|
|
1229
|
+
async send(topic: string, messages: Array<{ key?: string; value: string }>) {
|
|
1230
|
+
// Get current correlationId from CLS
|
|
1231
|
+
const correlationId = this.contextStorage.getContextId();
|
|
1232
|
+
|
|
1233
|
+
// Add correlationId to message headers
|
|
1234
|
+
const enrichedMessages = messages.map(msg => ({
|
|
1235
|
+
...msg,
|
|
1236
|
+
headers: {
|
|
1237
|
+
'x-correlation-id': correlationId || 'no-correlation-id',
|
|
1238
|
+
},
|
|
1239
|
+
}));
|
|
1240
|
+
|
|
1241
|
+
try {
|
|
1242
|
+
await this.kafka.producer().send({
|
|
1243
|
+
topic,
|
|
1244
|
+
messages: enrichedMessages,
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
this.logger.info('Message published to Kafka', {
|
|
1248
|
+
props: { topic, messageCount: messages.length, correlationId },
|
|
1249
|
+
});
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
this.logger.error(error, {
|
|
1252
|
+
props: { topic, correlationId },
|
|
1253
|
+
});
|
|
1254
|
+
throw error;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
### Kafka Consumer - Extracting and Setting Correlation ID
|
|
1261
|
+
|
|
1262
|
+
**order.consumer.ts:**
|
|
1263
|
+
|
|
1264
|
+
```typescript
|
|
1265
|
+
import { Injectable } from '@nestjs/common';
|
|
1266
|
+
import { KafkaListener } from '@nestjs/microservices';
|
|
1267
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1268
|
+
import { validate as uuidValidate } from 'uuid';
|
|
1269
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
1270
|
+
|
|
1271
|
+
@Injectable()
|
|
1272
|
+
export class OrderConsumer {
|
|
1273
|
+
constructor(
|
|
1274
|
+
private readonly logger: LoggerService,
|
|
1275
|
+
private readonly contextStorage: ContextStorageService,
|
|
1276
|
+
) {}
|
|
1277
|
+
|
|
1278
|
+
@KafkaListener('order.created')
|
|
1279
|
+
async handleOrderCreated(message: OrderCreatedEvent) {
|
|
1280
|
+
// Extract correlationId from message or headers
|
|
1281
|
+
let correlationId = message.correlationId;
|
|
1282
|
+
|
|
1283
|
+
// Fallback to message ID or generate new
|
|
1284
|
+
if (!correlationId || !uuidValidate(correlationId)) {
|
|
1285
|
+
correlationId = message.id || uuidv4();
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// CRITICAL: Set correlationId at the start of handler
|
|
1289
|
+
this.contextStorage.setContextId(correlationId);
|
|
1290
|
+
|
|
1291
|
+
this.logger.info('Order received from Kafka', {
|
|
1292
|
+
props: { orderId: message.id, correlationId },
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
try {
|
|
1296
|
+
await this.processOrder(message);
|
|
1297
|
+
this.logger.info('Order processed successfully', {
|
|
1298
|
+
props: { orderId: message.id },
|
|
1299
|
+
});
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
this.logger.error(error, {
|
|
1302
|
+
props: { orderId: message.id, correlationId },
|
|
1303
|
+
});
|
|
1304
|
+
throw error;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
### Bull/BullMQ Worker - Complete Example
|
|
1311
|
+
|
|
1312
|
+
**email.processor.ts:**
|
|
1313
|
+
|
|
1314
|
+
```typescript
|
|
1315
|
+
import { Injectable } from '@nestjs/common';
|
|
1316
|
+
import { Processor, Process } from '@nestjs/bull';
|
|
1317
|
+
import { Job } from 'bull';
|
|
1318
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1319
|
+
|
|
1320
|
+
interface EmailJob {
|
|
1321
|
+
to: string;
|
|
1322
|
+
subject: string;
|
|
1323
|
+
body: string;
|
|
1324
|
+
correlationId?: string;
|
|
1325
|
+
userId: string;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
@Processor('email')
|
|
1329
|
+
export class EmailProcessor {
|
|
1330
|
+
constructor(
|
|
1331
|
+
private readonly logger: LoggerService,
|
|
1332
|
+
private readonly contextStorage: ContextStorageService,
|
|
1333
|
+
private readonly emailService: EmailService,
|
|
1334
|
+
) {}
|
|
1335
|
+
|
|
1336
|
+
@Process('send')
|
|
1337
|
+
async handleSendEmail(job: Job<EmailJob>) {
|
|
1338
|
+
const start = Date.now();
|
|
1339
|
+
|
|
1340
|
+
// CRITICAL: Set correlationId from job data or use job ID
|
|
1341
|
+
const correlationId = job.data.correlationId || job.id.toString();
|
|
1342
|
+
this.contextStorage.setContextId(correlationId);
|
|
1343
|
+
|
|
1344
|
+
this.logger.info('Email job started', {
|
|
1345
|
+
props: {
|
|
1346
|
+
jobId: job.id,
|
|
1347
|
+
to: job.data.to,
|
|
1348
|
+
correlationId,
|
|
1349
|
+
},
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
try {
|
|
1353
|
+
await this.emailService.send(job.data);
|
|
1354
|
+
|
|
1355
|
+
const duration = Date.now() - start;
|
|
1356
|
+
this.logger.info('Email sent successfully', {
|
|
1357
|
+
props: {
|
|
1358
|
+
jobId: job.id,
|
|
1359
|
+
durationMs: duration,
|
|
1360
|
+
},
|
|
1361
|
+
});
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
this.logger.error(error, {
|
|
1364
|
+
props: {
|
|
1365
|
+
jobId: job.id,
|
|
1366
|
+
correlationId,
|
|
1367
|
+
durationMs: Date.now() - start,
|
|
1368
|
+
},
|
|
1369
|
+
});
|
|
1370
|
+
throw error; // Re-throw for Bull retry mechanism
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
**Enqueuing jobs with correlationId:**
|
|
1377
|
+
|
|
1378
|
+
```typescript
|
|
1379
|
+
@Injectable()
|
|
1380
|
+
export class EmailService {
|
|
1381
|
+
constructor(
|
|
1382
|
+
private readonly emailQueue: Queue,
|
|
1383
|
+
private readonly contextStorage: ContextStorageService,
|
|
1384
|
+
) {}
|
|
1385
|
+
|
|
1386
|
+
async sendEmail(data: EmailData) {
|
|
1387
|
+
// Get current correlationId from CLS (if in HTTP context)
|
|
1388
|
+
const correlationId = this.contextStorage.getContextId();
|
|
1389
|
+
|
|
1390
|
+
// Enqueue job with correlationId
|
|
1391
|
+
await this.emailQueue.add('send', {
|
|
1392
|
+
...data,
|
|
1393
|
+
correlationId, // Propagate correlationId to job
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
### CRON / Background Jobs - Complete Example
|
|
1400
|
+
|
|
1401
|
+
**cleanup.scheduler.ts:**
|
|
1402
|
+
|
|
1403
|
+
```typescript
|
|
1404
|
+
import { Injectable } from '@nestjs/common';
|
|
1405
|
+
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
1406
|
+
import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
|
|
1407
|
+
|
|
1408
|
+
@Injectable()
|
|
1409
|
+
export class CleanupScheduler {
|
|
1410
|
+
constructor(
|
|
1411
|
+
private readonly logger: LoggerService,
|
|
1412
|
+
private readonly contextStorage: ContextStorageService,
|
|
1413
|
+
private readonly cleanupService: CleanupService,
|
|
1414
|
+
) {}
|
|
1415
|
+
|
|
1416
|
+
@Cron(CronExpression.EVERY_HOUR)
|
|
1417
|
+
async hourlyCleanup() {
|
|
1418
|
+
// Generate unique correlationId for this job
|
|
1419
|
+
const correlationId = `cleanup-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1420
|
+
this.contextStorage.setContextId(correlationId);
|
|
1421
|
+
|
|
1422
|
+
this.logger.info('Hourly cleanup job started', {
|
|
1423
|
+
props: { correlationId },
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
try {
|
|
1427
|
+
const deleted = await this.cleanupService.deleteOldRecords();
|
|
1428
|
+
|
|
1429
|
+
this.logger.info('Hourly cleanup completed', {
|
|
1430
|
+
props: {
|
|
1431
|
+
correlationId,
|
|
1432
|
+
deletedCount: deleted,
|
|
1433
|
+
},
|
|
1434
|
+
});
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
this.logger.error(error, {
|
|
1437
|
+
props: { correlationId },
|
|
1438
|
+
});
|
|
1439
|
+
// Don't throw - CRON jobs should fail silently to prevent crashes
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
|
1444
|
+
async dailyReport() {
|
|
1445
|
+
const correlationId = `daily-report-${Date.now()}`;
|
|
1446
|
+
this.contextStorage.setContextId(correlationId);
|
|
1447
|
+
|
|
1448
|
+
this.logger.info('Daily report generation started', {
|
|
1449
|
+
props: { correlationId },
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
try {
|
|
1453
|
+
await this.reportService.generateDailyReport();
|
|
1454
|
+
this.logger.info('Daily report generated', {
|
|
1455
|
+
props: { correlationId },
|
|
1456
|
+
});
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
this.logger.error(error, {
|
|
1459
|
+
props: { correlationId },
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
### End-to-End Correlation ID Flow Example
|
|
1467
|
+
|
|
1468
|
+
**Scenario: Sales → Kafka → Document → HTTP → Common Service**
|
|
1469
|
+
|
|
1470
|
+
**1. HTTP Entry (Sales Service):**
|
|
1471
|
+
|
|
1472
|
+
```typescript
|
|
1473
|
+
// sales.controller.ts
|
|
1474
|
+
export class SalesController {
|
|
1475
|
+
constructor(
|
|
1476
|
+
private readonly logger: LoggerService,
|
|
1477
|
+
private readonly contextStorage: ContextStorageService,
|
|
1478
|
+
private readonly kafkaProducer: KafkaProducer,
|
|
1479
|
+
) {}
|
|
1480
|
+
|
|
1481
|
+
@Post('orders')
|
|
1482
|
+
async createOrder(@Body() orderData: CreateOrderDto) {
|
|
1483
|
+
// Correlation ID set by ClsModule middleware from HTTP header
|
|
1484
|
+
// Header: x-correlation-id: abc-123-def-456
|
|
1485
|
+
|
|
1486
|
+
this.logger.info('Order creation request received', {
|
|
1487
|
+
props: { userId: orderData.userId },
|
|
1488
|
+
});
|
|
1489
|
+
// Log: { correlationId: 'abc-123-def-456', ... }
|
|
1490
|
+
|
|
1491
|
+
const order = await this.orderService.create(orderData);
|
|
1492
|
+
|
|
1493
|
+
// Extract correlationId from CLS before Kafka publish
|
|
1494
|
+
const correlationId = this.contextStorage.getContextId(); // 'abc-123-def-456'
|
|
1495
|
+
|
|
1496
|
+
// Publish to Kafka with correlationId
|
|
1497
|
+
await this.kafkaProducer.send({
|
|
1498
|
+
topic: 'order.created',
|
|
1499
|
+
messages: [{
|
|
1500
|
+
value: JSON.stringify({
|
|
1501
|
+
...order,
|
|
1502
|
+
correlationId, // Propagate: 'abc-123-def-456'
|
|
1503
|
+
}),
|
|
1504
|
+
}],
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
return order;
|
|
1508
|
+
}
|
|
1509
|
+
```
|
|
1510
|
+
|
|
1511
|
+
**2. Kafka Consumer (Document Service):**
|
|
1512
|
+
|
|
1513
|
+
```typescript
|
|
1514
|
+
// document.consumer.ts
|
|
1515
|
+
@KafkaListener('order.created')
|
|
1516
|
+
async handleOrderCreated(message: OrderCreatedEvent) {
|
|
1517
|
+
// Extract correlationId from message
|
|
1518
|
+
const correlationId = message.correlationId; // 'abc-123-def-456'
|
|
1519
|
+
|
|
1520
|
+
// CRITICAL: Set in CLS at handler start
|
|
1521
|
+
this.contextStorage.setContextId(correlationId);
|
|
1522
|
+
|
|
1523
|
+
this.logger.info('Processing order document', {
|
|
1524
|
+
props: { orderId: message.id },
|
|
1525
|
+
});
|
|
1526
|
+
// Log: { correlationId: 'abc-123-def-456', ... }
|
|
1527
|
+
|
|
1528
|
+
try {
|
|
1529
|
+
await this.documentService.createInvoice(message);
|
|
1530
|
+
|
|
1531
|
+
// Make HTTP call to Common Service with correlationId
|
|
1532
|
+
await this.httpClient.post('http://common-service/invoices', {
|
|
1533
|
+
orderId: message.id,
|
|
1534
|
+
correlationId, // Propagate: 'abc-123-def-456'
|
|
1535
|
+
}, {
|
|
1536
|
+
headers: {
|
|
1537
|
+
'x-correlation-id': correlationId, // Propagate in header
|
|
1538
|
+
},
|
|
1539
|
+
});
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
this.logger.error(error, {
|
|
1542
|
+
props: { orderId: message.id },
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
**3. HTTP Handler (Common Service):**
|
|
1549
|
+
|
|
1550
|
+
```typescript
|
|
1551
|
+
// common.controller.ts
|
|
1552
|
+
@Post('invoices')
|
|
1553
|
+
async createInvoice(@Body() data: CreateInvoiceDto, @Headers() headers: Headers) {
|
|
1554
|
+
// ClsModule middleware extracts correlationId from header
|
|
1555
|
+
// Header: x-correlation-id: abc-123-def-456
|
|
1556
|
+
|
|
1557
|
+
this.logger.info('Invoice creation request received', {
|
|
1558
|
+
props: { orderId: data.orderId },
|
|
1559
|
+
});
|
|
1560
|
+
// Log: { correlationId: 'abc-123-def-456', ... }
|
|
1561
|
+
|
|
1562
|
+
const invoice = await this.invoiceService.create(data);
|
|
1563
|
+
|
|
1564
|
+
this.logger.info('Invoice created', {
|
|
1565
|
+
props: { invoiceId: invoice.id, orderId: data.orderId },
|
|
1566
|
+
});
|
|
1567
|
+
// Log: { correlationId: 'abc-123-def-456', ... }
|
|
1568
|
+
|
|
1569
|
+
return invoice;
|
|
1570
|
+
}
|
|
1571
|
+
```
|
|
1572
|
+
|
|
1573
|
+
**4. Common Service (app.module.ts):**
|
|
1574
|
+
|
|
1575
|
+
```typescript
|
|
1576
|
+
@Module({
|
|
1577
|
+
imports: [
|
|
1578
|
+
ClsModule.forRoot({
|
|
1579
|
+
middleware: {
|
|
1580
|
+
mount: true,
|
|
1581
|
+
generateId: true,
|
|
1582
|
+
idGenerator: (req: Request) => {
|
|
1583
|
+
// Extract from header (propagated from Document Service)
|
|
1584
|
+
return req.headers['x-correlation-id'] as string || uuidv4();
|
|
1585
|
+
},
|
|
1586
|
+
},
|
|
1587
|
+
}),
|
|
1588
|
+
ContextModule,
|
|
1589
|
+
LoggerModule.forRoot(),
|
|
1590
|
+
],
|
|
1591
|
+
})
|
|
1592
|
+
export class AppModule {}
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
**Flow Summary:**
|
|
1596
|
+
|
|
1597
|
+
```
|
|
1598
|
+
HTTP Request → Sales Service
|
|
1599
|
+
Header: x-correlation-id: abc-123-def-456
|
|
1600
|
+
CLS: abc-123-def-456
|
|
1601
|
+
Log: { correlationId: 'abc-123-def-456' }
|
|
1602
|
+
|
|
1603
|
+
↓ Kafka Message
|
|
1604
|
+
|
|
1605
|
+
Kafka Topic: order.created
|
|
1606
|
+
Payload: { ..., correlationId: 'abc-123-def-456' }
|
|
1607
|
+
|
|
1608
|
+
↓ Consumer
|
|
1609
|
+
|
|
1610
|
+
Document Service
|
|
1611
|
+
CLS: abc-123-def-456 (set at handler start)
|
|
1612
|
+
Log: { correlationId: 'abc-123-def-456' }
|
|
1613
|
+
|
|
1614
|
+
↓ HTTP Request
|
|
1615
|
+
|
|
1616
|
+
HTTP Request → Common Service
|
|
1617
|
+
Header: x-correlation-id: abc-123-def-456
|
|
1618
|
+
CLS: abc-123-def-456 (extracted by middleware)
|
|
1619
|
+
Log: { correlationId: 'abc-123-def-456' }
|
|
1620
|
+
```
|
|
1621
|
+
|
|
1622
|
+
**Key Points:**
|
|
1623
|
+
|
|
1624
|
+
1. ✅ **HTTP → Kafka**: Extract from CLS, include in message payload
|
|
1625
|
+
2. ✅ **Kafka → Consumer**: Extract from message, set in CLS at handler start
|
|
1626
|
+
3. ✅ **Consumer → HTTP**: Extract from CLS, include in HTTP header
|
|
1627
|
+
4. ✅ **HTTP → Service**: CLS middleware extracts from header automatically
|
|
1628
|
+
5. ✅ **All logs**: Same correlationId throughout the flow
|
|
1629
|
+
|
|
652
1630
|
---
|
|
653
1631
|
|
|
654
1632
|
## API Reference
|