Skip to content

Logging

Implementing Robust Logging and Error Management

Section titled “Implementing Robust Logging and Error Management”

Effective logging is crucial for debugging, monitoring, and auditing applications.

Logging Architecture
================================================================================
Application
|
v
+------------------+
| Logger | <-- Centralized logging
+------------------+
|
+--------+--------+--------+
| | | |
v v v v
+-------+ +-------+ +-------+ +-------+
|Console| | File | | Log | | Cloud |
| | | | | Service| |Watch |
+-------+ +-------+ +-------+ +-------+
Log Levels:
- error: Application errors
- warn: Warning conditions
- info: Informational messages
- debug: Debug information
- verbose: Detailed tracing
================================================================================

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { Logger } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['error', 'warn', 'log', 'debug', 'verbose'],
});
const logger = new Logger('Bootstrap');
await app.listen(3000);
logger.log(`Application running on port 3000`);
}
bootstrap();
src/common/logging/custom-logger.service.ts
import { Injectable, LoggerService } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as fs from 'fs';
import * as path from 'path';
@Injectable()
export class CustomLoggerService implements LoggerService {
private logLevel: number;
private logLevels = ['error', 'warn', 'log', 'debug', 'verbose'];
constructor(private configService: ConfigService) {
const level = configService.get('LOG_LEVEL', 'info');
this.logLevel = this.logLevels.indexOf(level);
}
error(message: any, trace?: string, context?: string) {
this.log('error', message, context, trace);
}
warn(message: any, context?: string) {
this.log('warn', message, context);
}
log(message: any, context?: string) {
this.log('log', message, context);
}
debug(message: any, context?: string) {
this.log('debug', message, context);
}
verbose(message: any, context?: string) {
this.log('verbose', message, context);
}
private log(
level: string,
message: any,
context?: string,
trace?: string,
) {
const levelIndex = this.logLevels.indexOf(level);
if (levelIndex > this.logLevel) {
return;
}
const timestamp = new Date().toISOString();
const formattedMessage = this.formatMessage(level, message, context, trace);
// Console output
this.logToConsole(level, formattedMessage);
// File output
this.logToFile(level, formattedMessage);
}
private formatMessage(
level: string,
message: any,
context?: string,
trace?: string,
): string {
const timestamp = new Date().toISOString();
const contextStr = context ? ` [${context}]` : '';
const traceStr = trace ? `\n${trace}` : '';
return `${timestamp}${contextStr} ${level.toUpperCase()}: ${message}${traceStr}`;
}
private logToConsole(level: string, message: string) {
switch (level) {
case 'error':
console.error('\x1b[31m%s\x1b[0m', message);
break;
case 'warn':
console.warn('\x1b[33m%s\x1b[0m', message);
break;
default:
console.log(message);
}
}
private logToFile(level: string, message: string) {
const logDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, `${level}.log`);
fs.appendFileSync(logFile, message + '\n');
}
}
// Register in main.ts
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
app.useLogger(new CustomLoggerService(app.get(ConfigService)));

src/common/logging/json-logger.service.ts
import { Injectable, LoggerService } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
interface LogEntry {
timestamp: string;
level: string;
message: string;
context?: string;
trace?: string;
userId?: string;
requestId?: string;
metadata?: Record<string, any>;
}
@Injectable()
export class JsonLoggerService implements LoggerService {
constructor(private configService: ConfigService) {}
error(message: any, trace?: string, context?: string) {
this.log('error', message, { context, trace });
}
warn(message: any, context?: string) {
this.log('warn', message, { context });
}
log(message: any, context?: string) {
this.log('info', message, { context });
}
debug(message: any, context?: string) {
this.log('debug', message, { context });
}
verbose(message: any, context?: string) {
this.log('trace', message, { context });
}
private log(
level: string,
message: any,
options: {
context?: string;
trace?: string;
metadata?: Record<string, any>;
} = {},
) {
const entry: LogEntry = {
timestamp: new Date().toISOString(),
level,
message: typeof message === 'string' ? message : JSON.stringify(message),
...options,
};
console.log(JSON.stringify(entry));
}
}
src/common/middleware/request-context.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { Logger } from '@nestjs/common';
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
private logger = new Logger('Request');
use(req: Request, res: Response, next: NextFunction) {
const requestId = uuidv4();
const startTime = Date.now();
// Attach context to request
(req as any).context = {
requestId,
userId: (req as any).user?.id,
};
// Log request
this.logger.log({
message: 'Incoming request',
requestId,
method: req.method,
url: req.url,
userAgent: req.headers['user-agent'],
ip: req.ip,
});
// Log response on finish
res.on('finish', () => {
const duration = Date.now() - startTime;
this.logger.log({
message: 'Request completed',
requestId,
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
});
});
next();
}
}

