@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 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