Production-ready Winston logger module for NestJS with colorized dev output, structured JSON/ECS in production, and file logging
npm install @kyrasource/loggerbash
npm install @kyrasource/logger
`
Quick Start
$3
`typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { LoggerModule } from '@kyrasource/logger';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
LoggerModule.register(), // Register with env-based config
],
})
export class AppModule {}
`
$3
`env
Logger Configuration
LOGGER_ENABLED=true
NODE_ENV=development
LOG_LEVEL=info
LOG_DIR=./logs
File Logging
LOG_FILE_ENABLED=true
LOG_FILE_MAX_SIZE=20m
LOG_FILE_MAX_FILES=14
LOG_ERROR_FILE=error.log
LOG_COMBINED_FILE=combined.log
Logstash (optional)
LOGSTASH_ENABLED=false
LOGSTASH_HOST=localhost
LOGSTASH_PORT=5000
`
$3
`typescript
import { Injectable } from '@nestjs/common';
import { LoggerService } from '@kyrasource/logger';
@Injectable()
export class UserService {
constructor(private readonly logger: LoggerService) {
this.logger.setContext('UserService');
}
async createUser(dto: CreateUserDto) {
try {
this.logger.info(Creating user: ${dto.email});
const user = await this.userRepository.create(dto);
this.logger.info(User created, {
metadata: { userId: user.id, email: user.email },
});
return user;
} catch (error) {
this.logger.error('Failed to create user', error, {
metadata: { email: dto.email },
});
throw error;
}
}
}
`
Output Examples
$3
`
14:32:45.123 [INFO] [UserService] ✓ User created (req: abc123)
14:32:46.456 [ERROR] [AuthService] ✖ Invalid credentials - ▶ Error: Wrong password
Error: Wrong password
at AuthService.validatePassword (auth.service.ts:45:11)
at AuthService.login (auth.service.ts:20:5)
{
"email": "user@example.com",
"attempt": 2
}
`
$3
`json
{
"@timestamp": "2026-01-20T14:32:45.123Z",
"log.level": "ERROR",
"log.logger": "AuthService",
"message": "Invalid credentials",
"service.name": "api",
"trace.id": "abc123",
"user.id": "user-456",
"error.message": "Wrong password",
"error.stack_trace": "Error: Wrong password\n at AuthService.validatePassword...",
"error.type": "Error",
"labels": {
"email": "user@example.com",
"attempt": 2
}
}
`
API Reference
$3
#### Basic Logging
`typescript
// Error (red in dev, ECS in prod)
logger.error('Operation failed', error, options?);
// Warning (yellow in dev)
logger.warn('Retry attempt 3 of 5', options?);
// Info (cyan in dev)
logger.info('User logged in', options?);
// Debug (blue in dev)
logger.debug('Query executed', options?);
// Verbose (magenta in dev)
logger.verbose('Cache invalidated', options?);
// Silly (gray in dev, most verbose)
logger.silly('Detailed trace', options?);
`
#### Context Management
`typescript
logger.setContext('UserController');
logger.getContext(); // Returns: "UserController"
`
#### Specialized Logging
`typescript
// HTTP request/response
logger.logHttpRequest(
'GET',
'/api/users/123',
200,
145, // duration in ms
{ requestId: 'req-789' }
);
// Database operations
logger.logDatabaseOperation(
'findById',
'users',
23, // duration in ms
true, // success
null, // error
{ requestId: 'req-789' }
);
// External API calls
logger.logExternalCall(
'stripe-api',
'/v1/customers',
567, // duration in ms
200, // status code
null, // error
{ userId: 'user-123' }
);
`
$3
`typescript
interface LoggerOptions {
level?: LogLevel; // Override log level
context?: string; // Override context
requestId?: string; // Distributed trace ID
userId?: string; // User context
metadata?: LogMetadata; // Additional data
}
`
$3
`typescript
import { LoggerService } from '@kyrasource/logger';
@Controller('users')
export class UsersController {
constructor(private readonly logger: LoggerService) {
this.logger.setContext('UsersController');
}
@Post()
async create(@Body() dto: CreateUserDto, @Headers('x-request-id') requestId: string) {
const startTime = Date.now();
try {
this.logger.info('Creating user', {
requestId,
metadata: { email: dto.email },
});
const user = await this.userService.create(dto);
const duration = Date.now() - startTime;
this.logger.info('User created successfully', {
requestId,
metadata: {
userId: user.id,
email: user.email,
duration,
},
});
return user;
} catch (error) {
this.logger.error('Failed to create user', error, {
requestId,
metadata: { email: dto.email },
});
throw new BadRequestException('Failed to create user');
}
}
}
`
Configuration
$3
`env
Enable/disable logging
LOGGER_ENABLED=true
Log level (error, warn, info, debug, verbose, silly)
LOG_LEVEL=info
Environment (development, production, test)
NODE_ENV=development
Date format in logs
LOG_DATE_FORMAT=YYYY-MM-DD HH:mm:ss.SSS
File logging
LOG_DIR=./logs
LOG_FILE_ENABLED=true
LOG_FILE_MAX_SIZE=20m # Max file size before rotation
LOG_FILE_MAX_FILES=14 # Max days to keep logs
LOG_ERROR_FILE=error.log # Error-only log file
LOG_COMBINED_FILE=combined.log # All levels
Console output
LOG_CONSOLE_ENABLED=true
Logstash HTTP transport
LOGSTASH_ENABLED=false
LOGSTASH_HOST=localhost:5044
LOGSTASH_PORT=5000
LOGSTASH_USERNAME=admin
LOGSTASH_PASSWORD=secret123
`
$3
`typescript
import { LoggerModule } from '@kyrasource/logger';
@Module({
imports: [
LoggerModule.registerCustom({
enabled: true,
environment: 'production',
defaultLevel: 'info',
dateFormat: 'YYYY-MM-DD HH:mm:ss.SSS',
fileMaxSize: '20m',
fileMaxFiles: 14,
logDir: './logs',
errorLogFile: 'error.log',
combinedLogFile: 'combined.log',
consoleEnabled: false,
fileEnabled: true,
logstashEnabled: true,
logstashHost: 'logstash.example.com',
logstashPort: 5044,
}),
],
})
export class AppModule {}
`
Sensitive Data Protection
The logger automatically masks sensitive information:
`typescript
const sensitiveData = {
email: 'user@example.com',
password: 'super-secret',
apiKey: 'sk-1234567890',
token: 'jwt-token-xyz',
};
logger.info('Login attempt', {
metadata: sensitiveData,
});
// Output (password/apiKey/token masked):
// {
// "email": "user@example.com",
// "password": "MASKED",
// "apiKey": "MASKED",
// "token": "MASKED"
// }
`
Custom sensitive keys:
`typescript
// Access the logger's internal method (advanced)
import { maskSensitiveData } from '@kyrasource/logger';
const masked = maskSensitiveData(
{ customSecret: 'value', data: 'public' },
['customSecret', 'password', 'token']
);
`
Integration Patterns
$3
`typescript
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { LoggerService } from '@kyrasource/logger';
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
constructor(private readonly logger: LoggerService) {}
use(req: Request, res: Response, next: NextFunction) {
const startTime = Date.now();
const { method, url, headers } = req;
const requestId = headers['x-request-id'] as string || Math.random().toString(36).substr(2, 9);
res.on('finish', () => {
const duration = Date.now() - startTime;
this.logger.logHttpRequest(method, url, res.statusCode, duration, {
requestId,
});
});
next();
}
}
// Apply in AppModule
@Module({
imports: [LoggerModule.register()],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes('*');
}
}
`
$3
`typescript
import { Injectable } from '@nestjs/common';
import { LoggerService } from '@kyrasource/logger';
@Injectable()
export class DatabaseInterceptor {
constructor(private readonly logger: LoggerService) {}
async intercept(query: string, params: any[], options?: any) {
const startTime = Date.now();
try {
const result = await this.executeQuery(query, params);
const duration = Date.now() - startTime;
this.logger.logDatabaseOperation(
'query',
options?.collection || 'unknown',
duration,
true,
null,
{
metadata: {
query: query.substring(0, 100), // First 100 chars
paramCount: params.length,
},
}
);
return result;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.logDatabaseOperation(
'query',
options?.collection || 'unknown',
duration,
false,
error
);
throw error;
}
}
}
`
$3
`typescript
import { Catch, ExceptionFilter, HttpException } from '@nestjs/common';
import { LoggerService } from '@kyrasource/logger';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
constructor(private readonly logger: LoggerService) {
this.logger.setContext('GlobalExceptionFilter');
}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const request = ctx.getRequest();
const response = ctx.getResponse();
const requestId = request.headers['x-request-id'] as string;
const status = exception instanceof HttpException ? exception.getStatus() : 500;
this.logger.error(
${request.method} ${request.url} - ${status},
exception as Error,
{
requestId,
metadata: {
method: request.method,
path: request.url,
status,
},
}
);
response.status(status).json({
statusCode: status,
message: exception instanceof HttpException ? exception.message : 'Internal server error',
requestId,
timestamp: new Date().toISOString(),
});
}
}
`
File Organization
`
logs/
├── error.log-2026-01-20 # Error-only, rotated daily
├── error.log-2026-01-19
├── combined.log-2026-01-20 # All levels, rotated daily
├── combined.log-2026-01-19
├── exceptions.log # Unhandled exceptions
└── rejections.log # Unhandled promise rejections
`
$3
- Size: Rotates when file exceeds LOG_FILE_MAX_SIZE (default: 20MB)
- Time: Rotates daily (suffix format: YYYY-MM-DD)
- Retention: Keeps last LOG_FILE_MAX_FILES days (default: 14)
Advanced Usage
$3
`typescript
import { createJsonFormatter, createECSFormatter } from '@kyrasource/logger';
// Use JSON formatter for all logs
const customFormatter = createJsonFormatter('my-service');
// Use ECS formatter for Elasticsearch
const ecsFormatter = createECSFormatter('my-service');
`
$3
`typescript
const logger = loggerService.getWinstonLogger();
// Access underlying Winston logger if needed
logger.info('Direct message', { custom: 'field' });
`
$3
`typescript
import { serializeError, maskSensitiveData, stripAnsiCodes } from '@kyrasource/logger';
const error = new Error('Something went wrong');
const serialized = serializeError(error);
const masked = maskSensitiveData({ password: 'secret' });
const clean = stripAnsiCodes('\x1b31mRed text\x1b[0m');
`
Troubleshooting
$3
- Check LOG_CONSOLE_ENABLED is true (default)
- Check LOGGER_ENABLED is true (default)
- Check LOG_LEVEL includes your log level
$3
- Ensure LOG_DIR directory exists and is writable
- Check LOG_FILE_ENABLED is true
- Verify LOG_FILE_MAX_FILES is a valid number (default: 14)
$3
- Verify LOGSTASH_ENABLED=true
- Check LOGSTASH_HOST is correct and reachable
- Confirm firewall allows connection to LOGSTASH_PORT
- Verify Logstash HTTP input is configured to accept JSON
$3
- Reduce LOG_FILE_MAX_SIZE (default: 20m)
- Increase LOG_FILE_MAX_FILES retention period
- Reduce log level (use 'warn' instead of 'silly' in production)
- Disable console logging in production with LOG_CONSOLE_ENABLED=false
$3
- Check field names match known sensitive keywords
- Custom masking: use maskSensitiveData() explicitly
Performance Considerations
| Feature | Impact | Recommendation |
|---------|--------|-----------------|
| Colorized output | Low (~2ms/log) | OK in dev, disable in production |
| File logging | Medium (~10ms/log) | Use in production, configure rotation |
| Logstash HTTP | High (~50-200ms/log) | Buffer logs, use async |
| ECS JSON formatting | Low (~5ms/log) | Use in production |
| Metadata serialization | Medium (~15ms/log) | Keep metadata small |
Exports
From @kyrasource/logger you can import:
`typescript
// Module & Service
export { LoggerModule, LoggerService }
// Configuration
export { loggerConfig }
// Types
export type { LoggerConfig, LogLevel, LogMetadata, LoggerOptions, FormattedLogEntry, ECSLog }
// Formatters
export { createColorizedFormatter, createSimpleColorizedFormatter, createFileFormatter }
export { createJsonFormatter, createECSFormatter, createCompactJsonFormatter }
// Utilities
export { serializeError, getErrorStack, isError, stringifyMetadata, mergeMetadata, stripAnsiCodes, maskSensitiveData }
``