src/common/logging/query-logger.ts
import { Logger } from '@nestjs/common';
import { QueryRunner } from 'typeorm';
export class QueryLogger implements Logger {
private logger = new Logger('Database');
log(level: 'log' | 'info' | 'warn', message: any) {
this.logger[level](message);
}
logMigration(message: string) {
this.logger.log(message);
}
logQuery(query: string, parameters?: any[]) {
this.logger.debug({
query,
parameters: parameters?.length ? parameters : undefined,
});
}
logQueryError(error: string, query: string, parameters?: any[]) {
this.logger.error({
error,
query,
parameters: parameters?.length ? parameters : undefined,
});
}
logQuerySlow(time: number, query: string, parameters?: any[]) {
this.logger.warn({
message: 'Slow query detected',
duration: `${time}ms`,
query,
parameters: parameters?.length ? parameters : undefined,
});
}
logSchemaBuild(message: string) {
this.logger.log(message);
}
}
// Use in database config
export default new DataSource({
type: 'postgres',
// ... connection settings
logger: new QueryLogger(),
maxQueryExecutionTime: 1000, // Log slow queries
});

src/common/filters/all-exceptions.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { QueryFailedError } from 'typeorm';
interface ErrorResponse {
statusCode: number;
message: string;
error: string;
timestamp: string;
path: string;
requestId?: string;
details?: any;
}
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
private logger = new Logger('Exception');
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status: number;
let message: string;
let error: string;
let details: any;
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'object') {
message = (exceptionResponse as any).message || exception.message;
error = (exceptionResponse as any).error || 'Error';
details = (exceptionResponse as any).details;
} else {
message = exceptionResponse;
error = 'Error';
}
} else if (exception instanceof QueryFailedError) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = 'Database error';
error = 'Database Error';
// Handle specific database errors
const dbError = exception as any;
if (dbError.code === '23505') {
status = HttpStatus.CONFLICT;
message = 'Resource already exists';
error = 'Conflict';
} else if (dbError.code === '23503') {
status = HttpStatus.BAD_REQUEST;
message = 'Referenced resource not found';
error = 'Bad Request';
}
this.logger.error({
message: 'Database query failed',
query: dbError.query,
parameters: dbError.parameters,
error: dbError.message,
code: dbError.code,
});
} else if (exception instanceof Error) {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = 'Internal server error';
error = 'Internal Server Error';
this.logger.error({
message: exception.message,
stack: exception.stack,
});
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = 'Unknown error';
error = 'Internal Server Error';
}
const errorResponse: ErrorResponse = {
statusCode: status,
message,
error,
timestamp: new Date().toISOString(),
path: request.url,
requestId: (request as any).context?.requestId,
};
if (details) {
errorResponse.details = details;
}
response.status(status).json(errorResponse);
}
}
// Register globally
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new AllExceptionsFilter());
await app.listen(3000);
}
src/common/filters/typeorm-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Response } from 'express';
import { QueryFailedError, EntityNotFoundError } from 'typeorm';
@Catch(QueryFailedError, EntityNotFoundError)
export class TypeOrmExceptionFilter implements ExceptionFilter {
private logger = new Logger('TypeORM');
catch(exception: QueryFailedError | EntityNotFoundError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Database error';
if (exception instanceof QueryFailedError) {
const error = exception as any;
switch (error.code) {
case '23505': // Unique violation
status = HttpStatus.CONFLICT;
message = this.parseUniqueViolation(error);
break;
case '23503': // Foreign key violation
status = HttpStatus.BAD_REQUEST;
message = 'Referenced resource does not exist';
break;
case '23502': // Not null violation
status = HttpStatus.BAD_REQUEST;
message = 'Required field is missing';
break;
case '23514': // Check violation
status = HttpStatus.BAD_REQUEST;
message = 'Data validation failed';
break;
default:
this.logger.error({
message: 'Unhandled database error',
code: error.code,
detail: error.detail,
});
}
} else if (exception instanceof EntityNotFoundError) {
status = HttpStatus.NOT_FOUND;
message = 'Resource not found';
}
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
private parseUniqueViolation(error: any): string {
const detail = error.detail;
// Parse: "Key (email)=(test@example.com) already exists."
const match = detail.match(/Key \((.+?)\)=\((.+?)\) already exists/);
if (match) {
return `${match[1]} '${match[2]}' already exists`;
}
return 'Resource already exists';
}
}

src/common/exceptions/domain.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class DomainException extends HttpException {
constructor(
message: string,
public readonly code: string,
public readonly details?: any,
status: HttpStatus = HttpStatus.BAD_REQUEST,
) {
super(
{
message,
error: 'Domain Error',
code,
details,
},
status,
);
}
}
// src/common/exceptions/entity-not-found.exception.ts
import { NotFoundException } from '@nestjs/common';
export class EntityNotFoundException extends NotFoundException {
constructor(entityName: string, id: string | number) {
super({
message: `${entityName} with ID ${id} not found`,
error: 'Not Found',
code: 'ENTITY_NOT_FOUND',
});
}
}
// src/common/exceptions/duplicate-entity.exception.ts
import { ConflictException } from '@nestjs/common';
export class DuplicateEntityException extends ConflictException {
constructor(entityName: string, field: string, value: string) {
super({
message: `${entityName} with ${field} '${value}' already exists`,
error: 'Conflict',
code: 'DUPLICATE_ENTITY',
});
}
}
// Usage in service
@Injectable()
export class UsersService {
async findOne(id: number): Promise<User> {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new EntityNotFoundException('User', id);
}
return user;
}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.userRepository.findOne({
where: { email: dto.email },
});
if (existing) {
throw new DuplicateEntityException('User', 'email', dto.email);
}
return this.userRepository.save(dto);
}
}

