@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.
- package/README.md +2206 -114
- package/context/infrastructure/nestjs/contextModule.js +2 -4
- package/context/infrastructure/nestjs/contextModule.js.map +1 -1
- package/index.d.ts +19 -5
- package/index.js +29 -5
- package/index.js.map +1 -1
- package/logger/domain/log.d.ts +3 -1
- package/logger/domain/loggerService.d.ts +9 -4
- package/logger/domain/loggerService.js +153 -31
- package/logger/domain/loggerService.js.map +1 -1
- package/logger/domain/loggerTransport.d.ts +5 -0
- package/logger/domain/loggerTransport.js +5 -0
- package/logger/domain/loggerTransport.js.map +1 -0
- package/logger/domain/normalizedLog.d.ts +23 -0
- package/logger/domain/normalizedLog.js +37 -0
- package/logger/domain/normalizedLog.js.map +1 -0
- package/logger/infrastructure/forwarding/centralLogForwarder.d.ts +15 -0
- package/logger/infrastructure/forwarding/centralLogForwarder.js +81 -0
- package/logger/infrastructure/forwarding/centralLogForwarder.js.map +1 -0
- package/logger/infrastructure/forwarding/httpCentralLogForwarder.d.ts +8 -0
- package/logger/infrastructure/forwarding/httpCentralLogForwarder.js +88 -0
- package/logger/infrastructure/forwarding/httpCentralLogForwarder.js.map +1 -0
- package/logger/infrastructure/forwarding/kafkaCentralLogForwarder.d.ts +9 -0
- package/logger/infrastructure/forwarding/kafkaCentralLogForwarder.js +50 -0
- package/logger/infrastructure/forwarding/kafkaCentralLogForwarder.js.map +1 -0
- package/logger/infrastructure/nestjs/loggerModule.d.ts +16 -9
- package/logger/infrastructure/nestjs/loggerModule.js +86 -136
- package/logger/infrastructure/nestjs/loggerModule.js.map +1 -1
- package/logger/infrastructure/nestjs/nestjsLoggerServiceAdapter.js +61 -11
- package/logger/infrastructure/nestjs/nestjsLoggerServiceAdapter.js.map +1 -1
- package/logger/infrastructure/winston/transports/fileTransport.d.ts +1 -1
- package/logger/infrastructure/winston/transports/fileTransport.js +5 -2
- package/logger/infrastructure/winston/transports/fileTransport.js.map +1 -1
- package/logger/infrastructure/winston/winstonLogger.js +78 -27
- package/logger/infrastructure/winston/winstonLogger.js.map +1 -1
- package/logger/infrastructure/winston/winstonTransportAdapter.d.ts +10 -0
- package/logger/infrastructure/winston/winstonTransportAdapter.js +128 -0
- package/logger/infrastructure/winston/winstonTransportAdapter.js.map +1 -0
- package/package.json +10 -14
- package/tsconfig.lib.tsbuildinfo +1 -1
- package/logger/infrastructure/winston/transports/prisma-transport.d.ts +0 -11
- package/logger/infrastructure/winston/transports/prisma-transport.js +0 -50
- package/logger/infrastructure/winston/transports/prisma-transport.js.map +0 -1
- package/logger/levelFilter.d.ts +0 -2
- package/logger/levelFilter.js +0 -47
- package/logger/levelFilter.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,169 +1,2261 @@
|
|
|
1
|
-
|
|
2
|
-
<div align="center">
|
|
3
|
-
<h3 align="center">NestJS Logger</h3>
|
|
1
|
+
# @infineit/winston-logger
|
|
4
2
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
---
|
|
17
37
|
|
|
18
|
-
|
|
19
|
-
- **Configurable**: Provides options to customize logger configuration.
|
|
20
|
-
- **Lightweight**: Minimal dependencies and overhead.
|
|
38
|
+
## Installation
|
|
21
39
|
|
|
22
|
-
|
|
40
|
+
```bash
|
|
41
|
+
npm install @infineit/winston-logger
|
|
42
|
+
```
|
|
23
43
|
|
|
24
|
-
|
|
25
|
-
* Decoupled log transporters
|
|
26
|
-
* Log levels
|
|
27
|
-
* Logging Rules
|
|
28
|
-
* Log formatters
|
|
44
|
+
### Peer Dependencies
|
|
29
45
|
|
|
30
|
-
|
|
46
|
+
This package requires the following peer dependencies (usually already installed in NestJS projects):
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
```bash
|
|
39
|
-
yarn add @infineit/winston-logger
|
|
40
|
-
```
|
|
53
|
+
---
|
|
41
54
|
|
|
42
|
-
|
|
55
|
+
## Quick Start
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
- Winston
|
|
46
|
-
- morgon
|
|
47
|
-
- prisma
|
|
57
|
+
### 1. Import Modules
|
|
48
58
|
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
### 2. Use LoggerService
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
```typescript
|
|
77
|
+
import { Injectable } from '@nestjs/common';
|
|
78
|
+
import { LoggerService } from '@infineit/winston-logger';
|
|
57
79
|
|
|
58
|
-
|
|
80
|
+
@Injectable()
|
|
81
|
+
export class MyService {
|
|
82
|
+
constructor(private readonly logger: LoggerService) {}
|
|
59
83
|
|
|
60
|
-
|
|
61
|
-
|
|
84
|
+
doSomething() {
|
|
85
|
+
this.logger.info('Processing started', {
|
|
86
|
+
userId: 123,
|
|
87
|
+
action: 'process',
|
|
88
|
+
});
|
|
62
89
|
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
import { ContextModule, LoggerModule } from '@infineit/winston-logger';
|
|
102
|
+
---
|
|
67
103
|
|
|
68
|
-
|
|
69
|
-
imports: [
|
|
70
|
-
ContextModule,
|
|
71
|
-
LoggerModule.forRoot(PrismaService),
|
|
72
|
-
]
|
|
73
|
-
})
|
|
74
|
-
```
|
|
104
|
+
## Architecture Overview
|
|
75
105
|
|
|
76
|
-
|
|
106
|
+
### LoggerService → LoggerTransport[] Flow
|
|
77
107
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
152
|
+
### Key Components
|
|
82
153
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
160
|
+
---
|
|
92
161
|
|
|
93
162
|
## Configuration
|
|
94
163
|
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
688
|
+
**No changes required:** If CLS is configured, correlationId is automatically included in forwarded logs.
|
|
110
689
|
|
|
111
|
-
|
|
690
|
+
### Fire-and-Forget Behavior
|
|
112
691
|
|
|
113
|
-
|
|
692
|
+
Central log forwarding is completely fire-and-forget:
|
|
114
693
|
|
|
115
|
-
|
|
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
|
-
|
|
699
|
+
**Important:** Original logging behavior remains completely unchanged. Forwarding is an additional, optional layer that runs in parallel.
|
|
118
700
|
|
|
119
|
-
###
|
|
701
|
+
### Environment-Specific Configuration
|
|
120
702
|
|
|
121
|
-
|
|
703
|
+
**Development:**
|
|
704
|
+
```bash
|
|
705
|
+
# Option 1: Disable forwarding in development
|
|
706
|
+
LOGGER_FORWARD_TO_CENTRAL=false
|
|
122
707
|
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
733
|
+
# Document project
|
|
734
|
+
LOGGER_KAFKA_TOPIC=project-logs-document
|
|
132
735
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1212
|
+
### Kafka Producer - Propagating Correlation ID
|
|
155
1213
|
|
|
156
|
-
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
2252
|
+
MIT
|
|
163
2253
|
|
|
164
2254
|
## Author
|
|
165
2255
|
|
|
166
|
-
Dharmesh Patel
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
2256
|
+
Dharmesh Patel
|
|
2257
|
+
|
|
2258
|
+
## Repository
|
|
2259
|
+
|
|
2260
|
+
https://github.com/dharmesh-r-patel/nestjs-logger
|
|
2261
|
+
|