@globalart/nestjs-logger 1.0.4 → 1.0.5

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.
@@ -18,5 +18,6 @@ export declare class HttpLoggerInterceptor implements NestInterceptor {
18
18
  private createLogEntry;
19
19
  private getClientIp;
20
20
  private sanitizeHeaders;
21
- private shouldExcludeUrl;
21
+ private getRequestMethod;
22
+ private shouldExcludeRequest;
22
23
  }
@@ -32,7 +32,8 @@ let HttpLoggerInterceptor = class HttpLoggerInterceptor {
32
32
  intercept(context, next) {
33
33
  const request = context.switchToHttp().getRequest();
34
34
  const response = context.switchToHttp().getResponse();
35
- if (this.shouldExcludeUrl(request.url)) {
35
+ const requestMethod = this.getRequestMethod(request.method);
36
+ if (this.shouldExcludeRequest(requestMethod, request.url)) {
36
37
  return next.handle();
37
38
  }
38
39
  const isExcluded = this.reflector.getAllAndOverride(constants_1.LOGGER_EXCLUDE_METADATA, [context.getHandler(), context.getClass()]);
@@ -130,14 +131,37 @@ let HttpLoggerInterceptor = class HttpLoggerInterceptor {
130
131
  }
131
132
  return this.dataSanitizer.sanitize(sanitized);
132
133
  }
133
- shouldExcludeUrl(url) {
134
- return this.config.exclude.some((excludeUrl) => {
135
- if (excludeUrl.includes("*")) {
136
- const pattern = excludeUrl.replace(/\*/g, ".*");
134
+ getRequestMethod(method) {
135
+ switch (method.toUpperCase()) {
136
+ case "GET":
137
+ return common_1.RequestMethod.GET;
138
+ case "POST":
139
+ return common_1.RequestMethod.POST;
140
+ case "PUT":
141
+ return common_1.RequestMethod.PUT;
142
+ case "DELETE":
143
+ return common_1.RequestMethod.DELETE;
144
+ case "PATCH":
145
+ return common_1.RequestMethod.PATCH;
146
+ case "OPTIONS":
147
+ return common_1.RequestMethod.OPTIONS;
148
+ case "HEAD":
149
+ return common_1.RequestMethod.HEAD;
150
+ default:
151
+ return common_1.RequestMethod.ALL;
152
+ }
153
+ }
154
+ shouldExcludeRequest(method, path) {
155
+ return this.config.exclude.some((excludeOption) => {
156
+ if (excludeOption.method !== method) {
157
+ return false;
158
+ }
159
+ if (excludeOption.path.includes("*")) {
160
+ const pattern = excludeOption.path.replace(/\*/g, ".*");
137
161
  const regex = new RegExp(`^${pattern}$`);
138
- return regex.test(url);
162
+ return regex.test(path);
139
163
  }
140
- return url === excludeUrl;
164
+ return path === excludeOption.path;
141
165
  });
142
166
  }
143
167
  };
@@ -1,4 +1,5 @@
1
1
  import { DynamicModule } from "@nestjs/common";
2
+ import { ExcludeOption } from "../types";
2
3
  export interface LoggerModuleOptions {
3
4
  level?: "error" | "warn" | "info" | "debug" | "verbose";
4
5
  timestamp?: boolean;
@@ -6,7 +7,7 @@ export interface LoggerModuleOptions {
6
7
  context?: string;
7
8
  format?: "json" | "text" | "pino";
8
9
  sensitiveFields?: string[];
9
- exclude?: string[];
10
+ exclude?: ExcludeOption[];
10
11
  }
11
12
  export interface LoggerModuleAsyncOptions {
12
13
  useFactory: (...args: any[]) => LoggerModuleOptions | Promise<LoggerModuleOptions>;
@@ -9,4 +9,6 @@ export declare abstract class BaseFormatter implements ILogFormatter {
9
9
  protected formatTimestamp(timestamp: Date): string;
10
10
  protected colorize(text: string, color: keyof typeof COLORS): string;
11
11
  protected getColorForLevel(level: string | number): keyof typeof COLORS;
12
+ private getColorForStringLevel;
13
+ private getColorForNumericLevel;
12
14
  }
@@ -16,22 +16,21 @@ class BaseFormatter {
16
16
  }
17
17
  getColorForLevel(level) {
18
18
  if (typeof level === "string") {
19
- switch (level) {
20
- case "error":
21
- return "red";
22
- case "warn":
23
- return "yellow";
24
- case "info":
25
- return "green";
26
- case "debug":
27
- return "blue";
28
- case "verbose":
29
- return "magenta";
30
- default:
31
- return "gray";
32
- }
19
+ return this.getColorForStringLevel(level);
33
20
  }
34
- // Для числовых уровней (Pino)
21
+ return this.getColorForNumericLevel(level);
22
+ }
23
+ getColorForStringLevel(level) {
24
+ const colorMap = {
25
+ error: "red",
26
+ warn: "yellow",
27
+ info: "green",
28
+ debug: "blue",
29
+ verbose: "magenta",
30
+ };
31
+ return colorMap[level] || "gray";
32
+ }
33
+ getColorForNumericLevel(level) {
35
34
  if (level >= 50)
36
35
  return "red";
37
36
  if (level >= 40)
@@ -3,4 +3,6 @@ import { LogEntry, HttpRequestLogEntry } from "../types";
3
3
  export declare class JsonFormatter extends BaseFormatter {
4
4
  format(entry: LogEntry): string;
5
5
  formatHttpRequest(entry: HttpRequestLogEntry): string;
6
+ private buildLogObject;
7
+ private addOptionalFields;
6
8
  }
@@ -11,23 +11,35 @@ const common_1 = require("@nestjs/common");
11
11
  const base_formatter_1 = require("./base-formatter");
12
12
  let JsonFormatter = class JsonFormatter extends base_formatter_1.BaseFormatter {
13
13
  format(entry) {
14
- const logObject = {
14
+ const logObject = this.buildLogObject(entry);
15
+ return JSON.stringify(logObject);
16
+ }
17
+ formatHttpRequest(entry) {
18
+ const jsonString = JSON.stringify(entry);
19
+ return this.options.colors
20
+ ? this.colorize(jsonString, this.getColorForLevel(entry.level))
21
+ : jsonString;
22
+ }
23
+ buildLogObject(entry) {
24
+ const baseObject = {
15
25
  timestamp: this.formatTimestamp(entry.timestamp),
16
26
  level: entry.level,
17
27
  message: entry.message,
18
- ...(entry.context && { context: entry.context }),
19
- ...(entry.metadata && { metadata: entry.metadata }),
20
- ...(entry.trace && { trace: entry.trace }),
21
28
  };
22
- return JSON.stringify(logObject);
29
+ return this.addOptionalFields(baseObject, entry);
23
30
  }
24
- formatHttpRequest(entry) {
25
- const jsonString = JSON.stringify(entry);
26
- if (this.options.colors) {
27
- const color = this.getColorForLevel(entry.level);
28
- return this.colorize(jsonString, color);
31
+ addOptionalFields(baseObject, entry) {
32
+ const result = { ...baseObject };
33
+ if (entry.context) {
34
+ result.context = entry.context;
35
+ }
36
+ if (entry.metadata) {
37
+ result.metadata = entry.metadata;
38
+ }
39
+ if (entry.trace) {
40
+ result.trace = entry.trace;
29
41
  }
30
- return jsonString;
42
+ return result;
31
43
  }
32
44
  };
33
45
  exports.JsonFormatter = JsonFormatter;
@@ -15,11 +15,9 @@ let PinoFormatter = class PinoFormatter extends base_formatter_1.BaseFormatter {
15
15
  }
16
16
  formatHttpRequest(entry) {
17
17
  const jsonString = JSON.stringify(entry);
18
- if (this.options.colors) {
19
- const color = this.getColorForLevel(entry.level);
20
- return this.colorize(jsonString, color);
21
- }
22
- return jsonString;
18
+ return this.options.colors
19
+ ? this.colorize(jsonString, this.getColorForLevel(entry.level))
20
+ : jsonString;
23
21
  }
24
22
  };
25
23
  exports.PinoFormatter = PinoFormatter;
@@ -3,4 +3,12 @@ import { LogEntry, HttpRequestLogEntry } from "../types";
3
3
  export declare class TextFormatter extends BaseFormatter {
4
4
  format(entry: LogEntry): string;
5
5
  formatHttpRequest(entry: HttpRequestLogEntry): string;
6
+ private buildLogParts;
7
+ private addTimestamp;
8
+ private addLevel;
9
+ private addContext;
10
+ private addMessage;
11
+ private addMetadata;
12
+ private hasMetadata;
13
+ private addTrace;
6
14
  }
@@ -11,32 +11,54 @@ const common_1 = require("@nestjs/common");
11
11
  const base_formatter_1 = require("./base-formatter");
12
12
  let TextFormatter = class TextFormatter extends base_formatter_1.BaseFormatter {
13
13
  format(entry) {
14
+ const parts = this.buildLogParts(entry);
15
+ const result = parts.join(" ");
16
+ return entry.trace ? this.addTrace(result, entry.trace) : result;
17
+ }
18
+ formatHttpRequest(entry) {
19
+ return JSON.stringify(entry);
20
+ }
21
+ buildLogParts(entry) {
14
22
  const parts = [];
23
+ this.addTimestamp(parts, entry);
24
+ this.addLevel(parts, entry);
25
+ this.addContext(parts, entry);
26
+ this.addMessage(parts, entry);
27
+ this.addMetadata(parts, entry);
28
+ return parts;
29
+ }
30
+ addTimestamp(parts, entry) {
15
31
  if (this.options.timestamp) {
16
32
  const timestamp = this.colorize(`[${this.formatTimestamp(entry.timestamp)}]`, "gray");
17
33
  parts.push(timestamp);
18
34
  }
35
+ }
36
+ addLevel(parts, entry) {
19
37
  const level = this.colorize(`[${entry.level.toUpperCase()}]`, this.getColorForLevel(entry.level));
20
38
  parts.push(level);
39
+ }
40
+ addContext(parts, entry) {
21
41
  if (entry.context) {
22
42
  const context = this.colorize(`[${entry.context}]`, "cyan");
23
43
  parts.push(context);
24
44
  }
45
+ }
46
+ addMessage(parts, entry) {
25
47
  const message = this.colorize(entry.message, "bright");
26
48
  parts.push(message);
27
- if (entry.metadata && Object.keys(entry.metadata).length > 0) {
49
+ }
50
+ addMetadata(parts, entry) {
51
+ if (this.hasMetadata(entry)) {
28
52
  const metadata = this.colorize(JSON.stringify(entry.metadata), "gray");
29
53
  parts.push(metadata);
30
54
  }
31
- let result = parts.join(" ");
32
- if (entry.trace) {
33
- const trace = this.colorize(entry.trace, "red");
34
- result += `\n${trace}`;
35
- }
36
- return result;
37
55
  }
38
- formatHttpRequest(entry) {
39
- return JSON.stringify(entry);
56
+ hasMetadata(entry) {
57
+ return Boolean(entry.metadata && Object.keys(entry.metadata).length > 0);
58
+ }
59
+ addTrace(result, trace) {
60
+ const coloredTrace = this.colorize(trace, "red");
61
+ return `${result}\n${coloredTrace}`;
40
62
  }
41
63
  };
42
64
  exports.TextFormatter = TextFormatter;
@@ -1,3 +1,4 @@
1
+ import { RequestMethod } from "@nestjs/common";
1
2
  export type LogLevel = "error" | "warn" | "info" | "debug" | "verbose";
2
3
  export type LogFormat = "json" | "text" | "pino";
3
4
  export declare const LOG_LEVELS: Record<LogLevel, number>;
@@ -35,6 +36,10 @@ export interface HttpResponse {
35
36
  readonly statusCode: number;
36
37
  readonly headers: Record<string, string>;
37
38
  }
39
+ export interface ExcludeOption {
40
+ method: RequestMethod;
41
+ path: string;
42
+ }
38
43
  export interface LoggerConfiguration {
39
44
  readonly level: LogLevel;
40
45
  readonly timestamp: boolean;
@@ -42,7 +47,7 @@ export interface LoggerConfiguration {
42
47
  readonly context?: string;
43
48
  readonly format: LogFormat;
44
49
  readonly sensitiveFields: readonly string[];
45
- readonly exclude: readonly string[];
50
+ readonly exclude: readonly ExcludeOption[];
46
51
  }
47
52
  export interface FormatterOptions {
48
53
  readonly colors: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globalart/nestjs-logger",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "A advanced logger for NestJS",
5
5
  "author": {
6
6
  "name": "GlobalArt, Inc"
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "@nestjs/common": "^11.1.3",
44
44
  "@nestjs/core": "^11.1.3",
45
- "@nestjs/microservices": "11.1.5",
45
+ "@nestjs/microservices": "11.1.6",
46
46
  "@nestjs/swagger": "^11.2.0",
47
47
  "@nestjs/testing": "^11.1.3",
48
48
  "@nestjs/typeorm": "^11.0.0"
package/README.md DELETED
@@ -1,364 +0,0 @@
1
- # @globalart/nestjs-logger
2
-
3
- A professional logging module for NestJS with clean architecture, strict typing, and extensibility.
4
-
5
- ## 🚀 Features
6
-
7
- - **Clean Architecture** - separation of concerns, SOLID principles
8
- - **Strict Typing** - full TypeScript type safety
9
- - **Performance** - optimized architecture with minimal allocations
10
- - **Extensibility** - easy to add new formatters and transports
11
- - **Testability** - dependency injection, easy mocking
12
- - **Automatic Context** - class detection from call stack
13
- - **HTTP Logging** - detailed request logging with format consistency
14
- - **Security** - automatic sanitization of sensitive data
15
- - **Colored Output** - beautiful console logs
16
- - **Multiple Formats** - Text, JSON, and Pino support
17
-
18
- ## 📦 Installation
19
-
20
- ```bash
21
- npm install @globalart/nestjs-logger
22
- ```
23
-
24
- ## 🎯 Quick Start
25
-
26
- ```typescript
27
- import { Module } from '@nestjs/common';
28
- import { LoggerModule } from '@globalart/nestjs-logger';
29
-
30
- @Module({
31
- imports: [
32
- LoggerModule.forRoot({
33
- level: 'info',
34
- timestamp: true,
35
- colors: true,
36
- format: 'text',
37
- }),
38
- ],
39
- })
40
- export class AppModule {}
41
- ```
42
-
43
- ## 🔧 Configuration
44
-
45
- ### Synchronous Configuration
46
-
47
- ```typescript
48
- LoggerModule.forRoot({
49
- level: 'debug',
50
- timestamp: true,
51
- colors: true,
52
- format: 'pino',
53
- sensitiveFields: ['password', 'secret'],
54
- exclude: ['/health', '/metrics'],
55
- })
56
- ```
57
-
58
- ### Asynchronous Configuration
59
-
60
- ```typescript
61
- LoggerModule.forRootAsync({
62
- useFactory: (configService: ConfigService) => ({
63
- level: configService.get('LOG_LEVEL', 'info'),
64
- format: configService.get('LOG_FORMAT', 'text'),
65
- colors: !configService.get('PRODUCTION'),
66
- sensitiveFields: configService.get('SENSITIVE_FIELDS', []),
67
- }),
68
- inject: [ConfigService],
69
- })
70
- ```
71
-
72
- ## 📝 Usage
73
-
74
- ### In Services
75
-
76
- ```typescript
77
- import { Injectable } from '@nestjs/common';
78
- import { LoggerService } from '@globalart/nestjs-logger';
79
-
80
- @Injectable()
81
- export class UserService {
82
- constructor(private readonly logger: LoggerService) {}
83
-
84
- async createUser(userData: CreateUserDto) {
85
- this.logger.log({
86
- message: 'Creating new user',
87
- metadata: { userId: userData.email }
88
- });
89
-
90
- try {
91
- const user = await this.userRepository.save(userData);
92
- this.logger.log({
93
- message: 'User created successfully',
94
- metadata: { id: user.id }
95
- });
96
- return user;
97
- } catch (error) {
98
- this.logger.error({
99
- message: 'Failed to create user',
100
- trace: error.stack,
101
- metadata: { email: userData.email }
102
- });
103
- throw error;
104
- }
105
- }
106
- }
107
- ```
108
-
109
- ### HTTP Logging
110
-
111
- ```typescript
112
- import { Controller, UseInterceptors } from '@nestjs/common';
113
- import { HttpLoggerInterceptor, LogContext } from '@globalart/nestjs-logger';
114
-
115
- @Controller('users')
116
- @UseInterceptors(HttpLoggerInterceptor)
117
- @LogContext('UserController')
118
- export class UserController {
119
- // All HTTP requests will be automatically logged
120
- }
121
- ```
122
-
123
- ### Global HTTP Logging
124
-
125
- ```typescript
126
- import { Module } from '@nestjs/common';
127
- import { APP_INTERCEPTOR } from '@nestjs/core';
128
- import { LoggerModule, HttpLoggerInterceptor } from '@globalart/nestjs-logger';
129
-
130
- @Module({
131
- imports: [LoggerModule.forRoot()],
132
- providers: [
133
- {
134
- provide: APP_INTERCEPTOR,
135
- useClass: HttpLoggerInterceptor,
136
- },
137
- ],
138
- })
139
- export class AppModule {}
140
- ```
141
-
142
- ## 🎨 Output Formats
143
-
144
- ### Text Format (Default)
145
- ```
146
- [2024-01-15T10:30:45.123Z] [INFO] [UserService] Creating new user {"userId":"123"}
147
- [2024-01-15T10:30:45.335Z] [INFO] [HttpLogger] GET /users - 200 (12ms)
148
- ```
149
-
150
- ### JSON Format
151
- ```json
152
- {
153
- "timestamp": "2024-01-15T10:30:45.123Z",
154
- "level": "info",
155
- "message": "Creating new user",
156
- "context": "UserService",
157
- "metadata": {"userId": "123"}
158
- }
159
- {
160
- "timestamp": "2024-01-15T10:30:45.335Z",
161
- "level": "info",
162
- "message": "GET /users - 200 (12ms)",
163
- "context": "HttpLogger",
164
- "metadata": {
165
- "requestId": "req-123",
166
- "method": "GET",
167
- "url": "/users",
168
- "statusCode": 200,
169
- "responseTime": 12,
170
- "remoteAddress": "127.0.0.1"
171
- }
172
- }
173
- ```
174
-
175
- ### Pino Format
176
- ```json
177
- {"level":30,"time":1642247445123,"pid":1234,"hostname":"app-server","req":{"id":"req-123","method":"GET","url":"/users","query":{},"params":{},"headers":{},"remoteAddress":"127.0.0.1"},"res":{"statusCode":200,"headers":{}},"responseTime":12,"msg":"request completed"}
178
- ```
179
-
180
- ## 🎯 API Reference
181
-
182
- ### LoggerService
183
-
184
- | Method | Description |
185
- |--------|-------------|
186
- | `log(options: LogOptions)` | Information message |
187
- | `error(options: LogOptions)` | Error with trace |
188
- | `warn(options: LogOptions)` | Warning message |
189
- | `debug(options: LogOptions)` | Debug information |
190
- | `verbose(options: LogOptions)` | Verbose logging |
191
- | `setContext(context: string)` | Set context |
192
- | `logHttpRequest(entry: HttpRequestLogEntry)` | Log HTTP request (Pino format only) |
193
-
194
- ### LogOptions Interface
195
-
196
- ```typescript
197
- interface LogOptions {
198
- message: string;
199
- context?: string;
200
- metadata?: Record<string, unknown>;
201
- trace?: string;
202
- }
203
- ```
204
-
205
- ### Decorators
206
-
207
- - `@LogContext(context: string)` - Set context for class/method
208
- - `@LogMetadata(metadata: Record<string, unknown>)` - Add metadata to logs
209
- - `@ExcludeLogging()` - Exclude logging for controller/method
210
-
211
- ### Configuration Options
212
-
213
- | Option | Type | Default | Description |
214
- |--------|------|---------|-------------|
215
- | `level` | `LogLevel` | `'info'` | Logging level |
216
- | `timestamp` | `boolean` | `true` | Show timestamp |
217
- | `colors` | `boolean` | `true` | Colored output |
218
- | `format` | `LogFormat` | `'text'` | Output format |
219
- | `context` | `string` | `undefined` | Default context |
220
- | `sensitiveFields` | `string[]` | `[...]` | Fields to sanitize |
221
- | `exclude` | `string[]` | `[]` | URLs to exclude from HTTP logging |
222
-
223
- ## 🏗️ Architecture
224
-
225
- The package is built on Clean Architecture principles:
226
-
227
- ### Core Components
228
-
229
- - **LoggerModule** - Main module with configuration
230
- - **LoggerService** - Primary logging service
231
- - **HttpLoggerInterceptor** - HTTP request logging interceptor
232
-
233
- ### Formatters
234
-
235
- - **TextFormatter** - Human-readable text output
236
- - **JsonFormatter** - Structured JSON output
237
- - **PinoFormatter** - Pino-compatible JSON output
238
-
239
- ### Utilities
240
-
241
- - **ContextResolver** - Automatic context detection
242
- - **DataSanitizer** - Sensitive data sanitization
243
- - **RequestIdGenerator** - Unique request ID generation
244
-
245
- ### Writers
246
-
247
- - **ConsoleWriter** - Console output (extensible for other transports)
248
-
249
- ### Contracts & Types
250
-
251
- - **ILogger** - Logger interface
252
- - **ILogFormatter** - Formatter interface
253
- - **ILogWriter** - Writer interface
254
- - **LogEntry** - Standard log entry structure
255
- - **HttpRequestLogEntry** - HTTP-specific log entry structure
256
-
257
- ## 🔒 Security
258
-
259
- Automatic sanitization of sensitive fields:
260
- - `password`, `pass`
261
- - `token`, `accessToken`, `refreshToken`
262
- - `secret`, `key`, `apiKey`
263
- - `authorization`, `auth`
264
- - `credential`, `credentials`
265
-
266
- ## 🧪 Testing
267
-
268
- ```typescript
269
- import { LoggerService } from '@globalart/nestjs-logger';
270
-
271
- describe('UserService', () => {
272
- let service: UserService;
273
- let logger: jest.Mocked<LoggerService>;
274
-
275
- beforeEach(() => {
276
- logger = {
277
- log: jest.fn(),
278
- error: jest.fn(),
279
- warn: jest.fn(),
280
- debug: jest.fn(),
281
- verbose: jest.fn(),
282
- setContext: jest.fn(),
283
- logHttpRequest: jest.fn(),
284
- } as any;
285
-
286
- service = new UserService(logger);
287
- });
288
-
289
- it('should log user creation', () => {
290
- service.createUser(userData);
291
- expect(logger.log).toHaveBeenCalledWith({
292
- message: 'Creating new user',
293
- metadata: { userId: userData.email }
294
- });
295
- });
296
- });
297
- ```
298
-
299
- ## 🔄 Migration from v1
300
-
301
- ```typescript
302
- // v1 (old API)
303
- import { LoggerModule, LoggerInterceptor } from '@globalart/nestjs-logger';
304
-
305
- // v2 (new API)
306
- import { LoggerModule, HttpLoggerInterceptor } from '@globalart/nestjs-logger';
307
-
308
- // Legacy exports available for compatibility:
309
- import { LegacyLoggerModule, LoggerInterceptor } from '@globalart/nestjs-logger';
310
- ```
311
-
312
- ## 📈 Performance
313
-
314
- - Minimal allocations in hot paths
315
- - Lazy formatter initialization
316
- - Optimized context resolution
317
- - Efficient data sanitization
318
- - Format-specific HTTP logging optimization
319
-
320
- ## 🎨 Customization
321
-
322
- ### Custom Formatter
323
-
324
- ```typescript
325
- import { Injectable } from '@nestjs/common';
326
- import { BaseFormatter } from '@globalart/nestjs-logger';
327
-
328
- @Injectable()
329
- export class CustomFormatter extends BaseFormatter {
330
- format(entry: LogEntry): string {
331
- return `[${entry.level.toUpperCase()}] ${entry.message}`;
332
- }
333
-
334
- formatHttpRequest(entry: HttpRequestLogEntry): string {
335
- return JSON.stringify(entry);
336
- }
337
- }
338
- ```
339
-
340
- ### Custom Writer
341
-
342
- ```typescript
343
- import { Injectable } from '@nestjs/common';
344
- import { ILogWriter } from '@globalart/nestjs-logger';
345
-
346
- @Injectable()
347
- export class FileWriter implements ILogWriter {
348
- write(formattedLog: string): void {
349
- // Write to file
350
- }
351
- }
352
- ```
353
-
354
- ## 🤝 Contributing
355
-
356
- 1. Fork the repository
357
- 2. Create a feature branch
358
- 3. Make your changes
359
- 4. Add tests
360
- 5. Create a Pull Request
361
-
362
- ## 📄 License
363
-
364
- MIT License