src/users/users.service.ts
import {
Injectable,
Logger,
NotFoundException,
ConflictException,
InternalServerErrorException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, QueryFailedError } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
private logger = new Logger(UsersService.name);
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async create(dto: CreateUserDto): Promise<User> {
try {
const user = this.userRepository.create(dto);
return await this.userRepository.save(user);
} catch (error) {
if (error instanceof QueryFailedError) {
const dbError = error as any;
if (dbError.code === '23505') {
throw new ConflictException('Email already exists');
}
this.logger.error('Database error during user creation', {
error: dbError.message,
code: dbError.code,
});
}
throw new InternalServerErrorException('Failed to create user');
}
}
async findOne(id: number): Promise<User> {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async update(id: number, dto: UpdateUserDto): Promise<User> {
try {
const user = await this.findOne(id);
Object.assign(user, dto);
return await this.userRepository.save(user);
} catch (error) {
if (error instanceof NotFoundException) {
throw error;
}
this.logger.error(`Failed to update user ${id}`, error);
throw new InternalServerErrorException('Failed to update user');
}
}
}

Logging & Error Handling Quick Reference
+------------------------------------------------------------------+
| |
| Log Level | Use Case |
| -------------------|------------------------------------------|
| error | Application errors, exceptions |
| warn | Warning conditions, deprecated features |
| info | Important business events |
| debug | Debug information, query details |
| verbose | Detailed tracing |
| |
| Exception Type | HTTP Status |
| -------------------|------------------------------------------|
| BadRequestException| 400 |
| UnauthorizedException| 401 |
| ForbiddenException| 403 |
| NotFoundException| 404 |
| ConflictException| 409 |
| InternalServerErrorException| 500 |
| |
| Best Practices | Description |
| -------------------|------------------------------------------|
| Structured logging| Use JSON format for logs |
| Request context | Include requestId, userId |
| Global filters | Catch all exceptions |
| Custom exceptions| Domain-specific errors |
| Log sensitive data| Never log passwords, tokens |
| |
+------------------------------------------------------------------+

Chapter 39: Deployment Strategies


Last Updated: February 2026