@infineit/winston-logger 1.0.30 → 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.
Files changed (46) hide show
  1. package/README.md +2206 -114
  2. package/context/infrastructure/nestjs/contextModule.js +2 -4
  3. package/context/infrastructure/nestjs/contextModule.js.map +1 -1
  4. package/index.d.ts +19 -5
  5. package/index.js +29 -5
  6. package/index.js.map +1 -1
  7. package/logger/domain/log.d.ts +3 -1
  8. package/logger/domain/loggerService.d.ts +9 -4
  9. package/logger/domain/loggerService.js +153 -31
  10. package/logger/domain/loggerService.js.map +1 -1
  11. package/logger/domain/loggerTransport.d.ts +5 -0
  12. package/logger/domain/loggerTransport.js +5 -0
  13. package/logger/domain/loggerTransport.js.map +1 -0
  14. package/logger/domain/normalizedLog.d.ts +23 -0
  15. package/logger/domain/normalizedLog.js +37 -0
  16. package/logger/domain/normalizedLog.js.map +1 -0
  17. package/logger/infrastructure/forwarding/centralLogForwarder.d.ts +15 -0
  18. package/logger/infrastructure/forwarding/centralLogForwarder.js +81 -0
  19. package/logger/infrastructure/forwarding/centralLogForwarder.js.map +1 -0
  20. package/logger/infrastructure/forwarding/httpCentralLogForwarder.d.ts +8 -0
  21. package/logger/infrastructure/forwarding/httpCentralLogForwarder.js +88 -0
  22. package/logger/infrastructure/forwarding/httpCentralLogForwarder.js.map +1 -0
  23. package/logger/infrastructure/forwarding/kafkaCentralLogForwarder.d.ts +9 -0
  24. package/logger/infrastructure/forwarding/kafkaCentralLogForwarder.js +50 -0
  25. package/logger/infrastructure/forwarding/kafkaCentralLogForwarder.js.map +1 -0
  26. package/logger/infrastructure/nestjs/loggerModule.d.ts +16 -9
  27. package/logger/infrastructure/nestjs/loggerModule.js +86 -136
  28. package/logger/infrastructure/nestjs/loggerModule.js.map +1 -1
  29. package/logger/infrastructure/nestjs/nestjsLoggerServiceAdapter.js +61 -11
  30. package/logger/infrastructure/nestjs/nestjsLoggerServiceAdapter.js.map +1 -1
  31. package/logger/infrastructure/winston/transports/fileTransport.d.ts +1 -1
  32. package/logger/infrastructure/winston/transports/fileTransport.js +5 -2
  33. package/logger/infrastructure/winston/transports/fileTransport.js.map +1 -1
  34. package/logger/infrastructure/winston/winstonLogger.js +78 -27
  35. package/logger/infrastructure/winston/winstonLogger.js.map +1 -1
  36. package/logger/infrastructure/winston/winstonTransportAdapter.d.ts +10 -0
  37. package/logger/infrastructure/winston/winstonTransportAdapter.js +128 -0
  38. package/logger/infrastructure/winston/winstonTransportAdapter.js.map +1 -0
  39. package/package.json +10 -14
  40. package/tsconfig.lib.tsbuildinfo +1 -1
  41. package/logger/infrastructure/winston/transports/prisma-transport.d.ts +0 -11
  42. package/logger/infrastructure/winston/transports/prisma-transport.js +0 -50
  43. package/logger/infrastructure/winston/transports/prisma-transport.js.map +0 -1
  44. package/logger/levelFilter.d.ts +0 -2
  45. package/logger/levelFilter.js +0 -47
  46. package/logger/levelFilter.js.map +0 -1
package/README.md CHANGED
@@ -1,169 +1,2261 @@
1
- <br />
2
- <div align="center">
3
- <h3 align="center">NestJS Logger</h3>
1
+ # @infineit/winston-logger
4
2
 
5
- <p align="center">
6
- A Nest.js production-ready logger implementation.
7
- </p>
8
- </div>
3
+ Enterprise-level logging library for NestJS applications with support for multiple contexts (HTTP, Kafka, Bull/BullMQ, CRON, CLI) and flexible transport architecture.
9
4
 
5
+ ## Features
10
6
 
11
- ## Introduction
7
+ - ✅ **Multi-Context Support**: Works in HTTP services, Kafka consumers, Bull/BullMQ workers, CRON jobs, and CLI tasks
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)
10
+ - ✅ **Log Normalization**: Automatic normalization and error serialization before transport
11
+ - ✅ **CLS Integration**: Correlation ID support via AsyncLocalStorage (CLS)
12
+ - ✅ **Fire-and-Forget**: Non-blocking, failure-safe logging that never breaks business logic
13
+ - ✅ **Zero Dependencies**: No database, HTTP framework, or ORM dependencies (HTTP forwarding uses built-in Node.js modules)
14
+ - ✅ **Type-Safe**: Full TypeScript support with comprehensive type definitions
12
15
 
13
- `@infineit/winston-logger` is a lightweight project for implements a production-ready logger for Nest.js applications using Winston, Morgan and Prisma. This package simplifies the setup of logger and provides configurable options for instrumenting your application.
16
+ ## Table of Contents
14
17
 
