From 7d6963e5a198549eeedd3ae10e8cdeed314d824f Mon Sep 17 00:00:00 2001 From: farjadakbar4 Date: Thu, 21 Mar 2024 14:20:11 +0500 Subject: [PATCH] feat: add error log module to save all errors in table. add launch.json for debugger --- .vscode/launch.json | 23 ++++++++++ src/app.module.ts | 10 +++++ src/error-logs/domain/logs.ts | 15 +++++++ .../document/document-persistence.module.ts | 21 +++++++++ .../document/entities/logs.schema.ts | 44 +++++++++++++++++++ .../document/mappers/logs.mapper.ts | 30 +++++++++++++ .../document/repositories/logs.repository.ts | 22 ++++++++++ .../persistence/logs.repository.ts | 7 +++ .../relational/entities/logs.entity.ts | 32 ++++++++++++++ .../relational/mappers/logs.mapper.ts | 30 +++++++++++++ .../relational-persistence.module.ts | 17 +++++++ .../repositories/logs.repository.ts | 24 ++++++++++ src/error-logs/logs.filter.ts | 39 ++++++++++++++++ src/error-logs/logs.module.ts | 20 +++++++++ src/error-logs/logs.service.ts | 22 ++++++++++ tsconfig.json | 5 ++- 16 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 .vscode/launch.json create mode 100644 src/error-logs/domain/logs.ts create mode 100644 src/error-logs/infrastructure/persistence/document/document-persistence.module.ts create mode 100644 src/error-logs/infrastructure/persistence/document/entities/logs.schema.ts create mode 100644 src/error-logs/infrastructure/persistence/document/mappers/logs.mapper.ts create mode 100644 src/error-logs/infrastructure/persistence/document/repositories/logs.repository.ts create mode 100644 src/error-logs/infrastructure/persistence/logs.repository.ts create mode 100644 src/error-logs/infrastructure/persistence/relational/entities/logs.entity.ts create mode 100644 src/error-logs/infrastructure/persistence/relational/mappers/logs.mapper.ts create mode 100644 src/error-logs/infrastructure/persistence/relational/relational-persistence.module.ts create mode 100644 src/error-logs/infrastructure/persistence/relational/repositories/logs.repository.ts create mode 100644 src/error-logs/logs.filter.ts create mode 100644 src/error-logs/logs.module.ts create mode 100644 src/error-logs/logs.service.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..9f6b28786 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Nest Debug", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "start:debug", + "--", + "--inspect-brk" + ], + "console": "integratedTerminal", + "restart": true, + "protocol": "auto", + "port": 9229, + "autoAttachChildProcesses": true, + "justMyCode": true + }, + ] +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 8372eea57..074155c25 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -30,6 +30,9 @@ import { MailerModule } from './mailer/mailer.module'; import { MongooseModule } from '@nestjs/mongoose'; import { MongooseConfigService } from './database/mongoose-config.service'; import { DatabaseConfig } from './database/config/database-config.type'; +import { LogsFilter } from './error-logs/logs.filter'; +import { APP_FILTER } from '@nestjs/core'; +import { LogsModule } from './error-logs/logs.module'; @Module({ imports: [ @@ -91,7 +94,14 @@ import { DatabaseConfig } from './database/config/database-config.type'; SessionModule, MailModule, MailerModule, + LogsModule, HomeModule, ], + providers: [ + { + provide: APP_FILTER, + useClass: LogsFilter, + }, + ], }) export class AppModule {} diff --git a/src/error-logs/domain/logs.ts b/src/error-logs/domain/logs.ts new file mode 100644 index 000000000..7614da40b --- /dev/null +++ b/src/error-logs/domain/logs.ts @@ -0,0 +1,15 @@ +import { Allow } from 'class-validator'; + +export class Logs { + @Allow() + id: string; + + @Allow() + path: string; + + message: JSON; + stack: JSON; + method: string; + payload?: JSON | null; + status: number; +} diff --git a/src/error-logs/infrastructure/persistence/document/document-persistence.module.ts b/src/error-logs/infrastructure/persistence/document/document-persistence.module.ts new file mode 100644 index 000000000..076db623b --- /dev/null +++ b/src/error-logs/infrastructure/persistence/document/document-persistence.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { LogsSchema, LogsSchemaClass } from './entities/logs.schema'; +import { LogsRepository } from '../logs.repository'; +import { LogsDocumentRepository } from './repositories/logs.repository'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: LogsSchemaClass.name, schema: LogsSchema }, + ]), + ], + providers: [ + { + provide: LogsRepository, + useClass: LogsDocumentRepository, + }, + ], + exports: [LogsRepository], +}) +export class DocumentLogsPersistenceModule {} diff --git a/src/error-logs/infrastructure/persistence/document/entities/logs.schema.ts b/src/error-logs/infrastructure/persistence/document/entities/logs.schema.ts new file mode 100644 index 000000000..729f1a026 --- /dev/null +++ b/src/error-logs/infrastructure/persistence/document/entities/logs.schema.ts @@ -0,0 +1,44 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { HydratedDocument, SchemaTypes, now } from 'mongoose'; +import { EntityDocumentHelper } from 'src/utils/document-entity-helper'; +import { Logs } from '@/error-logs/domain/logs'; + +export type LogsSchemaDocument = HydratedDocument; + +@Schema({ + timestamps: true, + toJSON: { + virtuals: true, + getters: true, + }, +}) +export class LogsSchemaClass extends EntityDocumentHelper implements Logs { + @Prop({ + type: SchemaTypes.String, + unique: true, + }) + id: string; + + @Prop() + path: string; + + @Prop({ type: Object }) + message: JSON; + + @Prop({ type: Object }) + stack: JSON; + + @Prop() + method: string; + + @Prop({ type: Object }) + payload?: JSON | null; + + @Prop() + status: number; + + @Prop({ default: now }) + timestamp: Date; +} + +export const LogsSchema = SchemaFactory.createForClass(LogsSchemaClass); diff --git a/src/error-logs/infrastructure/persistence/document/mappers/logs.mapper.ts b/src/error-logs/infrastructure/persistence/document/mappers/logs.mapper.ts new file mode 100644 index 000000000..58508a91d --- /dev/null +++ b/src/error-logs/infrastructure/persistence/document/mappers/logs.mapper.ts @@ -0,0 +1,30 @@ +import { Logs } from "@/error-logs/domain/logs"; +import { LogsSchemaClass } from '../entities/logs.schema'; + +export class LogsMapper { + static toDomain(raw: LogsSchemaClass): Logs { + const logs = new Logs(); + logs.id = raw.id; + logs.message = raw.message; + logs.method = raw.method; + logs.path = raw.path; + logs.payload = raw.payload; + logs.stack = raw.stack; + logs.status = raw.status; + + return logs; + } + + static toPersistence(logs: Logs): LogsSchemaClass { + const logsEntity = new LogsSchemaClass(); + + logsEntity.message = logs.message; + logsEntity.method = logs.method; + logsEntity.path = logs.path; + logsEntity.stack = logs.stack; + logsEntity.payload = logs.payload; + logsEntity.status = logs.status; + + return logsEntity; + } +} diff --git a/src/error-logs/infrastructure/persistence/document/repositories/logs.repository.ts b/src/error-logs/infrastructure/persistence/document/repositories/logs.repository.ts new file mode 100644 index 000000000..ec1d5c537 --- /dev/null +++ b/src/error-logs/infrastructure/persistence/document/repositories/logs.repository.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { LogsRepository } from '../../logs.repository'; +import { LogsSchemaClass } from '../entities/logs.schema'; // Assuming this is where your entity class is defined +import { Logs } from '@/error-logs/domain/logs'; +import { LogsMapper } from '../mappers/logs.mapper'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; + +@Injectable() +export class LogsDocumentRepository implements LogsRepository { + constructor( + @InjectModel(LogsSchemaClass.name) + private readonly logsModel: Model, + ) {} + + async create(data: Logs): Promise { + const persistenceModel = LogsMapper.toPersistence(data); + const createdLog = new this.logsModel(persistenceModel); + const logObject = await createdLog.save(); + return LogsMapper.toDomain(logObject); + } +} diff --git a/src/error-logs/infrastructure/persistence/logs.repository.ts b/src/error-logs/infrastructure/persistence/logs.repository.ts new file mode 100644 index 000000000..48e7d1d60 --- /dev/null +++ b/src/error-logs/infrastructure/persistence/logs.repository.ts @@ -0,0 +1,7 @@ +import { Logs } from '@/error-logs/domain/logs'; + +export abstract class LogsRepository { + abstract create( + data: Omit, + ): Promise; +} diff --git a/src/error-logs/infrastructure/persistence/relational/entities/logs.entity.ts b/src/error-logs/infrastructure/persistence/relational/entities/logs.entity.ts new file mode 100644 index 000000000..e0d332e91 --- /dev/null +++ b/src/error-logs/infrastructure/persistence/relational/entities/logs.entity.ts @@ -0,0 +1,32 @@ +import { Column, CreateDateColumn, Entity, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm'; +import { EntityRelationalHelper } from 'src/utils/relational-entity-helper'; +import { Logs } from '@/error-logs/domain/logs'; + +@Entity({ + name: 'logs', +}) +export class LogsEntity extends EntityRelationalHelper implements Logs { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column({ type: 'text' }) + path: string; + + @Column({ type: 'json' }) + message: JSON; + + @Column({ type: 'json' }) + stack: JSON; + + @Column({ type: 'text' }) + method: string; + + @Column({ type: 'json', nullable: true }) + payload?: JSON | null; + + @Column({ type: 'integer' }) + status: number; + + @CreateDateColumn() + timestamp: Date; +} diff --git a/src/error-logs/infrastructure/persistence/relational/mappers/logs.mapper.ts b/src/error-logs/infrastructure/persistence/relational/mappers/logs.mapper.ts new file mode 100644 index 000000000..f49952aa0 --- /dev/null +++ b/src/error-logs/infrastructure/persistence/relational/mappers/logs.mapper.ts @@ -0,0 +1,30 @@ +import { Logs } from "@/error-logs/domain/logs"; +import { LogsEntity } from "../entities/logs.entity"; + +export class LogsMapper { + static toDomain(raw: LogsEntity): Logs { + const logs = new Logs(); + logs.id = raw.id; + logs.message = raw.message; + logs.method = raw.method; + logs.path = raw.path; + logs.payload = raw.payload; + logs.stack = raw.stack; + logs.status = raw.status; + + return logs; + } + + static toPersistence(logs: Logs): LogsEntity { + const logsEntity = new LogsEntity(); + + logsEntity.message = logs.message; + logsEntity.method = logs.method; + logsEntity.path = logs.path; + logsEntity.stack = logs.stack; + logsEntity.payload = logs.payload; + logsEntity.status = logs.status; + + return logsEntity; + } +} diff --git a/src/error-logs/infrastructure/persistence/relational/relational-persistence.module.ts b/src/error-logs/infrastructure/persistence/relational/relational-persistence.module.ts new file mode 100644 index 000000000..09dedfc7a --- /dev/null +++ b/src/error-logs/infrastructure/persistence/relational/relational-persistence.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { LogsEntity } from './entities/logs.entity'; +import { LogsRepository } from '../logs.repository'; +import { LogsRelationalRepository } from './repositories/logs.repository'; + +@Module({ + imports: [TypeOrmModule.forFeature([LogsEntity])], + providers: [ + { + provide: LogsRepository, + useClass: LogsRelationalRepository, + }, + ], + exports: [LogsRepository], +}) +export class RelationalLogsPersistenceModule {} diff --git a/src/error-logs/infrastructure/persistence/relational/repositories/logs.repository.ts b/src/error-logs/infrastructure/persistence/relational/repositories/logs.repository.ts new file mode 100644 index 000000000..c32ebfeda --- /dev/null +++ b/src/error-logs/infrastructure/persistence/relational/repositories/logs.repository.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { LogsRepository } from '../../logs.repository'; +import { LogsEntity } from '../entities/logs.entity'; +import { Logs } from '@/error-logs/domain/logs'; +import { LogsMapper } from '../mappers/logs.mapper'; + + +@Injectable() +export class LogsRelationalRepository implements LogsRepository { + constructor( + @InjectRepository(LogsEntity) + private readonly logsRepository: Repository, + ) {} + + async create(data: Logs): Promise { + const persistenceModel = LogsMapper.toPersistence(data); + const newEntity = await this.logsRepository.save( + this.logsRepository.create(persistenceModel), + ); + return LogsMapper.toDomain(newEntity); + } +} diff --git a/src/error-logs/logs.filter.ts b/src/error-logs/logs.filter.ts new file mode 100644 index 000000000..41f08b237 --- /dev/null +++ b/src/error-logs/logs.filter.ts @@ -0,0 +1,39 @@ +import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common'; +import { HttpException } from '@nestjs/common/exceptions'; +import { LogService } from './logs.service'; + +@Catch() +export class LogsFilter implements ExceptionFilter { + constructor(private readonly errorLoggerService: LogService) {} + + async catch(exception: any, host: ArgumentsHost) { + if (exception) { + // Exception occurred, log it + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const message = (exception instanceof HttpException ? exception.getResponse() : 'Internal Server Error') as JSON; + const stack = exception.stack; + const path = request.url; + const requestBody = request.body; + const method = request.method; + + // Send the response based on the original exception status + const status = exception instanceof HttpException ? exception.getStatus() : 500; + + this.errorLoggerService.logError(path, message, stack, method, status, requestBody); + + if (status === HttpStatus.INTERNAL_SERVER_ERROR) { + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + message: 'Internal Server Error', + }); + } else { + // For non-500 status codes, maintain the original response status + response.status(status).json(exception.getResponse()); + } + } + } +} diff --git a/src/error-logs/logs.module.ts b/src/error-logs/logs.module.ts new file mode 100644 index 000000000..5e9958528 --- /dev/null +++ b/src/error-logs/logs.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; +import databaseConfig from '@/database/config/database.config'; +import { DatabaseConfig } from '@/database/config/database-config.type'; +import { LogService } from './logs.service'; +import { RelationalLogsPersistenceModule } from './infrastructure/persistence/relational/relational-persistence.module'; +import { DocumentLogsPersistenceModule } from './infrastructure/persistence/document/document-persistence.module'; + + +const infrastructurePersistenceModule = (databaseConfig() as DatabaseConfig) + .isDocumentDatabase + ? DocumentLogsPersistenceModule + : RelationalLogsPersistenceModule; + +@Module({ + imports: [infrastructurePersistenceModule], + providers: [LogService], + exports: [LogService, infrastructurePersistenceModule], +}) + +export class LogsModule {} \ No newline at end of file diff --git a/src/error-logs/logs.service.ts b/src/error-logs/logs.service.ts new file mode 100644 index 000000000..0e3001b18 --- /dev/null +++ b/src/error-logs/logs.service.ts @@ -0,0 +1,22 @@ +// error-logger.service.ts +import { Injectable } from '@nestjs/common'; +import { Logs } from './domain/logs'; +import { LogsRepository } from './infrastructure/persistence/logs.repository'; + +@Injectable() +export class LogService { + constructor( + private readonly logsRepository: LogsRepository, + ) {} + + async logError(path: string, message: JSON, stack: JSON, method: string, status: number, payload?: JSON): Promise { + return await this.logsRepository.create({ + path, + message, + stack, + method, + status, + payload + }) + } +} diff --git a/tsconfig.json b/tsconfig.json index 4da464eba..5c34b7b71 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,9 @@ "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, - "esModuleInterop": true + "esModuleInterop": true, + "paths": { + "@/*": ["src/*"], + } } }