npm package: @kyrasource/logger
Description: Production-ready Winston logger module for NestJS with colorized dev output, structured JSON/ECS in production, and file logging
Author: Kyra Source
Latest version: 1.0.1

# @kyrasource/logger

Production-ready structured logging for NestJS applications using Winston. Ships with **colorized development output**, **ECS-compliant JSON in production**, **automatic file logging**, and **optional Logstash integration**.

## ✨ Features

- ✅ **Colorized Development Logs** - Human-readable console output with colors, badges, and context
- ✅ **Structured JSON/ECS Production** - Elasticsearch-compatible logs for Logstash and Kibana
- ✅ **File Logging** - Automatic rotation with separate error and combined logs
- ✅ **Error Serialization** - Full stack traces and error context
- ✅ **Request Tracking** - Request IDs and user context across logs
- ✅ **Metadata Support** - Flexible key-value metadata attachment
- ✅ **Logstash Integration** - Optional HTTP transport to centralized logging
- ✅ **Sensitive Data Masking** - Automatic masking of passwords, tokens, API keys
- ✅ **TypeScript Support** - Full type safety with interfaces
- ✅ **Zero External Dependencies** - Uses only Winston (peer dependency)

## Installation

```bash
npm install @kyrasource/logger
```

## Quick Start

### 1. Import in Your App Module

```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 {}
```

### 2. Configure via `.env`

```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. Inject and Use

```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

### Development (Colorized)

```
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
  }
```

### Production (ECS JSON)

```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

### LoggerService Methods

#### 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' }
);
```

### LoggerOptions Interface

```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
}
```

### Example: Full-Featured Logging

```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

### Environment Variables

```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
```

### Programmatic Configuration

```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

### Global Request Logging Middleware

```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('*');
  }
}
```

### Database Query Logging

```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;
    }
  }
}
```

### Error Handling with Logging

```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<Request>();
    const response = ctx.getResponse<Response>();

    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
```

### Rotation Policy

- **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

### Custom Formatters

```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');
```

### Direct Winston Access

```typescript
const logger = loggerService.getWinstonLogger();

// Access underlying Winston logger if needed
logger.info('Direct message', { custom: 'field' });
```

### Error Utilities

```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('\x1b[31mRed text\x1b[0m');
```

## Troubleshooting

### No logs appearing in console

- Check `LOG_CONSOLE_ENABLED` is `true` (default)
- Check `LOGGER_ENABLED` is `true` (default)
- Check `LOG_LEVEL` includes your log level

### Logs not rotating

- 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)

### Logstash not receiving logs

- 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

### High memory usage

- 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`

### Sensitive data not masked

- 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 }
```

## License

MIT - See LICENSE file

## Support

For issues, feature requests, or documentation:
- GitHub: [kyrasource/logger](https://github.com/kyrasource/logger)
- npm: [@kyrasource/logger](https://www.npmjs.com/package/@kyrasource/logger)
- Docs: See [LOGGING_GUIDE.md](./LOGGING_GUIDE.md) for advanced patterns