18
+ - [Installation](#installation)
19
+ - [Quick Start](#quick-start)
20
+ - [Architecture Overview](#architecture-overview)
21
+ - [Configuration](#configuration)
22
+ - [Usage in Different Contexts](#usage-in-different-contexts)
23
+ - [Transport Architecture](#transport-architecture)
24
+ - [Central Log Forwarding](#central-log-forwarding)
25
+ - [Log Normalization](#log-normalization)
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)
30
+ - [API Reference](#api-reference)
31
+ - [Migration Guide (Legacy app.useLogger Users)](#migration-guide-legacy-appuselogger-users)
32
+ - [Best Practices](#best-practices)
33
+ - [Forbidden Actions](#forbidden-actions)
34
+ - [Examples](#examples)
15
35
 
16
- ## Features
36
+ ---
17
37
 
18
- - **Easy Integration**: Quickly integrate logger with winston for distributed log.
19
- - **Configurable**: Provides options to customize logger configuration.
20
- - **Lightweight**: Minimal dependencies and overhead.
38
+ ## Installation
21
39
 
22
- That implements a production-ready system advanced or basic Microservices or Monoliths projects applying concepts like:
40
+ ```bash
41
+ npm install @infineit/winston-logger
42
+ ```
23
43
 
24
- * Correlation IDs
25
- * Decoupled log transporters
26
- * Log levels
27
- * Logging Rules
28
- * Log formatters
44
+ ### Peer Dependencies
29
45
 
30
- ## Installation
46
+ This package requires the following peer dependencies (usually already installed in NestJS projects):
31
47
 
32
- * You can install this package using npm:
33
- ```bash
34
- npm install @infineit/winston-logger
35
- ```
48
+ - `@nestjs/common` 11.1.9
49
+ - `@nestjs/config` 4.2.0
50
+ - `@nestjs/core` 11.1.9
51
+ - `nestjs-cls` 5.0.1
36
52
 
37
- Or using Yarn:
38
- ```bash
39
- yarn add @infineit/winston-logger
40
- ```
53
+ ---
41
54
 
42
- ### Prerequisites
55
+ ## Quick Start
43
56
 
44
- - NestJS
45
- - Winston
46
- - morgon
47
- - prisma
57
+ ### 1. Import Modules
48
58
 
49
- ### Getting Started
59
+ ```typescript
60
+ import { Module } from '@nestjs/common';
61
+ import { ConfigModule } from '@nestjs/config';
62
+ import { ContextModule, LoggerModule } from '@infineit/winston-logger';
50
63
 
51
- Follow these steps to set up and run logger. If all steps are followed correctly, logger should start without any issues:
64
+ @Module({
65
+ imports: [
66
+ ConfigModule.forRoot({ isGlobal: true }),
67
+ ContextModule, // Required for CLS (correlation ID support)
68
+ LoggerModule.forRoot(), // Configure logger
69
+ ],
70
+ })
71
+ export class AppModule {}
72
+ ```
52
73
 
53
- 1. To use `@infineit/winston-logger` in your NestJS application, simply import it of your `main.ts` file:
74
+ ### 2. Use LoggerService
54
75
 
55
- ```bash
56
- import { NestjsLoggerServiceAdapter } from '@infineit/winston-logger';;
76
+ ```typescript
77
+ import { Injectable } from '@nestjs/common';
78
+ import { LoggerService } from '@infineit/winston-logger';
57
79
 
58
- const app = await NestFactory.create(AppModule, {bufferLogs: true });
80
+ @Injectable()
81
+ export class MyService {
82
+ constructor(private readonly logger: LoggerService) {}
59
83
 
60
- app.useLogger(app.get(NestjsLoggerServiceAdapter));
61
- ```
84
+ doSomething() {
85
+ this.logger.info('Processing started', {
86
+ userId: 123,
87
+ action: 'process',
88
+ });
62
89
 
63
- 2. import in app.module.ts:
90
+ try {
91
+ // Your business logic
92
+ } catch (error) {
93
+ this.logger.error(error, {
94
+ userId: 123,
95
+ action: 'process',
96
+ });
97
+ }
98
+ }
99
+ }
100
+ ```
64
101
 
65
- ```bash
66
- import { ContextModule, LoggerModule } from '@infineit/winston-logger';
102
+ ---
67
103
 
68
- @Module({
69
- imports: [
70
- ContextModule,
71
- LoggerModule.forRoot(PrismaService),
72
- ]
73
- })
74
- ```
104
+ ## Architecture Overview
75
105
 
76
- 3. app.service.ts:
106
+ ### LoggerService → LoggerTransport[] Flow
77
107
 
78
- ```bash
79
- import Logger, { LoggerKey } from '@infineit/winston-logger';
108
+ ```
109
+ ┌─────────────────────────────────────────────────────────────┐
110
+ │ Application Code │
111
+ │ (HTTP Services, Kafka Consumers, Bull Workers, etc.) │
112
+ └────────────────────┬────────────────────────────────────────┘
113
+
114
+
115
+ ┌─────────────────────────────────────────────────────────────┐
116
+ │ LoggerService │
117
+ │ • Normalizes logs │
118
+ │ • Serializes errors │
119
+ │ • Retrieves correlationId from CLS │
120
+ │ • Sends to all transports (fire-and-forget) │
121
+ └────────────────────┬────────────────────────────────────────┘
122
+
123
+
124
+ ┌───────────────────────┐
125
+ │ NormalizedLog │
126
+ │ • timestamp │
127
+ │ • level │
128
+ │ • message │
129
+ │ • error (serialized)│
130
+ │ • correlationId │
131
+ │ • ... │
132
+ └───────────────────────┘
133
+
134
+
135
+ ┌─────────────────────────────────────────────────────────────┐
136
+ │ LoggerTransport[] (Array) │
137
+ └────────────────────┬────────────────────────────────────────┘
138
+
139
+ ┌────────────┼────────────┐
140
+ │ │ │
141
+ ▼ ▼ ▼
142
+ ┌──────────────┐ ┌──────────┐ ┌──────────┐
143
+ │WinstonTransport│ │ Custom │ │ Custom │
144
+ │ Adapter │ │ Transport│ │ Transport│
145
+ │ │ │ │ │ │
146
+ │ • Console │ │ • Kafka │ │ • HTTP │
147
+ │ • File │ │ • Database│ │ • etc. │
148
+ │ • Slack │ │ │ │ │
149
+ └──────────────┘ └──────────┘ └──────────┘
150
+ ```
80
151
 
81
- constructor(@Inject(LoggerKey) private logger: Logger) {}
152
+ ### Key Components
82
153
 
83
- this.logger.info('I am an info message!', {
84
- props: {
85
- foo: 'bar',
86
- baz: 'qux',
87
- },
88
- });
89
- ```
154
+ 1. **LoggerService**: Main logging service that normalizes logs and sends them to transports
155
+ 2. **LoggerTransport**: Interface for all log transports (Winston, Kafka, HTTP, Database, etc.)
156
+ 3. **WinstonTransportAdapter**: Wraps Winston as one transport among many
157
+ 4. **ContextModule**: Provides CLS (AsyncLocalStorage) for correlation ID management
158
+ 5. **NormalizedLog**: Standardized log format sent to all transports
90
159
 
91
- **Reminder**: Make sure prisma is initialize and winstonlog modal is running before starting your NestJS application to avoid initialization errors.
160
+ ---
92
161
 
93
162
  ## Configuration
94
163
 
95
- You can configure the Logger using environment variables in your `.env` file:
164
+ ### LoggerModule Configuration
165
+
166
+ ```typescript
167
+ import { LoggerModule, LoggerModuleConfig } from '@infineit/winston-logger';
168
+
169
+ const config: LoggerModuleConfig = {
170
+ nodeEnv: 'production', // 'production' | 'testing' | 'development'
171
+ slack_webhook: 'https://...', // Slack webhook URL (optional)
172
+ console_print: true, // Enable console output (boolean or 'true'/'false')
173
+ log_in_file: true, // Enable file logging (boolean or 'true'/'false')
174
+ organization: 'my-org', // Organization name (optional)
175
+ context: 'user-service', // Bounded context name (optional)
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')
182
+ };
183
+
184
+ @Module({
185
+ imports: [
186
+ LoggerModule.forRoot(config),
187
+ ],
188
+ })
189
+ export class AppModule {}
190
+ ```
191
+
192
+ ### Configuration via Environment Variables
193
+
194
+ You can also configure via `@nestjs/config`:
195
+
196
+ ```typescript
197
+ // .env
198
+ NODE_ENV=production
199
+ SLACK_WEBHOOK=https://hooks.slack.com/services/...
200
+ CONSOLE_PRINT=true
201
+ LOG_IN_FILE=true
202
+ LOGGER_ORGANIZATION=my-org
203
+ LOGGER_CONTEXT=user-service
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
213
+ ```
214
+
215
+ ```typescript
216
+ // logger.config.ts
217
+ import { registerAs } from '@nestjs/config';
218
+
219
+ export default registerAs('logger', () => ({
220
+ nodeEnv: process.env.NODE_ENV,
221
+ slack_webhook: process.env.SLACK_WEBHOOK,
222
+ console_print: process.env.CONSOLE_PRINT,
223
+ log_in_file: process.env.LOG_IN_FILE,
224
+ organization: process.env.LOGGER_ORGANIZATION,
225
+ context: process.env.LOGGER_CONTEXT,
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,
232
+ }));
233
+ ```
234
+
235
+ ```typescript
236
+ @Module({
237
+ imports: [
238
+ ConfigModule.forRoot({
239
+ isGlobal: true,
240
+ load: [loggerConfig],
241
+ }),
242
+ LoggerModule.forRoot(), // Will read from ConfigService
243
+ ],
244
+ })
245
+ export class AppModule {}
246
+ ```
247
+
248
+ ### Configuration Options
249
+
250
+ | Option | Type | Required | Default | Description |
251
+ |--------|------|----------|---------|-------------|
252
+ | `nodeEnv` | `string` | No | `undefined` | Environment: `'production'`, `'testing'`, or `'development'` |
253
+ | `slack_webhook` | `string` | No | `undefined` | Slack webhook URL for fatal error notifications |
254
+ | `console_print` | `boolean \| string` | No | `false` | Enable console output (`true`/`false` or `'true'`/`'false'`) |
255
+ | `log_in_file` | `boolean \| string` | No | `false` | Enable file logging (`true`/`false` or `'true'`/`'false'`) |
256
+ | `organization` | `string` | No | `undefined` | Organization or project name |
257
+ | `context` | `string` | No | `undefined` | Bounded context name |
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'`) |
263
+
264
+ ### Default Behavior
265
+
266
+ - **Development/Testing**: Console and file logging enabled by default
267
+ - **Production**: Console and file logging disabled by default (unless explicitly enabled)
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
+ ```
305
+
306
+ ---
307
+
308
+ ## Usage in Different Contexts
309
+
310
+ ### HTTP Services
311
+
312
+ ```typescript
313
+ import { Injectable } from '@nestjs/common';
314
+ import { LoggerService } from '@infineit/winston-logger';
315
+
316
+ @Injectable()
317
+ export class UserController {
318
+ constructor(private readonly logger: LoggerService) {}
319
+
320
+ @Get('/users/:id')
321
+ async getUser(@Param('id') id: string) {
322
+ // Correlation ID is automatically retrieved from CLS
323
+ // (if CLS middleware is configured in your app)
324
+ this.logger.info('Fetching user', { userId: id });
325
+
326
+ try {
327
+ const user = await this.userService.findById(id);
328
+ return user;
329
+ } catch (error) {
330
+ this.logger.error(error, { userId: id });
331
+ throw error;
332
+ }
333
+ }
334
+ }
335
+ ```
336
+
337
+ **Important**: For HTTP services, you must configure CLS middleware manually:
338
+
339
+ ```typescript
340
+ import { ClsModule } from 'nestjs-cls';
341
+
342
+ @Module({
343
+ imports: [
344
+ ClsModule.forRoot({
345
+ middleware: {
346
+ mount: true, // Enable middleware
347
+ generateId: true, // Auto-generate correlation ID
348
+ idGenerator: (req: Request) => req.headers['x-correlation-id'] || uuidv4(),
349
+ },
350
+ }),
351
+ ContextModule, // Provides CLS service
352
+ LoggerModule.forRoot(),
353
+ ],
354
+ })
355
+ export class AppModule {}
356
+ ```
357
+
358
+ ### Kafka Consumers
359
+
360
+ ```typescript
361
+ import { Injectable } from '@nestjs/common';
362
+ import { LoggerService } from '@infineit/winston-logger';
363
+ import { ContextStorageService } from '@infineit/winston-logger';
364
+
365
+ @Injectable()
366
+ export class OrderConsumer {
367
+ constructor(
368
+ private readonly logger: LoggerService,
369
+ private readonly contextStorage: ContextStorageService,
370
+ ) {}
371
+
372
+ @KafkaListener('order.created')
373
+ async handleOrderCreated(message: OrderCreatedEvent) {
374
+ // Set correlation ID from Kafka message
375
+ this.contextStorage.setContextId(message.correlationId || message.id);
376
+
377
+ this.logger.info('Processing order', {
378
+ orderId: message.id,
379
+ userId: message.userId,
380
+ });
381
+
382
+ try {
383
+ await this.processOrder(message);
384
+ } catch (error) {
385
+ this.logger.error(error, {
386
+ orderId: message.id,
387
+ });
388
+ }
389
+ }
390
+ }
391
+ ```
392
+
393
+ ### Bull/BullMQ Workers
394
+
395
+ ```typescript
396
+ import { Injectable } from '@nestjs/common';
397
+ import { Processor, Process } from '@nestjs/bull';
398
+ import { Job } from 'bull';
399
+ import { LoggerService } from '@infineit/winston-logger';
400
+ import { ContextStorageService } from '@infineit/winston-logger';
401
+
402
+ @Processor('email')
403
+ export class EmailProcessor {
404
+ constructor(
405
+ private readonly logger: LoggerService,
406
+ private readonly contextStorage: ContextStorageService,
407
+ ) {}
408
+
409
+ @Process('send')
410
+ async handleSendEmail(job: Job<EmailJob>) {
411
+ // Set correlation ID from job
412
+ this.contextStorage.setContextId(job.id.toString());
413
+
414
+ this.logger.info('Sending email', {
415
+ jobId: job.id,
416
+ email: job.data.to,
417
+ });
418
+
419
+ try {
420
+ await this.emailService.send(job.data);
421
+ this.logger.info('Email sent', {
422
+ jobId: job.id,
423
+ durationMs: Date.now() - job.timestamp,
424
+ });
425
+ } catch (error) {
426
+ this.logger.error(error, {
427
+ jobId: job.id,
428
+ });
429
+ throw error;
430
+ }
431
+ }
432
+ }
433
+ ```
434
+
435
+ ### CRON Jobs
436
+
437
+ ```typescript
438
+ import { Injectable } from '@nestjs/common';
439
+ import { Cron, CronExpression } from '@nestjs/schedule';
440
+ import { LoggerService } from '@infineit/winston-logger';
441
+ import { ContextStorageService } from '@infineit/winston-logger';
442
+
443
+ @Injectable()
444
+ export class CleanupService {
445
+ constructor(
446
+ private readonly logger: LoggerService,
447
+ private readonly contextStorage: ContextStorageService,
448
+ ) {}
449
+
450
+ @Cron(CronExpression.EVERY_HOUR)
451
+ async cleanup() {
452
+ // Set correlation ID for this job
453
+ this.contextStorage.setContextId(`cleanup-${Date.now()}`);
454
+
455
+ this.logger.info('Starting cleanup job');
456
+
457
+ try {
458
+ const deleted = await this.repository.deleteOldRecords();
459
+ this.logger.info('Cleanup completed', {
460
+ deletedCount: deleted,
461
+ });
462
+ } catch (error) {
463
+ this.logger.error(error);
464
+ }
465
+ }
466
+ }
467
+ ```
468
+
469
+ ### CLI Tasks
470
+
471
+ ```typescript
472
+ import { Injectable } from '@nestjs/common';
473
+ import { LoggerService } from '@infineit/winston-logger';
474
+ import { ContextStorageService } from '@infineit/winston-logger';
475
+
476
+ @Injectable()
477
+ export class MigrationService {
478
+ constructor(
479
+ private readonly logger: LoggerService,
480
+ private readonly contextStorage: ContextStorageService,
481
+ ) {}
482
+
483
+ async runMigration() {
484
+ // Set correlation ID for this task
485
+ this.contextStorage.setContextId(`migration-${Date.now()}`);
486
+
487
+ this.logger.info('Starting migration');
488
+
489
+ try {
490
+ await this.migrate();
491
+ this.logger.info('Migration completed');
492
+ } catch (error) {
493
+ this.logger.error(error);
494
+ process.exit(1);
495
+ }
496
+ }
497
+ }
498
+ ```
499
+
500
+ ---
501
+
502
+ ## Transport Architecture
503
+
504
+ ### Built-in Transports
505
+
506
+ The library includes `WinstonTransportAdapter` which wraps Winston and provides:
507
+
508
+ - **ConsoleTransport**: Colorized console output
509
+ - **FileTransport**: Daily rotating file logs (`logs/log-YYYY-MM-DD-HH.log`)
510
+ - **SlackTransport**: Fatal error notifications to Slack
511
+
512
+ ### Custom Transports
513
+
514
+ You can create custom transports by implementing the `LoggerTransport` interface:
515
+
516
+ ```typescript
517
+ import { Injectable } from '@nestjs/common';
518
+ import { LoggerTransport, NormalizedLog } from '@infineit/winston-logger';
519
+
520
+ @Injectable()
521
+ export class KafkaTransport implements LoggerTransport {
522
+ constructor(private readonly kafkaProducer: KafkaProducer) {}
523
+
524
+ log(normalizedLog: NormalizedLog): void {
525
+ try {
526
+ // Send to Kafka (fire-and-forget)
527
+ this.kafkaProducer.send({
528
+ topic: 'logs',
529
+ messages: [{
530
+ value: JSON.stringify(normalizedLog),
531
+ }],
532
+ });
533
+ } catch (error) {
534
+ // Swallow errors - never break business logic
535
+ console.error('KafkaTransport error (swallowed):', error);
536
+ }
537
+ }
538
+ }
539
+ ```
540
+
541
+ ### Registering Custom Transports
542
+
543
+ ```typescript
544
+ import { Provider } from '@nestjs/common';
545
+ import { LoggerModule } from '@infineit/winston-logger';
546
+ import { KafkaTransport } from './kafka-transport';
547
+
548
+ const customTransports: Provider[] = [
549
+ {
550
+ provide: KafkaTransport,
551
+ useClass: KafkaTransport,
552
+ },
553
+ ];
554
+
555
+ @Module({
556
+ imports: [
557
+ LoggerModule.forRoot(config, customTransports),
558
+ ],
559
+ })
560
+ export class AppModule {}
561
+ ```
562
+
563
+ ### Transport Requirements
564
+
565
+ All transports must:
566
+
567
+ 1. ✅ Implement `LoggerTransport` interface
568
+ 2. ✅ Accept `NormalizedLog` (never raw `Error` objects)
569
+ 3. ✅ Be fire-and-forget (non-blocking)
570
+ 4. ✅ Never throw errors
571
+ 5. ✅ Handle failures gracefully
572
+
573
+ ---
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
96
683
 
97
- - `NODE_ENV`: development / staging / testing / production.
98
- - `LOGGER_ORGANIZATION`: The name of your organization as it will appear in log.
99
- - `LOGGER_CONTEXT`: The name of your context as it will appear in log.
100
- - `LOGGER_APP`: The name of your app as it will appear in log.
101
- - `LOGGER_DATABASE_STORAGE`: True/False. Default True for production or testing, It will store log to database.
102
- - `LOGGER_LOG_LEVEL`: Default log level warn,error,fatal for production or testing, It will log.
103
- - `LOGGER_DURATION`: True/False. Default False, It will store request duration in database.
104
- - `LOGGER_DURATION_LOG_LEVEL`: Default log level warn,error,fatal for production or testing, It will calculate duration for request.
105
- - `LOGGER_CONSOLE_PRINT`: True/False. Default False for production or testing.
106
- - `LOGGER_LOG_IN_FILE`: True/False. Default False for production or testing.
107
- - `LOGGER_SLACK_INC_WEBHOOK_URL`: Slack url, eg. `https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX`.
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
108
687
 
109
- ## Project structure
688
+ **No changes required:** If CLS is configured, correlationId is automatically included in forwarded logs.
110
689
 
111
- ## Key concepts
690
+ ### Fire-and-Forget Behavior
112
691
 
113
- ### NestJS Logger
692
+ Central log forwarding is completely fire-and-forget:
114
693
 
115
- NestJS uses a custom logger for bootstrap and internal logging. To use our logger, we need to create an adapter that implements the NestJS `LoggerService` interface. That is implemented in the `NestjsLoggerServiceAdapter` class.
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
116
698
 
117
- We pass that adapter to the NestJS app on the `main.ts` file.
699
+ **Important:** Original logging behavior remains completely unchanged. Forwarding is an additional, optional layer that runs in parallel.
118
700
 
119
- ### Correlation IDs
701
+ ### Environment-Specific Configuration
120
702
 
121
- To manage correlation IDs, we use `nestjs-cls` library that implements a Local Storage. With that, we can isolate and share data on a request lifecycle.
703
+ **Development:**
704
+ ```bash
705
+ # Option 1: Disable forwarding in development
706
+ LOGGER_FORWARD_TO_CENTRAL=false
122
707
 
123
- The system reads the `x-correlation-id` HTTP header and stores it on the Local Storage. If the header is not present, the system generates a new UUID and stores it on the Local Storage.
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
+ ```
124
713
 
125
- ### Context Wrapper
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
126
720
 
127
- To add custom data to all the logs, we use a wrapper `LoggerContextWrapper`.
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
+ ```
128
726
 
129
- That class is injected with a Transient scope. By that, we can get the caller class and add it to the logs.
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
130
732
 
131
- ## Prisma Table
733
+ # Document project
734
+ LOGGER_KAFKA_TOPIC=project-logs-document
132
735
 
133
- ```
134
- model winstonlog {
135
- id_log String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
136
- level String @db.VarChar(80)
137
- message String @db.Text
138
- context String? @db.VarChar(255)
139
- correlationId String? @db.Uuid
140
- sourceClass String? @db.VarChar(255)
141
- props Json?
142
- organization String? @db.VarChar(40)
143
- app String? @db.VarChar(40)
144
- durationMs Decimal? @default(0) @db.Decimal(10, 4)
145
- stack String? @db.Text
146
- label String? @db.VarChar(40)
147
- timestamp DateTime @default(now()) @db.Timestamptz(6)
736
+ # Common service
737
+ LOGGER_KAFKA_TOPIC=project-logs-common
738
+ ```
148
739
 
149
- @@schema("public")
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
+
764
+ ## Log Normalization
765
+
766
+ ### Automatic Normalization
767
+
768
+ All logs are automatically normalized before being sent to transports:
769
+
770
+ ```typescript
771
+ // You can pass Error objects or LogData with errors
772
+ this.logger.error(new Error('Something went wrong'), {
773
+ userId: 123,
774
+ });
775
+
776
+ // LoggerService automatically:
777
+ // 1. Serializes the Error to SerializedError
778
+ // 2. Creates NormalizedLog with all required fields
779
+ // 3. Sends to all transports
780
+ ```
781
+
782
+ ### NormalizedLog Structure
783
+
784
+ ```typescript
785
+ interface NormalizedLog {
786
+ timestamp: number; // Unix timestamp in milliseconds
787
+ level: LogLevel; // 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'emergency'
788
+ message: string; // Log message
789
+ organization?: string; // From config
790
+ context?: string; // From config
791
+ app?: string; // From config
792
+ sourceClass?: string; // Auto-detected from class name
793
+ correlationId?: string; // From CLS (can be undefined)
794
+ error?: SerializedError; // Serialized error (never raw Error)
795
+ props?: Record<string, any>; // Custom properties
796
+ durationMs?: number; // Duration in milliseconds
797
+ label?: string; // Auto-generated: `${organization}.${context}.${app}`
798
+ stack?: string; // Error stack trace (if error present)
799
+ }
800
+ ```
801
+
802
+ ### Error Serialization
803
+
804
+ Errors are automatically serialized using `serializeError()`:
805
+
806
+ ```typescript
807
+ import { serializeError } from '@infineit/winston-logger';
808
+
809
+ const error = new Error('Test error');
810
+ error.code = 'ERR_TEST';
811
+
812
+ const serialized = serializeError(error);
813
+ // {
814
+ // name: 'Error',
815
+ // message: 'Test error',
816
+ // stack: 'Error: Test error\n at ...',
817
+ // code: 'ERR_TEST'
818
+ // }
819
+ ```
820
+
821
+ **Important**: If you pass errors in `LogData`, they must already be serialized:
822
+
823
+ ```typescript
824
+ import { serializeError } from '@infineit/winston-logger';
825
+
826
+ const error = new Error('Test');
827
+ this.logger.info('Message', {
828
+ error: serializeError(error), // Must serialize manually if in LogData
829
+ });
830
+ ```
831
+
832
+ ---
833
+
834
+ ## Correlation ID (CLS)
835
+
836
+ ### Automatic Retrieval
837
+
838
+ `LoggerService` automatically retrieves correlation ID from CLS:
839
+
840
+ ```typescript
841
+ // If CLS is configured, correlationId is automatically included
842
+ this.logger.info('Message');
843
+ // Log includes: { correlationId: '...' } (from CLS)
844
+ ```
845
+
846
+ ### Manual Setting (Non-HTTP Contexts)
847
+
848
+ For non-HTTP contexts (Kafka, Bull, CRON, CLI), set correlation ID manually:
849
+
850
+ ```typescript
851
+ import { ContextStorageService } from '@infineit/winston-logger';
852
+
853
+ constructor(
854
+ private readonly logger: LoggerService,
855
+ private readonly contextStorage: ContextStorageService,
856
+ ) {}
857
+
858
+ async processMessage(message: Message) {
859
+ // Set correlation ID from message
860
+ this.contextStorage.setContextId(message.correlationId);
861
+
862
+ this.logger.info('Processing message');
863
+ // Log includes: { correlationId: message.correlationId }
864
+ }
865
+ ```
866
+
867
+ ### Optional Correlation ID
868
+
869
+ Correlation ID is **optional** - `undefined` is valid:
870
+
871
+ ```typescript
872
+ // If no correlation ID is set, logs will have:
873
+ // { correlationId: undefined }
874
+ // This is perfectly valid and expected in some contexts
875
+ ```
876
+
877
+ ### HTTP Context Setup
878
+
879
+ For HTTP services, configure CLS middleware:
880
+
881
+ ```typescript
882
+ import { ClsModule } from 'nestjs-cls';
883
+ import { v4 as uuidv4 } from 'uuid';
884
+
885
+ @Module({
886
+ imports: [
887
+ ClsModule.forRoot({
888
+ middleware: {
889
+ mount: true,
890
+ generateId: true,
891
+ idGenerator: (req: Request) => {
892
+ // Use existing correlation ID from header, or generate new one
893
+ return req.headers['x-correlation-id'] as string || uuidv4();
894
+ },
895
+ },
896
+ }),
897
+ ContextModule, // Provides CLS service
898
+ LoggerModule.forRoot(),
899
+ ],
900
+ })
901
+ export class AppModule {}
902
+ ```
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;
150
1085
  }
151
- ```
152
- ## Inspiration
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
+ ```
153
1211
 
154
- This project is inspired by the excellent work from:
1212
+ ### Kafka Producer - Propagating Correlation ID
155
1213
 
156
- - [Medium](https://medium.com/@jose-luis-navarro/logging-on-nestjs-like-a-pro-with-correlation-ids-log-aggregation-winston-morgan-and-more-d03e3bb56772)
157
- - [Logger](https://github.com/jnm733/nestjs-logger/tree/main)
158
- - [winston-pg](https://github.com/InnovA2/winston-pg/tree/main)
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
+
1630
+ ---
1631
+
1632
+ ## API Reference
1633
+
1634
+ ### LoggerService
1635
+
1636
+ Main logging service injected into your classes.
1637
+
1638
+ #### Methods
1639
+
1640
+ ```typescript
1641
+ // Generic log method
1642
+ log(level: LogLevel, message: string | Error, data?: LogData, profile?: string): void
1643
+
1644
+ // Convenience methods
1645
+ debug(message: string, data?: LogData, profile?: string): void
1646
+ info(message: string, data?: LogData, profile?: string): void
1647
+ warn(message: string | Error, data?: LogData, profile?: string): void
1648
+ error(message: string | Error, data?: LogData, profile?: string): void
1649
+ fatal(message: string | Error, data?: LogData, profile?: string): void
1650
+ emergency(message: string | Error, data?: LogData, profile?: string): void
1651
+
1652
+ // Profile (currently not implemented in transport architecture)
1653
+ startProfile(id: string): void
1654
+ ```
1655
+
1656
+ #### LogLevel
1657
+
1658
+ ```typescript
1659
+ enum LogLevel {
1660
+ Emergency = 'emergency', // One or more systems are unusable
1661
+ Fatal = 'fatal', // A person must take an action immediately
1662
+ Error = 'error', // Error events are likely to cause problems
1663
+ Warn = 'warn', // Warning events might cause problems
1664
+ Info = 'info', // Routine information
1665
+ Debug = 'debug', // Debug or trace information
1666
+ }
1667
+ ```
1668
+
1669
+ #### LogData
1670
+
1671
+ ```typescript
1672
+ interface LogData {
1673
+ organization?: string; // Override config organization
1674
+ context?: string; // Override config context
1675
+ app?: string; // Override config app
1676
+ sourceClass?: string; // Override auto-detected class name
1677
+ correlationId?: string; // Override CLS correlation ID
1678
+ error?: SerializedError; // Error (must be serialized)
1679
+ props?: Record<string, any>; // Custom properties
1680
+ durationMs?: number; // Duration in milliseconds
1681
+ }
1682
+ ```
1683
+
1684
+ ### ContextStorageService
1685
+
1686
+ Service for managing CLS context (correlation ID).
1687
+
1688
+ #### Methods
1689
+
1690
+ ```typescript
1691
+ // Get correlation ID from CLS
1692
+ getContextId(): string | undefined
1693
+
1694
+ // Set correlation ID in CLS
1695
+ setContextId(id: string): void
1696
+
1697
+ // Get any value from CLS
1698
+ get<T>(key: string): T | undefined
1699
+
1700
+ // Set any value in CLS
1701
+ set<T>(key: string, value: T): void
1702
+ ```
1703
+
1704
+ ---
1705
+
1706
+ ## Migration Guide (Legacy app.useLogger Users)
1707
+
1708
+ ### ⚠️ Deprecated: app.useLogger() and app.flushLogs()
1709
+
1710
+ If you're migrating from an older version of `@infineit/winston-logger` that used `app.useLogger()` and `app.flushLogs()`, this section is for you.
1711
+
1712
+ **Status**: These methods have been **removed** and are **no longer available**.
1713
+
1714
+ ### Why Was It Removed?
1715
+
1716
+ The `app.useLogger()` and `app.flushLogs()` methods were removed for the following architectural reasons:
1717
+
1718
+ 1. **HTTP Assumption Violation**: These methods assumed HTTP runtime, making the logger unusable in Kafka consumers, Bull workers, CRON jobs, and CLI tasks.
1719
+
1720
+ 2. **Middleware Auto-Mounting**: The old pattern auto-mounted middleware, violating the library's core principle that applications must configure middleware themselves.
1721
+
1722
+ 3. **Transport Architecture**: The new architecture uses a pluggable `LoggerTransport[]` system where Winston is one transport among many, not the only transport.
1723
+
1724
+ 4. **Fire-and-Forget Guarantee**: The new architecture ensures all logging is truly fire-and-forget without requiring explicit `flushLogs()` calls.
1725
+
1726
+ ### Old Deprecated Usage ❌
1727
+
1728
+ ```typescript
1729
+ // ❌ DEPRECATED - This no longer works
1730
+ import { NestFactory } from '@nestjs/core';
1731
+ import { LoggerModule } from '@infineit/winston-logger';
1732
+
1733
+ async function bootstrap() {
1734
+ const app = await NestFactory.create(AppModule);
1735
+
1736
+ // ❌ DEPRECATED: app.useLogger() is removed
1737
+ app.useLogger(app.get(LoggerModule));
1738
+
1739
+ await app.listen(3000);
1740
+
1741
+ // ❌ DEPRECATED: app.flushLogs() is removed
1742
+ app.flushLogs();
1743
+ }
1744
+
1745
+ bootstrap();
1746
+ ```
1747
+
1748
+ ### New Correct Usage ✅
1749
+
1750
+ ```typescript
1751
+ // ✅ CORRECT: Use LoggerService injection
1752
+ import { NestFactory } from '@nestjs/core';
1753
+ import { ContextModule, LoggerModule } from '@infineit/winston-logger';
1754
+
1755
+ @Module({
1756
+ imports: [
1757
+ ContextModule,
1758
+ LoggerModule.forRoot(),
1759
+ ],
1760
+ })
1761
+ export class AppModule {}
1762
+
1763
+ async function bootstrap() {
1764
+ const app = await NestFactory.create(AppModule);
1765
+
1766
+ // ✅ No app.useLogger() needed - LoggerService is automatically available
1767
+ // ✅ No app.flushLogs() needed - logging is fire-and-forget
1768
+
1769
+ await app.listen(3000);
1770
+ }
1771
+
1772
+ bootstrap();
1773
+ ```
1774
+
1775
+ ### Using LoggerService in Your Code
1776
+
1777
+ ```typescript
1778
+ // ✅ CORRECT: Inject LoggerService
1779
+ import { Injectable } from '@nestjs/common';
1780
+ import { LoggerService } from '@infineit/winston-logger';
1781
+
1782
+ @Injectable()
1783
+ export class MyService {
1784
+ constructor(private readonly logger: LoggerService) {}
1785
+
1786
+ doSomething() {
1787
+ // ✅ Logging is automatic and fire-and-forget
1788
+ this.logger.info('Processing started');
1789
+
1790
+ // ✅ No need to flush - logs are sent immediately
1791
+ // ✅ No need to await - logging is non-blocking
1792
+ }
1793
+ }
1794
+ ```
1795
+
1796
+ ### Comparison Table
1797
+
1798
+ | Feature | Old (Deprecated) ❌ | New (Current) ✅ |
1799
+ |---------|---------------------|------------------|
1800
+ | **Setup** | `app.useLogger(...)` | `LoggerModule.forRoot()` in imports |
1801
+ | **Flush** | `app.flushLogs()` required | Not needed (fire-and-forget) |
1802
+ | **Usage** | Direct logger access | Inject `LoggerService` |
1803
+ | **HTTP Only** | Yes (assumed HTTP runtime) | No (works in all contexts) |
1804
+ | **Middleware** | Auto-mounted | Manual configuration |
1805
+ | **Transports** | Winston only | Pluggable `LoggerTransport[]` |
1806
+ | **Blocking** | Could block with `flushLogs()` | Always non-blocking |
1807
+ | **Context Support** | HTTP only | HTTP, Kafka, Bull, CRON, CLI |
1808
+
1809
+ ### Key Changes Summary
1810
+
1811
+ #### ❌ What's Removed
1812
+
1813
+ - `app.useLogger()` - **No longer exists**
1814
+ - `app.flushLogs()` - **No longer exists**
1815
+ - HTTP-only assumptions - **Removed**
1816
+ - Auto-mounted middleware - **Removed**
1817
+
1818
+ #### ✅ What's New
1819
+
1820
+ - **LoggerService injection** - Inject `LoggerService` into your classes
1821
+ - **Multi-context support** - Works in HTTP, Kafka, Bull, CRON, CLI
1822
+ - **Transport architecture** - Pluggable transports via `LoggerTransport[]`
1823
+ - **Automatic normalization** - Logs are normalized before transport
1824
+ - **CLS integration** - Correlation ID support via AsyncLocalStorage
1825
+
1826
+ ### Migration Steps
1827
+
1828
+ 1. **Remove `app.useLogger()` calls**
1829
+ ```typescript
1830
+ // ❌ Remove this
1831
+ app.useLogger(app.get(LoggerModule));
1832
+ ```
1833
+
1834
+ 2. **Remove `app.flushLogs()` calls**
1835
+ ```typescript
1836
+ // ❌ Remove this
1837
+ app.flushLogs();
1838
+ ```
1839
+
1840
+ 3. **Add LoggerModule to imports**
1841
+ ```typescript
1842
+ @Module({
1843
+ imports: [
1844
+ ContextModule, // Required for CLS
1845
+ LoggerModule.forRoot(), // Add this
1846
+ ],
1847
+ })
1848
+ export class AppModule {}
1849
+ ```
1850
+
1851
+ 4. **Inject LoggerService instead of direct logger access**
1852
+ ```typescript
1853
+ // ❌ Old way (if you had direct logger access)
1854
+ // logger.info('message');
1855
+
1856
+ // ✅ New way
1857
+ constructor(private readonly logger: LoggerService) {}
1858
+ this.logger.info('message');
1859
+ ```
1860
+
1861
+ 5. **Configure CLS middleware (for HTTP services)**
1862
+ ```typescript
1863
+ // ✅ Add CLS middleware configuration
1864
+ ClsModule.forRoot({
1865
+ middleware: {
1866
+ mount: true,
1867
+ generateId: true,
1868
+ },
1869
+ }),
1870
+ ```
1871
+
1872
+ ### Reassurance: Nothing Breaks
1873
+
1874
+ **Good news**: Your existing logging calls will continue to work! The only changes needed are:
1875
+
1876
+ 1. ✅ Remove `app.useLogger()` and `app.flushLogs()` calls
1877
+ 2. ✅ Add `LoggerModule.forRoot()` to your module imports
1878
+ 3. ✅ Inject `LoggerService` where you need logging
1879
+
1880
+ **Logging behavior**:
1881
+ - ✅ Still fire-and-forget (no `flushLogs()` needed)
1882
+ - ✅ Still non-blocking
1883
+ - ✅ Still failure-safe (errors are swallowed)
1884
+ - ✅ Still works in all contexts (HTTP, Kafka, Bull, etc.)
1885
+
1886
+ **No breaking changes to logging API**:
1887
+ - ✅ Same log levels: `debug`, `info`, `warn`, `error`, `fatal`, `emergency`
1888
+ - ✅ Same method signatures
1889
+ - ✅ Same `LogData` structure
1890
+ - ✅ Same error handling
1891
+
1892
+ ### Example: Complete Migration
1893
+
1894
+ **Before (Deprecated)**:
1895
+ ```typescript
1896
+ import { NestFactory } from '@nestjs/core';
1897
+ import { LoggerModule } from '@infineit/winston-logger';
1898
+
1899
+ @Module({
1900
+ imports: [LoggerModule],
1901
+ })
1902
+ export class AppModule {}
1903
+
1904
+ async function bootstrap() {
1905
+ const app = await NestFactory.create(AppModule);
1906
+ app.useLogger(app.get(LoggerModule)); // ❌ Remove
1907
+ await app.listen(3000);
1908
+ app.flushLogs(); // ❌ Remove
1909
+ }
1910
+ ```
1911
+
1912
+ **After (Current)**:
1913
+ ```typescript
1914
+ import { NestFactory } from '@nestjs/core';
1915
+ import { ClsModule } from 'nestjs-cls';
1916
+ import { ContextModule, LoggerModule } from '@infineit/winston-logger';
1917
+
1918
+ @Module({
1919
+ imports: [
1920
+ ClsModule.forRoot({
1921
+ middleware: { mount: true, generateId: true },
1922
+ }),
1923
+ ContextModule, // ✅ Add
1924
+ LoggerModule.forRoot(), // ✅ Add
1925
+ ],
1926
+ })
1927
+ export class AppModule {}
1928
+
1929
+ async function bootstrap() {
1930
+ const app = await NestFactory.create(AppModule);
1931
+ // ✅ No app.useLogger() needed
1932
+ await app.listen(3000);
1933
+ // ✅ No app.flushLogs() needed
1934
+ }
1935
+ ```
1936
+
1937
+ ### Need Help?
1938
+
1939
+ If you encounter issues during migration:
1940
+
1941
+ 1. **Check module imports**: Ensure `ContextModule` and `LoggerModule.forRoot()` are in your `AppModule`
1942
+ 2. **Check CLS setup**: For HTTP services, ensure `ClsModule` is configured
1943
+ 3. **Check injection**: Ensure `LoggerService` is injected via constructor
1944
+ 4. **Review examples**: See the "Usage in Different Contexts" section above
1945
+
1946
+ ---
1947
+
1948
+ ## Best Practices
1949
+
1950
+ ### 1. Fire-and-Forget Logging
1951
+
1952
+ ✅ **DO**: Log without awaiting
1953
+ ```typescript
1954
+ this.logger.info('Processing started');
1955
+ // Continue with business logic immediately
1956
+ ```
1957
+
1958
+ ❌ **DON'T**: Try to await logging (methods return `void`)
1959
+ ```typescript
1960
+ // This won't work - logger methods return void
1961
+ await this.logger.info('Message'); // ❌
1962
+ ```
1963
+
1964
+ ### 2. Error Handling
1965
+
1966
+ ✅ **DO**: Pass Error objects directly
1967
+ ```typescript
1968
+ try {
1969
+ // ...
1970
+ } catch (error) {
1971
+ this.logger.error(error, { userId: 123 });
1972
+ }
1973
+ ```
1974
+
1975
+ ✅ **DO**: Include context in error logs
1976
+ ```typescript
1977
+ this.logger.error(error, {
1978
+ userId: 123,
1979
+ orderId: 456,
1980
+ action: 'process-payment',
1981
+ });
1982
+ ```
1983
+
1984
+ ### 3. Structured Logging
1985
+
1986
+ ✅ **DO**: Use `props` for structured data
1987
+ ```typescript
1988
+ this.logger.info('User logged in', {
1989
+ props: {
1990
+ userId: 123,
1991
+ email: 'user@example.com',
1992
+ ipAddress: '192.168.1.1',
1993
+ },
1994
+ });
1995
+ ```
1996
+
1997
+ ### 4. Correlation ID
1998
+
1999
+ ✅ **DO**: Set correlation ID in non-HTTP contexts
2000
+ ```typescript
2001
+ // Kafka consumer
2002
+ this.contextStorage.setContextId(message.correlationId);
2003
+
2004
+ // Bull worker
2005
+ this.contextStorage.setContextId(job.id.toString());
2006
+ ```
2007
+
2008
+ ✅ **DO**: Use correlation ID for request tracing
2009
+ ```typescript
2010
+ // All logs in the same request/job will have the same correlationId
2011
+ this.logger.info('Step 1');
2012
+ this.logger.info('Step 2');
2013
+ // Both logs have the same correlationId
2014
+ ```
2015
+
2016
+ ### 5. Performance Logging
2017
+
2018
+ ✅ **DO**: Include duration for performance monitoring
2019
+ ```typescript
2020
+ const start = Date.now();
2021
+ await this.processData();
2022
+ this.logger.info('Processing completed', {
2023
+ durationMs: Date.now() - start,
2024
+ });
2025
+ ```
2026
+
2027
+ ---
2028
+
2029
+ ## Forbidden Actions
2030
+
2031
+ ### ❌ DO NOT: Add Database Dependencies
2032
+
2033
+ ```typescript
2034
+ // ❌ FORBIDDEN: Do not add Prisma, TypeORM, Mongoose, etc.
2035
+ import { PrismaClient } from '@prisma/client';
2036
+ ```
2037
+
2038
+ **Why**: The logger must work in all contexts without database assumptions.
2039
+
2040
+ ### ❌ DO NOT: Access HTTP Objects
2041
+
2042
+ ```typescript
2043
+ // ❌ FORBIDDEN: Do not inject HttpAdapterHost, Request, Response
2044
+ constructor(
2045
+ private readonly httpAdapter: HttpAdapterHost, // ❌
2046
+ ) {}
2047
+ ```
2048
+
2049
+ **Why**: The logger must work in non-HTTP contexts (Kafka, Bull, CRON, CLI).
2050
+
2051
+ ### ❌ DO NOT: Auto-Mount Middleware
2052
+
2053
+ ```typescript
2054
+ // ❌ FORBIDDEN: Do not implement NestModule or configure middleware
2055
+ export class LoggerModule implements NestModule { // ❌
2056
+ configure(consumer: MiddlewareConsumer) { // ❌
2057
+ consumer.apply(...).forRoutes('*');
2058
+ }
2059
+ }
2060
+ ```
2061
+
2062
+ **Why**: Applications must configure middleware themselves for flexibility.
2063
+
2064
+ ### ❌ DO NOT: Generate Correlation ID
2065
+
2066
+ ```typescript
2067
+ // ❌ FORBIDDEN: Do not generate correlation ID in logger
2068
+ import { v4 as uuidv4 } from 'uuid';
2069
+ const correlationId = uuidv4(); // ❌
2070
+ ```
2071
+
2072
+ **Why**: Correlation ID must come from CLS or be set by the application.
2073
+
2074
+ ### ❌ DO NOT: Throw Errors
2075
+
2076
+ ```typescript
2077
+ // ❌ FORBIDDEN: Do not throw errors in logging code
2078
+ if (!transport) {
2079
+ throw new Error('Transport not found'); // ❌
2080
+ }
2081
+ ```
2082
+
2083
+ **Why**: Logging failures must never break business logic. All errors are swallowed.
2084
+
2085
+ ### ❌ DO NOT: Use Async/Await in Logging
2086
+
2087
+ ```typescript
2088
+ // ❌ FORBIDDEN: Do not use async/await in logging
2089
+ async log(...) { // ❌
2090
+ await this.transport.send(...);
2091
+ }
2092
+ ```
2093
+
2094
+ **Why**: Logging must be fire-and-forget and non-blocking.
2095
+
2096
+ ---
2097
+
2098
+ ## Examples
2099
+
2100
+ ### Complete HTTP Service Example
2101
+
2102
+ ```typescript
2103
+ import { Module } from '@nestjs/common';
2104
+ import { ConfigModule } from '@nestjs/config';
2105
+ import { ClsModule } from 'nestjs-cls';
2106
+ import { v4 as uuidv4 } from 'uuid';
2107
+ import { ContextModule, LoggerModule } from '@infineit/winston-logger';
2108
+
2109
+ @Module({
2110
+ imports: [
2111
+ ConfigModule.forRoot({ isGlobal: true }),
2112
+ ClsModule.forRoot({
2113
+ middleware: {
2114
+ mount: true,
2115
+ generateId: true,
2116
+ idGenerator: (req: Request) =>
2117
+ req.headers['x-correlation-id'] as string || uuidv4(),
2118
+ },
2119
+ }),
2120
+ ContextModule,
2121
+ LoggerModule.forRoot({
2122
+ nodeEnv: process.env.NODE_ENV,
2123
+ organization: 'my-org',
2124
+ context: 'user-service',
2125
+ app: 'api',
2126
+ }),
2127
+ ],
2128
+ })
2129
+ export class AppModule {}
2130
+ ```
2131
+
2132
+ ```typescript
2133
+ import { Injectable } from '@nestjs/common';
2134
+ import { LoggerService } from '@infineit/winston-logger';
2135
+
2136
+ @Injectable()
2137
+ export class UserService {
2138
+ constructor(private readonly logger: LoggerService) {}
2139
+
2140
+ async findById(id: string) {
2141
+ this.logger.info('Fetching user', { userId: id });
2142
+
2143
+ try {
2144
+ const user = await this.repository.findById(id);
2145
+ this.logger.info('User found', { userId: id });
2146
+ return user;
2147
+ } catch (error) {
2148
+ this.logger.error(error, { userId: id });
2149
+ throw error;
2150
+ }
2151
+ }
2152
+ }
2153
+ ```
2154
+
2155
+ ### Complete Kafka Consumer Example
2156
+
2157
+ ```typescript
2158
+ import { Injectable } from '@nestjs/common';
2159
+ import { KafkaListener } from '@nestjs/microservices';
2160
+ import { LoggerService, ContextStorageService } from '@infineit/winston-logger';
2161
+
2162
+ @Injectable()
2163
+ export class OrderConsumer {
2164
+ constructor(
2165
+ private readonly logger: LoggerService,
2166
+ private readonly contextStorage: ContextStorageService,
2167
+ ) {}
2168
+
2169
+ @KafkaListener('order.created')
2170
+ async handleOrderCreated(message: OrderCreatedEvent) {
2171
+ // Set correlation ID from message
2172
+ this.contextStorage.setContextId(message.correlationId || message.id);
2173
+
2174
+ const start = Date.now();
2175
+ this.logger.info('Processing order', {
2176
+ orderId: message.id,
2177
+ userId: message.userId,
2178
+ });
2179
+
2180
+ try {
2181
+ await this.processOrder(message);
2182
+
2183
+ this.logger.info('Order processed', {
2184
+ orderId: message.id,
2185
+ durationMs: Date.now() - start,
2186
+ });
2187
+ } catch (error) {
2188
+ this.logger.error(error, {
2189
+ orderId: message.id,
2190
+ durationMs: Date.now() - start,
2191
+ });
2192
+ throw error;
2193
+ }
2194
+ }
2195
+ }
2196
+ ```
2197
+
2198
+ ### Custom Transport Example
2199
+
2200
+ ```typescript
2201
+ import { Injectable } from '@nestjs/common';
2202
+ import { LoggerTransport, NormalizedLog } from '@infineit/winston-logger';
2203
+ import { KafkaProducer } from './kafka-producer';
2204
+
2205
+ @Injectable()
2206
+ export class KafkaLogTransport implements LoggerTransport {
2207
+ constructor(private readonly kafkaProducer: KafkaProducer) {}
2208
+
2209
+ log(normalizedLog: NormalizedLog): void {
2210
+ try {
2211
+ // Fire-and-forget: send to Kafka without awaiting
2212
+ this.kafkaProducer.send({
2213
+ topic: 'application-logs',
2214
+ messages: [{
2215
+ key: normalizedLog.correlationId || 'no-correlation-id',
2216
+ value: JSON.stringify(normalizedLog),
2217
+ timestamp: normalizedLog.timestamp.toString(),
2218
+ }],
2219
+ });
2220
+ } catch (error) {
2221
+ // Swallow errors - never break business logic
2222
+ console.error('KafkaLogTransport error (swallowed):', error);
2223
+ }
2224
+ }
2225
+ }
2226
+ ```
2227
+
2228
+ ```typescript
2229
+ import { Provider } from '@nestjs/common';
2230
+ import { LoggerModule } from '@infineit/winston-logger';
2231
+ import { KafkaLogTransport } from './kafka-log-transport';
2232
+
2233
+ const customTransports: Provider[] = [
2234
+ {
2235
+ provide: KafkaLogTransport,
2236
+ useClass: KafkaLogTransport,
2237
+ },
2238
+ ];
2239
+
2240
+ @Module({
2241
+ imports: [
2242
+ LoggerModule.forRoot(config, customTransports),
2243
+ ],
2244
+ })
2245
+ export class AppModule {}
2246
+ ```
2247
+
2248
+ ---
159
2249
 
160
2250
  ## License
161
2251
 
162
- This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
2252
+ MIT
163
2253
 
164
2254
  ## Author
165
2255
 
166
- Dharmesh Patel 🇮🇳 <br>
167
- - [GitHub](https://github.com/dharmesh-r-patel/nestjs-starter)
168
- - [LinkedIn](https://www.linkedin.com/in/dharmeshbbay)
169
- - [Instagram](https://www.instagram.com/dharmesh_numbertank)
2256
+ Dharmesh Patel
2257
+
2258
+ ## Repository
2259
+
2260
+ https://github.com/dharmesh-r-patel/nestjs-logger
2261
+